├── .browserslistrc ├── postcss.config.js ├── .vscode └── settings.json ├── src ├── assets │ ├── icon │ │ ├── 16.png │ │ ├── 48.png │ │ └── 128.png │ └── styles │ │ ├── popup.scss │ │ ├── watcher │ │ ├── LiveRoomHelper.scss │ │ └── GetPic.scss │ │ ├── variables.scss │ │ ├── module │ │ ├── SubscribeChannel.scss │ │ ├── HotKeyMenu.scss │ │ ├── StickerHistory.scss │ │ └── RetrieveInvalidVideo.scss │ │ ├── variables.ts │ │ ├── index.scss │ │ ├── options.scss │ │ ├── global.scss │ │ └── iconfont.css ├── _locales │ ├── zh_CN │ │ └── messages.json │ ├── en │ │ └── messages.json │ └── ja │ │ └── messages.json ├── typings │ ├── vue-shims.d.ts │ ├── global.d.ts │ ├── vue.d.ts │ └── hotKeyMenu.d.ts ├── OptionsInit │ ├── index.ts │ ├── OLiveRoomHelper.ts │ └── OSubscribeChannel.ts ├── scripts │ ├── base │ │ ├── interface │ │ │ ├── IRetrieveInvalidVideo.ts │ │ │ ├── IPopup.ts │ │ │ ├── IMultipleAccounts.ts │ │ │ └── IOptions.ts │ │ ├── storage │ │ │ ├── template │ │ │ │ ├── index.ts │ │ │ │ ├── TPopup.ts │ │ │ │ ├── TemplateBase.ts │ │ │ │ ├── TMultipleAccounts.ts │ │ │ │ ├── TLiveRoomHelper.ts │ │ │ │ ├── TComment.ts │ │ │ │ ├── TRetrieveInvalidVideo.ts │ │ │ │ └── TSubscribeChannel.ts │ │ │ ├── ExtStorage.ts │ │ │ └── extStorage.ts │ │ ├── enums │ │ │ ├── OptionsType.ts │ │ │ └── ContentJsType.ts │ │ ├── singletonBase │ │ │ └── singleton.ts │ │ ├── request │ │ │ └── index.ts │ │ ├── Util.ts │ │ ├── HotKeyMenu.ts │ │ └── IconUtil.ts │ └── module │ │ ├── index.ts │ │ ├── ModuleBase.ts │ │ ├── SubscribeChannel.ts │ │ ├── RetrieveInvalidVideo.ts │ │ └── StickerHistory.ts ├── Listener │ ├── index.ts │ ├── ResourceListListener.ts │ ├── CommentListener.ts │ ├── ListenerBase.ts │ └── ChannelListener.ts ├── api │ ├── index.ts │ ├── JijiDown.ts │ ├── BiliPlus.ts │ ├── BilibiliPassport.ts │ └── BilibiliApi.ts ├── Watcher │ ├── index.ts │ ├── WatcherBase.ts │ ├── LiveRoomHelper.ts │ ├── HaiLinHappy.ts │ ├── LinkConverter.ts │ └── GetPicWatcher.ts ├── background │ ├── background.html │ ├── background.ts │ └── scripts │ │ └── Account.ts ├── popup │ ├── popup.html │ ├── store │ │ ├── index.ts │ │ └── modules │ │ │ └── user.ts │ ├── popup.ts │ ├── router.ts │ ├── index.vue │ └── views │ │ ├── SubscribeChannel.vue │ │ └── MultipleAccounts.vue ├── options │ ├── options.html │ ├── options.ts │ ├── views │ │ ├── DevelopmentHelper.vue │ │ ├── Home.vue │ │ ├── LiveRoomHelper.vue │ │ └── SubscribeChannel.vue │ ├── index.vue │ └── router.ts ├── manifest.json ├── btools.ts └── components │ └── Navbar.vue ├── .prettierrc.json ├── dev ├── iconfont │ └── index.js └── zip │ └── index.js ├── .gitignore ├── .babelrc ├── README.md ├── tsconfig.json ├── LICENSE ├── .eslintrc.js ├── package.json └── webpack.config.ts /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require('autoprefixer')] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "node_modules": true 4 | } 5 | } -------------------------------------------------------------------------------- /src/assets/icon/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imba97/Btools-vue/HEAD/src/assets/icon/16.png -------------------------------------------------------------------------------- /src/assets/icon/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imba97/Btools-vue/HEAD/src/assets/icon/48.png -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "none" 5 | } -------------------------------------------------------------------------------- /src/assets/icon/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imba97/Btools-vue/HEAD/src/assets/icon/128.png -------------------------------------------------------------------------------- /src/_locales/zh_CN/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginDesc": {"message": "逼砣,以B站为主增强网站功能,优化网站浏览体验。致力于简洁实用方便。"} 3 | } 4 | -------------------------------------------------------------------------------- /src/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginDesc": {"message": "Bilibili tools,increase user experience"} 3 | } 4 | -------------------------------------------------------------------------------- /src/typings/vue-shims.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue' 3 | export default Vue 4 | } 5 | -------------------------------------------------------------------------------- /src/OptionsInit/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@/OptionsInit/OSubscribeChannel' 2 | export * from '@/OptionsInit/OLiveRoomHelper' 3 | -------------------------------------------------------------------------------- /src/assets/styles/popup.scss: -------------------------------------------------------------------------------- 1 | @import '~@/assets/styles/iconfont.css'; 2 | 3 | * { 4 | margin: 0; 5 | padding: 0; 6 | } 7 | -------------------------------------------------------------------------------- /src/assets/styles/watcher/LiveRoomHelper.scss: -------------------------------------------------------------------------------- 1 | .miniPlayerHide[class~='minimal'] { 2 | transform: translate(2000px, 0px) !important; 3 | } 4 | -------------------------------------------------------------------------------- /src/_locales/ja/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginDesc": {"message": "ビリビリ ツールズ、Bilibili動画を主に、ウェーブサイトの機能を強化し、ユーザー体験を最適化する。簡潔、実用、便利にさせたいと思います。"} 3 | } 4 | -------------------------------------------------------------------------------- /src/scripts/base/interface/IRetrieveInvalidVideo.ts: -------------------------------------------------------------------------------- 1 | export interface IRetrieveInvalidVideoCommonHMK { 2 | mid: string 3 | aid: string 4 | } 5 | -------------------------------------------------------------------------------- /src/assets/styles/variables.scss: -------------------------------------------------------------------------------- 1 | $primary-color: #f25d8e; 2 | 3 | $error-color: #e73a38; 4 | $success-color: #63b931; 5 | $info-color: #ccc 6 | 7 | -------------------------------------------------------------------------------- /src/typings/global.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Window { 3 | // 直播间 打开封面 用 4 | __NEPTUNE_IS_MY_WAIFU__: any 5 | } 6 | } 7 | 8 | export {} 9 | -------------------------------------------------------------------------------- /dev/iconfont/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const IconfontBuilder = require('simple-iconfont-builder') 3 | 4 | IconfontBuilder.build( 5 | path.resolve(__dirname, '../../src/assets/styles/iconfont.css') 6 | ) 7 | -------------------------------------------------------------------------------- /src/scripts/base/storage/template/index.ts: -------------------------------------------------------------------------------- 1 | export * from './TRetrieveInvalidVideo' 2 | 3 | export * from './TComment' 4 | 5 | export * from './TSubscribeChannel' 6 | 7 | export * from './TLiveRoomHelper' 8 | 9 | export {} 10 | -------------------------------------------------------------------------------- /src/Listener/index.ts: -------------------------------------------------------------------------------- 1 | export { ResourceListListener } from '@/Listener/ResourceListListener' 2 | export { CommentListener } from '@/Listener/CommentListener' 3 | export { ChannelListener } from '@/Listener/ChannelListener' 4 | 5 | export {} 6 | -------------------------------------------------------------------------------- /src/scripts/base/enums/OptionsType.ts: -------------------------------------------------------------------------------- 1 | export enum OptionsType { 2 | /** 3 | * 文本(鼠标点选) 4 | */ 5 | Text, 6 | 7 | /** 8 | * 输入框 9 | */ 10 | Input, 11 | 12 | /** 13 | * 密码框 14 | */ 15 | Password 16 | } 17 | -------------------------------------------------------------------------------- /src/scripts/base/enums/ContentJsType.ts: -------------------------------------------------------------------------------- 1 | export enum RequestApiType { 2 | /** 3 | * 收藏夹 4 | */ 5 | ResourceList, 6 | 7 | /** 8 | * 评论 9 | */ 10 | Reply, 11 | 12 | /** 13 | * 订阅频道 14 | */ 15 | Channel 16 | } 17 | -------------------------------------------------------------------------------- /src/assets/styles/module/SubscribeChannel.scss: -------------------------------------------------------------------------------- 1 | .btools-subscribe-button { 2 | position: absolute; 3 | top: 0; 4 | left: -40px; 5 | width: 30px; 6 | height: 30px; 7 | background: none; 8 | border: 0; 9 | cursor: pointer; 10 | } 11 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import BilibiliApi from './BilibiliApi' 2 | import BilibiliPassport from './BilibiliPassport' 3 | import BiliPlus from './BiliPlus' 4 | import JijiDown from './JijiDown' 5 | 6 | export { BilibiliApi, BilibiliPassport, BiliPlus, JijiDown } 7 | -------------------------------------------------------------------------------- /src/scripts/base/singletonBase/singleton.ts: -------------------------------------------------------------------------------- 1 | export default class Singleton { 2 | public static Instance(this: new () => T): T { 3 | if (!(this as any).instance) (this as any).instance = new this() 4 | return (this as any).instance 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/scripts/module/index.ts: -------------------------------------------------------------------------------- 1 | export { RetrieveInvalidVideo } from '@/scripts/module/RetrieveInvalidVideo' 2 | export { StickerHistory } from '@/scripts/module/StickerHistory' 3 | export { SubscribeChannel } from '@/scripts/module/SubscribeChannel' 4 | 5 | export {} 6 | -------------------------------------------------------------------------------- /src/Watcher/index.ts: -------------------------------------------------------------------------------- 1 | export { GetPicWatcher } from '@/Watcher/GetPicWatcher' 2 | export { LinkConverter } from '@/Watcher/LinkConverter' 3 | export { LiveRoomHelper } from '@/Watcher/LiveRoomHelper' 4 | export { HaiLinHappy } from '@/Watcher/HaiLinHappy' 5 | 6 | export {} 7 | -------------------------------------------------------------------------------- /src/assets/styles/variables.ts: -------------------------------------------------------------------------------- 1 | import Color from 'color' 2 | 3 | /** 主色 */ 4 | export const primaryColor = Color('#f25d8e') 5 | 6 | /** 成功颜色 */ 7 | export const successColor = Color('#e73a38') 8 | 9 | /** 错误颜色 */ 10 | export const errorColor = Color('#63b931') 11 | -------------------------------------------------------------------------------- /src/scripts/base/interface/IPopup.ts: -------------------------------------------------------------------------------- 1 | import { IVideoData } from '@/scripts/base/storage/template' 2 | 3 | export interface IChannelList { 4 | [uid: number]: { 5 | [sid: number]: { 6 | title: string 7 | videos: IVideoData[] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/typings/vue.d.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | declare module 'vue/types/vue' { 4 | // 3. 声明为 Vue 补充的东西 5 | 6 | interface Vue { 7 | // $chrome: typeof chrome 8 | } 9 | 10 | interface VueConstructor { 11 | // chrome: typeof chrome 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/background/background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | background 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/popup/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | popup 7 | 8 | 9 |
10 | 11 | -------------------------------------------------------------------------------- /src/options/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | options 7 | 8 | 9 |
10 | 11 | -------------------------------------------------------------------------------- /src/popup/store/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import { IUserState } from '@/popup/store/modules/user' 4 | 5 | Vue.use(Vuex) 6 | 7 | export interface IRootState { 8 | user: IUserState 9 | } 10 | 11 | export default new Vuex.Store({}) 12 | -------------------------------------------------------------------------------- /src/popup/popup.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Popup from '@/popup/index.vue' 3 | 4 | import '@styles/global' 5 | import '@styles/popup' 6 | 7 | import router from '@/popup/router' 8 | 9 | export default new Vue({ 10 | router, 11 | render: (h) => h(Popup) 12 | }).$mount('#app') 13 | -------------------------------------------------------------------------------- /src/options/options.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Options from '@/options/index.vue' 3 | 4 | import '@styles/global' 5 | import '@styles/options' 6 | 7 | import router from '@/options/router' 8 | 9 | export default new Vue({ 10 | router, 11 | render: (h) => h(Options) 12 | }).$mount('#app') 13 | -------------------------------------------------------------------------------- /dev/zip/index.js: -------------------------------------------------------------------------------- 1 | const { zip } = require('zip-a-folder') 2 | const path = require('path') 3 | 4 | ;(async function () { 5 | console.log('开始压缩') 6 | const zipPath = path.resolve(__dirname, `../../${process.argv[2]}.zip`) 7 | await zip('./Build', zipPath) 8 | console.log(`压缩成功 ${zipPath}`) 9 | })() 10 | -------------------------------------------------------------------------------- /src/scripts/module/ModuleBase.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 模块基类 3 | */ 4 | 5 | export default abstract class ModuleBase { 6 | private _name: string 7 | public constructor() { 8 | this._name = (this as any).__proto__.constructor.name 9 | this.handle() 10 | } 11 | 12 | protected abstract handle(): void 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /Build 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | # .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /src/typings/hotKeyMenu.d.ts: -------------------------------------------------------------------------------- 1 | declare interface HotKeyMenuOption { 2 | /** 快捷键菜单每一项的按键值 */ 3 | key: string 4 | 5 | /** 快捷键菜单每一项的显示文字 */ 6 | title: string 7 | 8 | /** 快捷键菜单每一项执行的函数 */ 9 | action: (overlordElement: HTMLElement) => void 10 | 11 | /** 快捷键菜单每一项添加到的位置(可选) */ 12 | position?: string 13 | } 14 | -------------------------------------------------------------------------------- /src/popup/store/modules/user.ts: -------------------------------------------------------------------------------- 1 | import { 2 | VuexModule, 3 | Module, 4 | Action, 5 | Mutation, 6 | getModule 7 | } from 'vuex-module-decorators' 8 | import store from '@/popup/store' 9 | 10 | export interface IUserState {} 11 | 12 | @Module({ dynamic: true, store, name: 'user' }) 13 | class User extends VuexModule implements IUserState {} 14 | 15 | export const UserModule = getModule(User) 16 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "modules": false, 7 | "targets": {"esmodules": true}, 8 | "useBuiltIns": "entry", 9 | "corejs": 3 10 | } 11 | ] 12 | ], 13 | "plugins": [ 14 | ["@babel/plugin-transform-async-to-generator", { 15 | "module": "bluebird", 16 | "method": "coroutine" 17 | }] 18 | ] 19 | } -------------------------------------------------------------------------------- /src/assets/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import '~@/assets/styles/variables.scss'; 2 | 3 | @import '~@/assets/styles/module/HotKeyMenu.scss'; 4 | 5 | @import '~@/assets/styles/module/RetrieveInvalidVideo.scss'; 6 | @import '~@/assets/styles/module/StickerHistory.scss'; 7 | @import '~@/assets/styles/module/SubscribeChannel.scss'; 8 | 9 | @import '~@/assets/styles/watcher/GetPic.scss'; 10 | @import '~@/assets/styles/watcher/LiveRoomHelper.scss'; 11 | -------------------------------------------------------------------------------- /src/scripts/base/storage/template/TPopup.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Popup 存储模板 3 | */ 4 | 5 | import { TemplateBase } from '@/scripts/base/storage/template/TemplateBase' 6 | 7 | /** 8 | * Popup 配置项 9 | */ 10 | export interface IPopup extends Object { 11 | /** 12 | * 当前 route path 13 | */ 14 | routePath?: string 15 | } 16 | 17 | export class TPopup extends TemplateBase { 18 | constructor(data: IPopup) { 19 | super(data) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/scripts/base/interface/IMultipleAccounts.ts: -------------------------------------------------------------------------------- 1 | export interface IAccountItem { 2 | /** 3 | * 用户ID 4 | */ 5 | uid: string 6 | 7 | /** 8 | * 用户名 9 | */ 10 | name: string 11 | 12 | /** 13 | * 头像 14 | */ 15 | avatar: string 16 | 17 | /** 18 | * token 19 | */ 20 | token: string 21 | 22 | /** 23 | * DedeUserID__ckMd5 24 | */ 25 | ckMd5: string 26 | 27 | /** 28 | * csrf 29 | */ 30 | csrf: string 31 | } 32 | -------------------------------------------------------------------------------- /src/api/JijiDown.ts: -------------------------------------------------------------------------------- 1 | import { ParsedUrlQueryInput } from 'querystring' 2 | import Request from '@/scripts/base/request' 3 | 4 | export default class JijiDown extends Request { 5 | constructor() { 6 | super() 7 | 8 | this.baseUrl = 'https://www.jijidown.com/api' 9 | } 10 | 11 | public videoInfo(id: string) { 12 | return this.request({ 13 | url: '/v1/video/get_info', 14 | method: 'GET', 15 | data: { 16 | id 17 | } 18 | }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/assets/styles/options.scss: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | .container { 7 | width: 100%; 8 | height: 500px; 9 | 10 | .title { 11 | text-align: center; 12 | color: #666; 13 | } 14 | .options { 15 | li { 16 | width: 100%; 17 | height: 30px; 18 | line-height: 30px; 19 | font-size: 20px; 20 | user-select: none; 21 | cursor: pointer; 22 | 23 | .name { 24 | padding-left: 20px; 25 | } 26 | 27 | .value { 28 | color: #f66; 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Listener/ResourceListListener.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 监听器:收藏夹 3 | */ 4 | 5 | import ListenerBase from '@/Listener/ListenerBase' 6 | import { RequestApiType } from '@/scripts/base/enums/ContentJsType' 7 | 8 | export class ResourceListListener extends ListenerBase { 9 | init() { 10 | this.urls = ['*://api.bilibili.com/x/v3/fav/resource/list*'] 11 | super.init() 12 | } 13 | 14 | handle() { 15 | this.sendToContentJs( 16 | { 17 | type: RequestApiType.ResourceList, 18 | tabId: this.tabId 19 | }, 20 | (response) => {} 21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Btools 2 | 3 | 用 Vue + TypeScript 重构插件 4 | 5 | # Build 6 | 7 | ```shell 8 | npm i 9 | # development Chrome 10 | npm run c 11 | # development Firefox 12 | npm run f 13 | # production Chrome 14 | npm run build:chrome 15 | # production Firefox 16 | npm run build:firefox 17 | ``` 18 | 19 | # Todo List 20 | 21 | - [x] 快捷键菜单 22 | - [x] 找回失效视频 23 | - [x] 历史表情 24 | - [x] 订阅频道 25 | - [x] 自定义颜文字 26 | - [x] 打开封面 27 | - [x] 网址链接转换 28 | 29 | - [x] 专栏助手 30 | 31 | - [x] 打开原图 32 | - [x] 新窗口打开原图 33 | 34 | - [x] 直播间助手 35 | 36 | - [x] 隐藏迷你播放器 37 | - [ ] ~~隐藏 PK 分数~~ 38 | 39 | - [x] 多帐号切换 -------------------------------------------------------------------------------- /src/scripts/base/storage/template/TemplateBase.ts: -------------------------------------------------------------------------------- 1 | export class TemplateBase { 2 | private _name: string 3 | 4 | protected _data: Object 5 | 6 | public constructor(data: Object) { 7 | this._data = data 8 | this._name = (this as any).__proto__.constructor.name 9 | } 10 | 11 | public GetName(): string { 12 | return this._name 13 | } 14 | 15 | public GetData(): Object { 16 | return this._data 17 | } 18 | 19 | public SetData(data: Object): void { 20 | this._data = data 21 | } 22 | } 23 | 24 | export interface ITemplateBase extends Object { 25 | setting?: { [key: string]: any } 26 | } 27 | -------------------------------------------------------------------------------- /src/Listener/CommentListener.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 监听器:评论 3 | */ 4 | 5 | import ListenerBase from '@/Listener/ListenerBase' 6 | import { RequestApiType } from '@/scripts/base/enums/ContentJsType' 7 | 8 | export class CommentListener extends ListenerBase { 9 | init() { 10 | this.urls = [ 11 | '*://api.bilibili.com/x/v2/reply/main*', 12 | '*://api.bilibili.com/x/v2/reply/jump*' 13 | ] 14 | super.init() 15 | } 16 | 17 | handle() { 18 | this.sendToContentJs( 19 | { 20 | type: RequestApiType.Reply, 21 | tabId: this.tabId 22 | }, 23 | (response) => {} 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/scripts/base/storage/template/TMultipleAccounts.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 多帐号 存储模板 3 | */ 4 | 5 | import { TemplateBase } from '@/scripts/base/storage/template/TemplateBase' 6 | import { IAccountItem } from '@/scripts/base/interface/IMultipleAccounts' 7 | 8 | /** 9 | * 多帐号 配置项 10 | */ 11 | export interface IMultipleAccounts extends Object { 12 | /** 13 | * 当前登录账号 UID 14 | */ 15 | currentAccount?: string 16 | 17 | /** 18 | * 用户列表 19 | */ 20 | userList?: IAccountItem[] 21 | } 22 | 23 | export class TMultipleAccounts extends TemplateBase { 24 | constructor(data: IMultipleAccounts) { 25 | super(data) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/scripts/base/interface/IOptions.ts: -------------------------------------------------------------------------------- 1 | import { OptionsType } from '@/scripts/base/enums/OptionsType' 2 | 3 | export interface IBtoolsConfigsOptions extends Object { 4 | /** 5 | * 选项名称 6 | */ 7 | name: string 8 | 9 | /** 10 | * 值 11 | */ 12 | value: T 13 | 14 | /** 15 | * 选项类型 16 | */ 17 | type: OptionsType 18 | } 19 | 20 | /** 21 | * 配置项 22 | */ 23 | export interface IBtoolsOptions { 24 | /** 25 | * 配置项名称 26 | */ 27 | name?: string 28 | 29 | /** 30 | * 选项值 31 | */ 32 | values?: IBtoolsConfigsOptions[] 33 | 34 | /** 35 | * 当前选项值 36 | */ 37 | current?: IBtoolsConfigsOptions 38 | } 39 | -------------------------------------------------------------------------------- /src/api/BiliPlus.ts: -------------------------------------------------------------------------------- 1 | import { ParsedUrlQueryInput } from 'querystring' 2 | import Request from '@/scripts/base/request' 3 | 4 | export default class BiliPlus extends Request { 5 | constructor() { 6 | super() 7 | 8 | this.baseUrl = 'https://www.biliplus.com/api' 9 | } 10 | 11 | public videoInfo(aid: string) { 12 | return this.request({ 13 | url: '/aidinfo', 14 | method: 'GET', 15 | data: { 16 | aid 17 | } 18 | }) 19 | } 20 | 21 | public videoDetail(id: string) { 22 | return this.request({ 23 | url: '/view', 24 | method: 'GET', 25 | data: { 26 | id 27 | } 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/scripts/base/storage/template/TLiveRoomHelper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 直播间助手存储模板 3 | */ 4 | 5 | import { TemplateBase } from '@/scripts/base/storage/template/TemplateBase' 6 | import { IBtoolsOptions } from '@/scripts/base/interface/IOptions' 7 | 8 | /** 9 | * 直播间助手 配置项 10 | */ 11 | export interface ILiveRoomHelperOptions extends Object { 12 | /** 13 | * 迷你播放器是否显示 14 | */ 15 | miniPlayer: IBtoolsOptions | null 16 | } 17 | 18 | export interface ILiveRoomHelper extends Object { 19 | setting?: ILiveRoomHelperOptions 20 | } 21 | 22 | export class TLiveRoomHelper extends TemplateBase { 23 | constructor(data: ILiveRoomHelper) { 24 | super(data) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/options/views/DevelopmentHelper.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 26 | 27 | -------------------------------------------------------------------------------- /src/options/views/Home.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | 21 | 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "noImplicitAny": false, 9 | "moduleResolution": "node", 10 | "experimentalDecorators": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "suppressImplicitAnyIndexErrors": true, 14 | "sourceMap": true, 15 | "allowJs": true, 16 | "baseUrl": ".", 17 | "types": ["webpack-env", "node", "chrome"], 18 | "paths": { 19 | "@/*": ["src/*"], 20 | "@styles/*": ["src/assets/styles/*"] 21 | }, 22 | "lib": ["esnext", "dom", "dom.iterable", "scripthost"] 23 | }, 24 | "include": ["src"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /src/background/background.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import _ from 'lodash' 3 | import { browser } from 'webextension-polyfill-ts' 4 | 5 | import { 6 | ResourceListListener, 7 | ChannelListener 8 | } from '@/Listener' 9 | 10 | // 加载 background scripts 11 | import './scripts/Account' 12 | 13 | // 加载监听器 14 | 15 | new ResourceListListener() 16 | new ChannelListener() 17 | 18 | browser.runtime.onMessage.addListener((request) => { 19 | const params = 20 | request.type === 'GET' ? { params: request.params } : { data: request.data } 21 | 22 | return axios({ 23 | method: request.type, 24 | baseURL: request.baseUrl, 25 | url: request.url, 26 | headers: request.headers || {}, 27 | ...params 28 | }).then((response) => { 29 | return response.data 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /src/Watcher/WatcherBase.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 观察者基类 3 | * 4 | * 功能: 5 | * 根据 URL 提供功能,监听页面 DOM 元素 6 | */ 7 | 8 | import _ from 'lodash' 9 | 10 | export abstract class WatcherBase { 11 | protected urls: RegExp[] = [] 12 | 13 | constructor() { 14 | // 初始化(子类设置 urls 等操作) 15 | this.init() 16 | 17 | let urlIndex = -1 18 | 19 | _.forEach(this.urls, (urlReg, index) => { 20 | if (urlReg.test(window.location.href)) { 21 | urlIndex = index 22 | return false 23 | } 24 | }) 25 | 26 | // 如果有匹配的 url 则执行处理函数 27 | if (urlIndex !== -1) 28 | this.handle({ 29 | index: urlIndex 30 | }) 31 | } 32 | 33 | protected abstract init(): void 34 | protected abstract handle(options: HandleOptions): void 35 | } 36 | 37 | export interface HandleOptions extends Object { 38 | index: number 39 | } 40 | -------------------------------------------------------------------------------- /src/popup/router.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | Vue.use(Router) 5 | 6 | const router = new Router({ 7 | routes: [ 8 | { 9 | path: '/', 10 | redirect: '/SubscribeChannel' 11 | }, 12 | { 13 | path: '/SubscribeChannel', 14 | name: 'SubscribeChannel', 15 | component: () => 16 | import( 17 | /* webpackChunkName: "PopupSubscribeChannel" */ 18 | '@/popup/views/SubscribeChannel.vue' 19 | ) 20 | }, 21 | { 22 | path: '/MultipleAccounts', 23 | name: 'MultipleAccounts', 24 | component: () => 25 | import( 26 | /* webpackChunkName: "PopupMultipleAccounts" */ 27 | '@/popup/views/MultipleAccounts.vue' 28 | ) 29 | } 30 | ], 31 | linkExactActiveClass: 'active' 32 | }) 33 | 34 | export default router 35 | -------------------------------------------------------------------------------- /src/api/BilibiliPassport.ts: -------------------------------------------------------------------------------- 1 | import Request from '@/scripts/base/request' 2 | 3 | export default class BilibiliPassport extends Request { 4 | constructor() { 5 | super() 6 | 7 | this.baseUrl = 'https://passport.bilibili.com' 8 | } 9 | 10 | /** 11 | * 获取二维码登录链接 12 | */ 13 | public getLoginUrl() { 14 | return this.request({ 15 | url: '/qrcode/getLoginUrl', 16 | method: 'GET' 17 | }) 18 | } 19 | 20 | /** 21 | * 获取登录信息 22 | */ 23 | public getLoginInfo(oauthKey: string) { 24 | return this.request({ 25 | url: '/qrcode/getLoginInfo', 26 | method: 'POST', 27 | data: { 28 | oauthKey 29 | } 30 | }) 31 | } 32 | 33 | public logout(biliCSRF: string, accountId: string) { 34 | return this.request( 35 | { 36 | url: '/login/exit/v2', 37 | method: 'POST', 38 | data: { 39 | biliCSRF 40 | } 41 | }, 42 | accountId 43 | ) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/options/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 39 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "background": { 3 | "persistent": true, 4 | "page": "background.html" 5 | }, 6 | "manifest_version": 2, 7 | "name": "Btools", 8 | "version": "2.0.0", 9 | "description": "__MSG_pluginDesc__", 10 | "icons": { 11 | "16": "icon/16.png", 12 | "48": "icon/48.png", 13 | "128": "icon/128.png" 14 | }, 15 | "browser_action": { 16 | "default_icon": "icon/128.png", 17 | "default_title": "Btools", 18 | "default_popup": "popup.html" 19 | }, 20 | "options_ui": { 21 | "page": "options.html" 22 | }, 23 | "content_scripts": [ 24 | { 25 | "matches": ["*://*.bilibili.com/*", "*://movie.douban.com/*"], 26 | "js": ["btools.js"], 27 | "css": ["btools.css"], 28 | "run_at": "document_start" 29 | } 30 | ], 31 | "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self';", 32 | "permissions": ["", "storage", "webRequest", "cookies"], 33 | 34 | "homepage_url": "https://btools.cc", 35 | "default_locale": "zh_CN" 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 imba97 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/assets/styles/global.scss: -------------------------------------------------------------------------------- 1 | @import "./variables.scss"; 2 | 3 | // 按钮 4 | @mixin button($color, $fontColor: #fff, $borderColor: #fff) { 5 | padding: 3px 5px; 6 | 7 | background-color: $color; 8 | color: $fontColor; 9 | 10 | border: 1px $borderColor solid; 11 | border-radius: 5px; 12 | 13 | cursor: pointer; 14 | } 15 | 16 | .btn-success { 17 | @include button($success-color) 18 | } 19 | 20 | .btn-error { 21 | @include button($error-color) 22 | } 23 | 24 | .btn-info { 25 | @include button(#f0f0f0, #999, #ccc) 26 | } 27 | 28 | // margin padding 29 | 30 | @for $i from 1 through 10 { 31 | .mt-#{$i * 5} { 32 | margin-top: $i * 5px; 33 | } 34 | 35 | .mb-#{$i * 5} { 36 | margin-bottom: $i * 5px; 37 | } 38 | 39 | .ml-#{$i * 5} { 40 | margin-left: $i * 5px; 41 | } 42 | 43 | .mr-#{$i * 5} { 44 | margin-right: $i * 5px; 45 | } 46 | 47 | .pt-#{$i * 5} { 48 | padding-top: $i * 5px; 49 | } 50 | 51 | .pb-#{$i * 5} { 52 | padding-bottom: $i * 5px; 53 | } 54 | 55 | .pl-#{$i * 5} { 56 | padding-left: $i * 5px; 57 | } 58 | 59 | .pr-#{$i * 5} { 60 | padding-right: $i * 5px; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/scripts/base/storage/template/TComment.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 评论相关功能存储模板 3 | * - 历史表情 4 | * - 自定义颜文字 5 | */ 6 | 7 | import { TemplateBase } from '@/scripts/base/storage/template/TemplateBase' 8 | 9 | /** 10 | * 历史表情接口 11 | */ 12 | export interface IStickerHistory { 13 | /** 14 | * 是否是颜文字 15 | */ 16 | isKaomoji: boolean 17 | 18 | /** 19 | * 表情文字 20 | */ 21 | text: string 22 | 23 | /** 24 | * 表情图片链接 25 | */ 26 | src: string 27 | } 28 | 29 | /** 30 | * 自定义颜文字接口 31 | */ 32 | export interface ICustomizeKaomoji { 33 | /** 34 | * 是否是大型颜文字 35 | */ 36 | isBig: boolean 37 | 38 | name?: string 39 | 40 | /** 41 | * 颜文字 42 | */ 43 | text: string 44 | } 45 | 46 | export type IEmoteItem = { 47 | text: string 48 | url: string 49 | } 50 | 51 | export type IEmotePackage = { 52 | /** 53 | * .current-type 下的 img url 54 | */ 55 | url: string 56 | 57 | /** 58 | * 表情列表 59 | */ 60 | emote: IEmoteItem[] 61 | } 62 | 63 | export interface IComment extends Object { 64 | stickerHistory?: IStickerHistory[] 65 | customizeKaomoji?: ICustomizeKaomoji[] 66 | } 67 | 68 | export class TComment extends TemplateBase { 69 | constructor(data: IComment) { 70 | super(data) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/options/router.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | Vue.use(Router) 5 | 6 | const router = new Router({ 7 | routes: [ 8 | { 9 | path: '/', 10 | name: 'Home', 11 | component: () => 12 | import( 13 | /* webpackChunkName: "OptionsHome" */ 14 | '@/options/views/Home.vue' 15 | ) 16 | }, 17 | { 18 | path: '/DevelopmentHelper', 19 | name: 'DevelopmentHelper', 20 | component: () => 21 | import( 22 | /* webpackChunkName: "OptionsDevelopmentHelper" */ 23 | '@/options/views/DevelopmentHelper.vue' 24 | ) 25 | }, 26 | { 27 | path: '/SubscribeChannel', 28 | name: 'SubscribeChannel', 29 | component: () => 30 | import( 31 | /* webpackChunkName: "OptionsSubscribeChannel" */ 32 | '@/options/views/SubscribeChannel.vue' 33 | ) 34 | }, 35 | { 36 | path: '/LiveRoomHelper', 37 | name: 'LiveRoomHelper', 38 | component: () => 39 | import( 40 | /* webpackChunkName: "OptionsLiveRoomHelper" */ 41 | '@/options/views/LiveRoomHelper.vue' 42 | ) 43 | } 44 | ], 45 | linkExactActiveClass: 'active' 46 | }) 47 | 48 | export default router 49 | -------------------------------------------------------------------------------- /src/scripts/base/storage/template/TRetrieveInvalidVideo.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 找回失效视频存储模板 3 | */ 4 | 5 | import { TemplateBase } from '@/scripts/base/storage/template/TemplateBase' 6 | 7 | export interface IVideoInfo extends Object { 8 | /** 9 | * 作者 UID 10 | */ 11 | mid: string 12 | 13 | /** 14 | * 视频标题 15 | */ 16 | title: string 17 | 18 | /** 19 | * 视频图片链接 20 | */ 21 | pic: string 22 | } 23 | 24 | export interface IVideoDetail extends Object { 25 | /** 26 | * 视频简介详情 27 | */ 28 | desc: string 29 | 30 | /** 31 | * 作者 32 | */ 33 | author: string 34 | 35 | /** 36 | * 分P 标题 37 | */ 38 | partNames: string[] 39 | 40 | /** 41 | * 发布时间 42 | */ 43 | created_at: string 44 | } 45 | 46 | interface INotInvalidVideoInfo extends Object { 47 | /** 48 | * 作者 ID 49 | */ 50 | mid: string 51 | 52 | /** 53 | * AV号 54 | */ 55 | aid: string 56 | } 57 | 58 | export interface IRetrieveInvalidVideo extends Object { 59 | videoInfo: { [key: string]: IVideoInfo } 60 | videoDetail: { [key: string]: IVideoDetail } 61 | notInvalidVideoInfo: { [key: string]: INotInvalidVideoInfo } 62 | } 63 | 64 | export class TRetrieveInvalidVideo extends TemplateBase { 65 | constructor(data: IRetrieveInvalidVideo) { 66 | super(data) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/btools.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { browser } from 'webextension-polyfill-ts' 3 | 4 | import '@styles/global' 5 | import '@styles/index' 6 | 7 | import Util from '@/scripts/base/Util' 8 | import { RequestApiType } from '@/scripts/base/enums/ContentJsType' 9 | 10 | /** 11 | * 加载 Btools Linstener 模块 12 | */ 13 | import { 14 | RetrieveInvalidVideo, 15 | SubscribeChannel 16 | } from '@/scripts/module' 17 | 18 | /** 19 | * 加载 Btools Watcher 模块 20 | */ 21 | import { 22 | GetPicWatcher, 23 | LinkConverter, 24 | LiveRoomHelper, 25 | HaiLinHappy 26 | } from '@/Watcher' 27 | 28 | Util.Instance().console('已开启', 'success') 29 | 30 | Vue.config.productionTip = false 31 | 32 | // Vue.use(VueRouter) 33 | // Vue.use(Vuex) 34 | 35 | // Linstener 模块 36 | browser.runtime.onMessage.addListener(function (request, sender) { 37 | // 根据类型调用不同功能模块 38 | switch (request.type) { 39 | case RequestApiType.ResourceList: 40 | // 找回失效视频 41 | new RetrieveInvalidVideo() 42 | break 43 | 44 | case RequestApiType.Channel: 45 | // 订阅频道 46 | new SubscribeChannel() 47 | break 48 | } 49 | 50 | // callback 目前不需要 51 | // return new Promise(() => {}) 52 | }) 53 | 54 | // Watcher 模块 55 | new GetPicWatcher() 56 | new LinkConverter() 57 | new LiveRoomHelper() 58 | new HaiLinHappy() 59 | -------------------------------------------------------------------------------- /src/Listener/ListenerBase.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 监听器基类 3 | * 4 | * 功能: 5 | * 与前端交互,监听API请求,当结果返回后再进行页面操作 6 | */ 7 | 8 | import { RequestApiType } from '@/scripts/base/enums/ContentJsType' 9 | import { browser } from 'webextension-polyfill-ts' 10 | 11 | interface IContentJs extends Object { 12 | type: RequestApiType 13 | tabId?: number 14 | } 15 | 16 | export default abstract class ListenerBase { 17 | protected tabId?: number 18 | protected urls: string[] = [] 19 | 20 | constructor() { 21 | this.init() 22 | } 23 | 24 | protected init() { 25 | browser.webRequest.onCompleted.addListener( 26 | (details) => { 27 | this.tabId = details.tabId 28 | // 执行处理函数 29 | this.handle() 30 | }, 31 | { 32 | urls: this.urls 33 | } 34 | ) 35 | } 36 | 37 | protected abstract handle(): void 38 | 39 | /** 40 | * 向 Content Js 发送数据 41 | * @param options 42 | * @param callback 43 | * @returns 44 | */ 45 | protected sendToContentJs( 46 | options: IContentJs, 47 | callback: (response: any) => void 48 | ) { 49 | // 如果传了 tabId 则直接用这个 50 | if (options.tabId && options.tabId !== -1) { 51 | browser.tabs.sendMessage(options.tabId, options).then(callback) 52 | return 53 | } 54 | // browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => { 55 | // if (tabs.length === 0) throw new Error('tabs length is 0') 56 | // browser.tabs.sendMessage(tabs[0].id!, options).then(callback) 57 | // }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Watcher/LiveRoomHelper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 直播间助手 3 | */ 4 | 5 | import Util from '@/scripts/base/Util' 6 | import { WatcherBase, HandleOptions } from '@/Watcher/WatcherBase' 7 | import $ from 'jquery' 8 | import _ from 'lodash' 9 | import { 10 | ILiveRoomHelper, 11 | TLiveRoomHelper 12 | } from '@/scripts/base/storage/template' 13 | import ExtStorage from '@/scripts/base/storage/ExtStorage' 14 | 15 | export class LiveRoomHelper extends WatcherBase { 16 | private _localData?: ILiveRoomHelper 17 | 18 | protected async init() { 19 | this.urls[LiveRoomHelperEnum.Live] = /live\.bilibili\.com/ 20 | } 21 | 22 | protected handle(options: HandleOptions): void { 23 | Util.Instance().console('直播间助手', 'success') 24 | 25 | switch (options.index) { 26 | case LiveRoomHelperEnum.Live: 27 | this.live() 28 | break 29 | } 30 | } 31 | 32 | private async live() { 33 | this._localData = await ExtStorage.Instance().getStorage< 34 | TLiveRoomHelper, 35 | ILiveRoomHelper 36 | >( 37 | new TLiveRoomHelper({ 38 | setting: { 39 | miniPlayer: null 40 | } 41 | }) 42 | ) 43 | 44 | // 关闭直播间迷你播放器 45 | if ( 46 | this._localData!.setting?.miniPlayer?.current && 47 | !this._localData!.setting.miniPlayer.current.value 48 | ) { 49 | const player = await Util.Instance().getElement( 50 | '.player-section .live-player-ctnr' 51 | ) 52 | $(player).addClass('miniPlayerHide') 53 | } 54 | } 55 | } 56 | 57 | enum LiveRoomHelperEnum { 58 | Live 59 | } 60 | -------------------------------------------------------------------------------- /src/OptionsInit/OLiveRoomHelper.ts: -------------------------------------------------------------------------------- 1 | import ExtStorage from '@/scripts/base/storage/ExtStorage' 2 | import { OptionsType } from '@/scripts/base/enums/OptionsType' 3 | import { 4 | ILiveRoomHelper, 5 | TLiveRoomHelper 6 | } from '@/scripts/base/storage/template' 7 | import _ from 'lodash' 8 | 9 | export class OLiveRoomHelper { 10 | private _localData: ILiveRoomHelper = {} 11 | 12 | async init() { 13 | await this.get() 14 | 15 | _.forEach(this._localData.setting, (item, key) => { 16 | switch (key) { 17 | case 'miniPlayer': 18 | if (!this._localData.setting?.miniPlayer) this.setMiniPlayer() 19 | break 20 | } 21 | }) 22 | 23 | return await this.save() 24 | } 25 | 26 | setMiniPlayer() { 27 | const values = [ 28 | { 29 | name: '开', 30 | type: OptionsType.Text, 31 | value: true 32 | }, 33 | { 34 | name: '关', 35 | type: OptionsType.Text, 36 | value: false 37 | } 38 | ] 39 | 40 | this._localData.setting!.miniPlayer = { 41 | name: '显示迷你播放器', 42 | values, 43 | current: values[0] 44 | } 45 | } 46 | 47 | async get() { 48 | this._localData = await ExtStorage.Instance().getStorage< 49 | TLiveRoomHelper, 50 | ILiveRoomHelper 51 | >( 52 | new TLiveRoomHelper({ 53 | setting: { 54 | miniPlayer: null 55 | } 56 | }) 57 | ) 58 | } 59 | 60 | async save() { 61 | return await ExtStorage.Instance().setStorage< 62 | TLiveRoomHelper, 63 | ILiveRoomHelper 64 | >(new TLiveRoomHelper(this._localData)) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/scripts/base/storage/template/TSubscribeChannel.ts: -------------------------------------------------------------------------------- 1 | import { TemplateBase } from '@/scripts/base/storage/template/TemplateBase' 2 | import { IBtoolsOptions } from '@/scripts/base/interface/IOptions' 3 | 4 | export interface IVideoData { 5 | /** 6 | * 视频封面链接 7 | */ 8 | pic: string 9 | 10 | /** 11 | * 视频 BV号 12 | */ 13 | bvid: string 14 | 15 | /** 16 | * 视频标题 17 | */ 18 | title: string 19 | 20 | /** 21 | * 是否已读 22 | */ 23 | readed: boolean 24 | } 25 | 26 | interface IChannel extends Object { 27 | [key: number]: number[] 28 | } 29 | 30 | interface IChannelInfo extends Object { 31 | [key: number]: { 32 | title: string 33 | } 34 | } 35 | 36 | export interface IUserInfo extends Object { 37 | [key: number]: { 38 | name?: string 39 | face?: string 40 | } 41 | } 42 | 43 | export interface IChannelVideoInfo extends Object { 44 | [key: number]: IChannelVideos 45 | } 46 | 47 | interface IChannelVideos extends Object { 48 | [key: number]: IVideoData[] 49 | } 50 | 51 | /** 52 | * 订阅频道 配置项 53 | */ 54 | export interface ISubscribeChannelOptions extends Object { 55 | time: IBtoolsOptions | null 56 | } 57 | 58 | export interface ISubscribeChannel { 59 | /** 60 | * 订阅的频道 61 | */ 62 | channel?: IChannel 63 | 64 | channelInfo?: IChannelInfo 65 | 66 | userInfo?: IUserInfo 67 | 68 | /** 69 | * 频道视频信息 70 | */ 71 | channelVideos?: IChannelVideoInfo 72 | 73 | /** 74 | * 设置 - 查询间隔 75 | */ 76 | setting?: ISubscribeChannelOptions 77 | } 78 | 79 | export class TSubscribeChannel extends TemplateBase { 80 | constructor(data: ISubscribeChannel) { 81 | super(data) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/components/Navbar.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 71 | 72 | 82 | -------------------------------------------------------------------------------- /src/OptionsInit/OSubscribeChannel.ts: -------------------------------------------------------------------------------- 1 | import ExtStorage from '@/scripts/base/storage/ExtStorage' 2 | import { OptionsType } from '@/scripts/base/enums/OptionsType' 3 | import { 4 | ISubscribeChannel, 5 | TSubscribeChannel 6 | } from '@/scripts/base/storage/template' 7 | import _ from 'lodash' 8 | 9 | export class OSubscribeChannel { 10 | private _localData: ISubscribeChannel = {} 11 | 12 | async init() { 13 | await this.get() 14 | 15 | _.forEach(this._localData.setting, (item, key) => { 16 | switch (key) { 17 | case 'time': 18 | if (!this._localData.setting?.time) this.setTime() 19 | break 20 | } 21 | }) 22 | 23 | return await this.save() 24 | } 25 | 26 | setTime() { 27 | const values = [ 28 | { 29 | name: '10分钟', 30 | type: OptionsType.Text, 31 | value: 10 32 | }, 33 | { 34 | name: '30分钟', 35 | type: OptionsType.Text, 36 | value: 30 37 | }, 38 | { 39 | name: '1小时', 40 | type: OptionsType.Text, 41 | value: 60 42 | } 43 | ] 44 | 45 | this._localData.setting!.time = { 46 | name: '检查更新周期', 47 | values, 48 | current: values[0] 49 | } 50 | } 51 | 52 | async get() { 53 | this._localData = await ExtStorage.Instance().getStorage< 54 | TSubscribeChannel, 55 | ISubscribeChannel 56 | >( 57 | new TSubscribeChannel({ 58 | setting: { 59 | time: null 60 | } 61 | }) 62 | ) 63 | } 64 | 65 | async save() { 66 | return await ExtStorage.Instance().setStorage< 67 | TSubscribeChannel, 68 | ISubscribeChannel 69 | >(new TSubscribeChannel(this._localData)) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Watcher/HaiLinHappy.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 直播间助手 3 | */ 4 | 5 | import Util from '@/scripts/base/Util' 6 | import { WatcherBase, HandleOptions } from '@/Watcher/WatcherBase' 7 | import $ from 'jquery' 8 | import _ from 'lodash' 9 | 10 | export class HaiLinHappy extends WatcherBase { 11 | protected async init() { 12 | this.urls[HaiLinHappyEnum.Brackets] = 13 | /movie\.douban.com\/subject\/\d+\/(?!celebrities)/ 14 | this.urls[HaiLinHappyEnum.WriterTopping] = 15 | /movie\.douban.com\/subject\/\d+\/celebrities/ 16 | } 17 | 18 | protected handle(options: HandleOptions): void { 19 | Util.Instance().console('海林老师狂喜器', 'success') 20 | 21 | switch (options.index) { 22 | case HaiLinHappyEnum.Brackets: 23 | this.Brackets() 24 | break 25 | 26 | case HaiLinHappyEnum.WriterTopping: 27 | this.writerTopping() 28 | break 29 | } 30 | } 31 | 32 | private async Brackets() { 33 | await Util.Instance().getElement('#celebrities') 34 | 35 | const celebrities = $('#celebrities h2 .pl') 36 | 37 | if (celebrities.length === 0) return 38 | 39 | celebrities.html(celebrities.html().replace('(', '<').replace(')', '>')) 40 | } 41 | 42 | private async writerTopping() { 43 | await Util.Instance().getElement('#wrapper') 44 | $('.list-wrapper').each((_, element) => { 45 | const title = $(element).find('h2') 46 | if (title.text() === '编剧 Writer') { 47 | element.remove() 48 | $(element).css('zoom', 1.5) 49 | title.css({ 50 | 'font-size': '30px' 51 | }) 52 | $('#celebrities').prepend(element) 53 | } 54 | }) 55 | } 56 | } 57 | 58 | enum HaiLinHappyEnum { 59 | Brackets, 60 | WriterTopping 61 | } 62 | -------------------------------------------------------------------------------- /src/assets/styles/iconfont.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:'iconfont';src:url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAScAAsAAAAAB8AAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABCAAAADsAAABUIIslek9TLzIAAAFEAAAARAAAAGA8GkjWY21hcAAAAYgAAABSAAABfn5d6q1nbHlmAAAB3AAAANkAAAGcUhLND2hlYWQAAAK4AAAALAAAADYfjXtoaGhlYQAAAuQAAAAcAAAAJAfeA4RobXR4AAADAAAAAAwAAAAMDAAAAGxvY2EAAAMMAAAACAAAAAgAbgDObWF4cAAAAxQAAAAeAAAAIAEQAFNuYW1lAAADNAAAAUAAAAJnEKM8sHBvc3QAAAR0AAAAKAAAADlMb6/ieJxjYGRgYOBiMGCwY2BycfMJYeDLSSzJY5BiYGGAAJA8MpsxJzM9kYEDxgPKsYBpDiBmg4gCACY7BUgAeJxjYGFhYJzAwMrAwNTJdIaBgaEfQjO+ZjBi5ACKMrAyM2AFAWmuKQwHnrE+Y2Nu+N/AwMB8hwFIMjCiKGICAGOKDGt4nO2QsQ2AMAwEz4mTAjEGBQXDULF/xRrJ22EMXvqX/2Q3BhpQ5Ut2sAcjdIta8sqW3Dlyx4O/fQxli1TXJSW4dX7tmefXSvxpOee+jE3U1gtvAAB4nGNgZgAC5kZmRgYRBk0GNwYGVSV1NXU1E3MzczMjcTFxMRF2NnY2ZiUQKQLiG4FkTEBqxNWU+BnV1ZTYROQZ2dnkGI3sGYHSZib6jEAFTAzhXp5aOjpanl7rYIzwjKhwI1NTo/CoCzCGEpcUZ3Iyl5QwkORkFJLiAlIQjgOKVjCD6QCKXjDjJSdIsTCSRmGIiSBvofjNhhS/CbOr6zHyMcozihkbAv1lpsfIRqSPmGfYcUqJMHKEcfHxca3nZBSWJtIn/y4vc+MCBUECN/cBTmkhRgDlAU8GAAAAeJxjYGRgYADiB/IMk+P5bb4ycLMwgMA989lbkWkWBmaQSg4GJhAPAP/sCB94nGNgZGBgbvjfwBDDwgACQJKRARUwAwBHCQJsBAAAAAQAAAAEAAAAAAAAAABuAM54nGNgZGBgYGZwB2IQYAJiLiBkYPgP5jMAAA/PAWQAAHichZE9bsJAEIWfwZAElChKpDRpVikoEsn8lEipUKCnoAez5ke211ovSNQ5TY6QE+QI6Whzikh52EMDRbza2W/evpkdyQDusIeH8rvnLtnDJbOSK7jAo3CV+pOwT34WrqGJnnCd+qtwAy94E26yY8YOnn/FrIV3YQ+3+BCu4AafwlXqX8I++Vu4hgf8CNep/wo3MPGuhZtoeeHA6qnTczXbqVVo0sik7niO9WITT+2pPNE2X5lUdYPOURrpVNtjm3y76DkXqciaRA15q+PYqMyatQ5dsHQu67fbkehBaBIMYKExhWOcQ2GGHeMKIQxSREV0Z/mY7gU2iFlp/3VP6LbIqR9yhS4CdM5cI7rSwnk6TY4tX+tRdXQrbsuahDSUWs1JYrLiDzzcramE1AMsi6oMfbS5ohN/UMyQ/AHYk29XeJxjYGKAAC4G7ICZkYmRmZGFgTW1qCi/iL24NDk5tbiYgQEAMjkFSw==) format('woff');font-weight:400;font-style:normal;font-display:swap}.iconfont{font-family:"iconfont"!important;font-size:16px;font-style:normal;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.icon-error:before{content:"\e606"}.icon-success:before{content:"\e605"} -------------------------------------------------------------------------------- /src/background/scripts/Account.ts: -------------------------------------------------------------------------------- 1 | // 获取当前登录的帐号 2 | import _ from 'lodash' 3 | import { browser, Cookies } from 'webextension-polyfill-ts' 4 | import ExtStorage from '@/scripts/base/storage/ExtStorage' 5 | import { 6 | IMultipleAccounts, 7 | TMultipleAccounts 8 | } from '@/scripts/base/storage/template/TMultipleAccounts' 9 | import { BilibiliApi } from '@/api' 10 | ;(async () => { 11 | const localData = await ExtStorage.Instance().getStorage< 12 | TMultipleAccounts, 13 | IMultipleAccounts 14 | >( 15 | new TMultipleAccounts({ 16 | currentAccount: '', 17 | userList: [] 18 | }) 19 | ) 20 | 21 | const cookies = await browser.cookies.getAll({ 22 | domain: '.bilibili.com' 23 | }) 24 | 25 | const accountCookieNames = [ 26 | 'SESSDATA', 27 | 'bili_jct', 28 | 'DedeUserID', 29 | 'DedeUserID__ckMd5' 30 | ] 31 | 32 | const accountCookie: { [key: string]: string | undefined } = {} 33 | 34 | _.forEach(accountCookieNames, (key) => { 35 | const cookie = _.find(cookies, { name: key }) 36 | 37 | accountCookie[key] = cookie?.value 38 | }) 39 | 40 | // 当前账号 41 | localData.currentAccount = accountCookie.DedeUserID 42 | 43 | // 本地存储有当前账号 44 | if (_.find(localData.userList, { uid: accountCookie.DedeUserID })) { 45 | return 46 | } 47 | 48 | if (!accountCookie.DedeUserID) { 49 | return 50 | } 51 | 52 | const userInfo = await BilibiliApi.Instance().userCard( 53 | accountCookie.DedeUserID 54 | ) 55 | 56 | localData.userList?.push({ 57 | uid: accountCookie.DedeUserID, 58 | name: userInfo.data.card.name, 59 | avatar: userInfo.data.card.face, 60 | token: accountCookie.SESSDATA!, 61 | ckMd5: accountCookie.DedeUserID__ckMd5!, 62 | csrf: accountCookie.bili_jct! 63 | }) 64 | 65 | // 保存 66 | ExtStorage.Instance().setStorage( 67 | new TMultipleAccounts(localData) 68 | ) 69 | })() 70 | -------------------------------------------------------------------------------- /src/popup/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 36 | 37 | 87 | -------------------------------------------------------------------------------- /src/api/BilibiliApi.ts: -------------------------------------------------------------------------------- 1 | import Request from '@/scripts/base/request' 2 | 3 | export default class BilibiliApi extends Request { 4 | constructor() { 5 | super() 6 | 7 | this.baseUrl = 'https://api.bilibili.com' 8 | } 9 | 10 | public myInfo() { 11 | return this.request({ 12 | url: '/x/space/myinfo', 13 | method: 'GET' 14 | }) 15 | } 16 | 17 | /** 18 | * 获取用户卡片信息 19 | */ 20 | public userCard(mid: string) { 21 | return this.request({ 22 | url: '/x/web-interface/card', 23 | method: 'GET', 24 | data: { 25 | mid 26 | } 27 | }) 28 | } 29 | 30 | /** 31 | * 获取频道信息 32 | */ 33 | public getChannelInfo(series_id: number) { 34 | return this.request({ 35 | url: '/x/series/series', 36 | method: 'GET', 37 | data: { 38 | series_id 39 | } 40 | }) 41 | } 42 | 43 | /** 44 | * 获取频道视频 45 | */ 46 | public getChannelVideo(mid: number, series_id: number, pn: number) { 47 | return this.request({ 48 | url: '/x/series/archives?ps=100&order=0&ctype=0', 49 | method: 'GET', 50 | data: { 51 | mid, 52 | series_id, 53 | pn 54 | } 55 | }) 56 | } 57 | 58 | /** 59 | * 获取视频信息 60 | */ 61 | public videoInfo(bvid: string) { 62 | return this.request({ 63 | url: '/x/web-interface/view', 64 | method: 'GET', 65 | data: { 66 | bvid 67 | } 68 | }) 69 | } 70 | 71 | /** 72 | * 表情包列表 73 | */ 74 | public emoteList() { 75 | return this.request({ 76 | url: '/x/emote/user/panel/web?business=reply', 77 | method: 'GET' 78 | }) 79 | } 80 | 81 | /** 82 | * 点赞接口 83 | * @param aid 视频ID 84 | * @param like 点赞 | 取消点赞 85 | * @param csrf 86 | */ 87 | public like(aid: number, like: 0 | 1, csrf: string) { 88 | return this.request({ 89 | url: '/x/web-interface/archive/like', 90 | method: 'POST', 91 | data: { 92 | aid, 93 | like, 94 | csrf 95 | } 96 | }) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['vue', '@typescript-eslint'], 3 | parserOptions: { 4 | parser: '@typescript-eslint/parser', 5 | env: { es6: true }, 6 | sourceType: 'module' 7 | }, 8 | root: true, 9 | env: { 10 | browser: true, 11 | node: true, 12 | serviceworker: true 13 | }, 14 | extends: [ 15 | 'plugin:vue/base', 16 | 'plugin:@typescript-eslint/recommended', 17 | 'plugin:vue/essential', 18 | 'standard' 19 | ], 20 | rules: { 21 | // 设置默认eslint规则 22 | 'one-var': 0, 23 | 'arrow-parens': 0, 24 | 'generator-star-spacing': 0, 25 | 'no-debugger': 0, 26 | 'no-console': 0, 27 | semi: 0, 28 | 'comma-dangle': ['error', 'never'], 29 | 'no-extra-semi': 2, 30 | 'space-before-function-paren': 0, 31 | eqeqeq: 0, 32 | 'spaced-comment': 0, 33 | 'no-useless-escape': 0, 34 | 'no-tabs': 0, 35 | 'no-mixed-spaces-and-tabs': 0, 36 | 'new-cap': 0, 37 | camelcase: 0, 38 | 'no-new': 0, 39 | indent: 0, 40 | // 设置typescript-eslint规则 41 | // https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin/docs/rules 42 | '@typescript-eslint/semi': 0, 43 | '@typescript-eslint/indent': 0, 44 | '@typescript-eslint/explicit-function-return-type': 0, 45 | '@typescript-eslint/no-explicit-any': 0, 46 | '@typescript-eslint/member-delimiter-style': 0, 47 | '@typescript-eslint/interface-name-prefix': 0, 48 | '@typescript-eslint/no-empty-interface': 0, 49 | 'no-unused-vars': 0, 50 | '@typescript-eslint/no-unused-vars': 0, 51 | 'import/no-named-default': 0, 52 | '@typescript-eslint/no-inferrable-types': 0, 53 | 'no-unused-expressions': 0, 54 | '@typescript-eslint/no-non-null-assertion': 0, 55 | '@typescript-eslint/no-empty-function': 0, 56 | '@typescript-eslint/camelcase': 0, 57 | 'no-use-before-define': 0, 58 | '@typescript-eslint/no-use-before-define': 0, 59 | 'no-proto': 0, 60 | '@typescript-eslint/ban-types': 0, 61 | 'no-useless-constructor': 0 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/scripts/base/storage/ExtStorage.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 扩展本地存储 3 | */ 4 | 5 | import _ from 'lodash' 6 | 7 | import { browser } from 'webextension-polyfill-ts' 8 | 9 | import { TemplateBase } from '@/scripts/base/storage/template/TemplateBase' 10 | import Singleton from '@/scripts/base/singletonBase/Singleton' 11 | 12 | export default class ExtStorage extends Singleton { 13 | public getStorage( 14 | configs: T 15 | ): Promise { 16 | return this._getStorage(configs) 17 | } 18 | 19 | public setStorage( 20 | configs: T 21 | ): Promise { 22 | return this._setStorage(configs) 23 | } 24 | 25 | public removeStorage( 26 | configs: T 27 | ): Promise { 28 | const data = configs.GetData() 29 | Object.keys(data).map(function (option: string | number) { 30 | // 置空后保存 31 | data[option] = null 32 | }) 33 | 34 | configs.SetData(data) 35 | 36 | return this._setStorage(configs) 37 | } 38 | 39 | public clear() { 40 | browser.storage.local.clear() 41 | } 42 | 43 | private _getStorage( 44 | configs: T 45 | ): Promise { 46 | return new Promise((resolve) => { 47 | // 根据类型自动分配模块 index 48 | const space = new Object() 49 | space[configs.GetName()] = configs.GetData() 50 | 51 | browser.storage.local.get(space).then((items) => { 52 | resolve({ 53 | ...configs.GetData(), 54 | ...items[configs.GetName()] 55 | }) 56 | }) 57 | }) 58 | } 59 | 60 | private _setStorage( 61 | configs: T 62 | ): Promise { 63 | return new Promise((resolve) => { 64 | // 根据类型自动分配模块 index 65 | const space = {} 66 | space[configs.GetName()] = configs.GetData() 67 | 68 | browser.storage.local.set(space).then(() => { 69 | resolve(configs.GetData() as TResult) 70 | }) 71 | }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/scripts/base/storage/extStorage.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 扩展本地存储 3 | */ 4 | 5 | import _ from 'lodash' 6 | 7 | import { browser } from 'webextension-polyfill-ts' 8 | 9 | import { TemplateBase } from '@/scripts/base/storage/template/TemplateBase' 10 | import Singleton from '@/scripts/base/singletonBase/Singleton' 11 | 12 | export default class ExtStorage extends Singleton { 13 | public getStorage( 14 | configs: T 15 | ): Promise { 16 | return this._getStorage(configs) 17 | } 18 | 19 | public setStorage( 20 | configs: T 21 | ): Promise { 22 | return this._setStorage(configs) 23 | } 24 | 25 | public removeStorage( 26 | configs: T 27 | ): Promise { 28 | const data = configs.GetData() 29 | Object.keys(data).map(function (option: string | number) { 30 | // 置空后保存 31 | data[option] = null 32 | }) 33 | 34 | configs.SetData(data) 35 | 36 | return this._setStorage(configs) 37 | } 38 | 39 | public clear() { 40 | browser.storage.local.clear() 41 | } 42 | 43 | private _getStorage( 44 | configs: T 45 | ): Promise { 46 | return new Promise((resolve) => { 47 | // 根据类型自动分配模块 index 48 | const space = new Object() 49 | space[configs.GetName()] = configs.GetData() 50 | 51 | browser.storage.local.get(space).then((items) => { 52 | resolve({ 53 | ...configs.GetData(), 54 | ...items[configs.GetName()] 55 | }) 56 | }) 57 | }) 58 | } 59 | 60 | private _setStorage( 61 | configs: T 62 | ): Promise { 63 | return new Promise((resolve) => { 64 | // 根据类型自动分配模块 index 65 | const space = {} 66 | space[configs.GetName()] = configs.GetData() 67 | 68 | browser.storage.local.set(space).then(() => { 69 | resolve(configs.GetData() as TResult) 70 | }) 71 | }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Watcher/LinkConverter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 链接转换 3 | */ 4 | 5 | import Util from '@/scripts/base/Util' 6 | import { WatcherBase, HandleOptions } from '@/Watcher/WatcherBase' 7 | import $ from 'jquery' 8 | import _ from 'lodash' 9 | 10 | export class LinkConverter extends WatcherBase { 11 | private _url_reg = 12 | /(? { 36 | this.onTitleChange() 37 | }) 38 | } 39 | 40 | /** 41 | * 标题发生变化 42 | */ 43 | private async onTitleChange() { 44 | // 获取简介 45 | const desc_info = await Util.Instance().getElement('.desc-info span') 46 | 47 | // 替换 url 48 | const html = desc_info.innerHTML.replace(this._url_reg, (url) => { 49 | return `${url}` 50 | }) 51 | 52 | desc_info.innerHTML = html 53 | } 54 | 55 | private read() { 56 | window.onload = async () => { 57 | const article_p = await Util.Instance().getElements( 58 | '#read-article-holder p' 59 | ) 60 | 61 | // 循环所有 p 标签 62 | article_p.forEach((p) => { 63 | // 替换 url 64 | const html = p.innerHTML.replace(this._url_reg, (url) => { 65 | return `${url}` 66 | }) 67 | 68 | p.innerHTML = html 69 | }) 70 | } 71 | } 72 | } 73 | 74 | enum LinkConverterEnum { 75 | /** 76 | * 视频简介 77 | */ 78 | Video, 79 | 80 | /** 81 | * 专栏文章 82 | */ 83 | Read 84 | } 85 | -------------------------------------------------------------------------------- /src/assets/styles/watcher/GetPic.scss: -------------------------------------------------------------------------------- 1 | .live-skin-coloration-area { 2 | position: relative; 3 | } 4 | 5 | .btools-get-pic-live-room { 6 | position: absolute; 7 | top: -3px; 8 | right: 190px; 9 | width: 18px; 10 | height: 18px; 11 | 12 | cursor: pointer; 13 | 14 | i { 15 | position: absolute; 16 | top: 0; 17 | left: 0; 18 | width: 100%; 19 | height: 100%; 20 | } 21 | 22 | span { 23 | position: absolute; 24 | top: 3px; 25 | left: 20px; 26 | width: 35px; 27 | height: 18px; 28 | 29 | color: #00a1d6; 30 | } 31 | } 32 | 33 | #viewbox_report { 34 | position: relative; 35 | } 36 | 37 | .btools-get-pic-video { 38 | position: absolute; 39 | top: -100px; 40 | left: -100px; 41 | width: 30px; 42 | height: 30px; 43 | 44 | cursor: pointer; 45 | } 46 | 47 | // 专栏 打开原图 图片容器 48 | .btools-origina-drawing-container { 49 | position: fixed; 50 | top: 0; 51 | left: 0; 52 | width: 100%; 53 | height: 100%; 54 | 55 | z-index: 99999999; 56 | 57 | .btools-content, 58 | .btools-background { 59 | position: absolute; 60 | top: 0; 61 | left: 0; 62 | width: 100%; 63 | height: 100%; 64 | } 65 | 66 | .btools-content { 67 | &::-webkit-scrollbar { 68 | width: 8px; 69 | height: 8px; 70 | } 71 | 72 | // 滚动条 外 73 | &::-webkit-scrollbar-track { 74 | background-image: linear-gradient( 75 | to top, 76 | lightgrey 0%, 77 | lightgrey 1%, 78 | #e0e0e0 26%, 79 | #efefef 48%, 80 | #d9d9d9 75%, 81 | #bcbcbc 100% 82 | ); 83 | -webkit-border-radius: 2em; 84 | -moz-border-radius: 2em; 85 | border-radius: 2em; 86 | } 87 | 88 | // 滚动条 内 89 | &::-webkit-scrollbar-thumb { 90 | background-image: linear-gradient(to right, #868f96 0%, #596164 100%); 91 | -webkit-border-radius: 2em; 92 | -moz-border-radius: 2em; 93 | border-radius: 2em; 94 | } 95 | 96 | overflow: auto; 97 | 98 | img { 99 | position: absolute; 100 | user-select: none; 101 | } 102 | } 103 | 104 | .btools-background { 105 | opacity: 0.75; 106 | background-color: #000; 107 | z-index: -1; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/scripts/base/request/index.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import qs from 'querystring' 3 | import axios, { AxiosRequestConfig, Method } from 'axios' 4 | import { browser } from 'webextension-polyfill-ts' 5 | 6 | import Singleton from '@/scripts/base/singletonBase/Singleton' 7 | import ExtStorage from '@/scripts/base/storage/ExtStorage' 8 | import { 9 | IMultipleAccounts, 10 | TMultipleAccounts 11 | } from '@/scripts/base/storage/template/TMultipleAccounts' 12 | 13 | export default class Request extends Singleton { 14 | /** 15 | * 请求接口 URL 16 | */ 17 | protected baseUrl!: string 18 | 19 | protected async request(options: AxiosRequestConfig): Promise { 20 | // 如果没有 tabs.sendMessage 说明不是 content js 21 | if (_.get(browser, 'tabs.sendMessage', null) !== null) { 22 | return this.backgroundRequest(options) 23 | } 24 | 25 | const isGet = options.method?.toLocaleUpperCase() === 'GET' 26 | 27 | const headers: { [key: string]: any } = isGet 28 | ? {} 29 | : { 30 | 'content-type': 'application/x-www-form-urlencoded' 31 | } 32 | 33 | return new Promise((resolve, reject) => { 34 | browser.runtime 35 | .sendMessage({ 36 | type: options.method, 37 | baseUrl: this.baseUrl, 38 | url: options.url, 39 | ...(isGet 40 | ? { params: options.data } 41 | : { data: qs.stringify(options.data) }), 42 | headers 43 | }) 44 | .then((json) => { 45 | if (!json) reject(new Error('error')) 46 | resolve(json) 47 | }) 48 | }) 49 | } 50 | 51 | /** 52 | * background js 发起请求 53 | * @param options 54 | * @returns 55 | */ 56 | protected async backgroundRequest(options: AxiosRequestConfig) { 57 | const isGet = options.method?.toLocaleUpperCase() === 'GET' 58 | 59 | const headers: { [key: string]: any } = isGet 60 | ? {} 61 | : { 62 | 'content-type': 'application/x-www-form-urlencoded' 63 | } 64 | 65 | return await axios({ 66 | method: options.method, 67 | baseURL: this.baseUrl, 68 | url: options.url, 69 | ...(isGet 70 | ? { params: options.data } 71 | : { data: qs.stringify(options.data) }), 72 | headers 73 | }).then((response) => { 74 | return response.data 75 | }) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/assets/styles/module/HotKeyMenu.scss: -------------------------------------------------------------------------------- 1 | .Btools-hot-key-menu { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | 6 | width: 180px; 7 | height: 100px; 8 | 9 | border-radius: 10px; 10 | 11 | overflow: hidden; 12 | z-index: 9999999; 13 | display: none; 14 | user-select: none; 15 | 16 | .title { 17 | position: absolute; 18 | top: 10px; 19 | left: 0; 20 | 21 | margin: 0; 22 | padding: 0; 23 | 24 | font-size: 18px; 25 | width: 100%; 26 | height: 30px; 27 | 28 | text-align: center; 29 | 30 | color: #fff; 31 | 32 | cursor: default; 33 | 34 | z-index: 100; 35 | } 36 | 37 | /* 选项 ul */ 38 | .options { 39 | position: absolute; 40 | top: 40px; 41 | left: 0; 42 | 43 | margin: 0; 44 | padding: 0; 45 | 46 | width: 100%; 47 | 48 | box-sizing: border-box; 49 | 50 | z-index: 100; 51 | 52 | li { 53 | position: relative; 54 | margin: 5px 0; 55 | width: 100%; 56 | height: 30px; 57 | list-style-type: none; 58 | 59 | cursor: default; 60 | 61 | /* 选项快捷键 */ 62 | span { 63 | position: absolute; 64 | top: 0; 65 | left: 10px; 66 | width: 30px; 67 | height: 30px; 68 | line-height: 30px; 69 | 70 | text-align: center; 71 | font-size: 20px; 72 | font-weight: 700; 73 | 74 | border-radius: 5px; 75 | color: #666; 76 | background-color: #fff; 77 | } 78 | 79 | /* 选项标题 */ 80 | p { 81 | position: absolute; 82 | top: 0; 83 | left: 45px; 84 | 85 | margin: 0; 86 | padding: 0; 87 | 88 | width: 100%; 89 | height: 30px; 90 | line-height: 30px; 91 | 92 | font-size: 20px; 93 | 94 | color: #fff; 95 | } 96 | } 97 | } 98 | 99 | .background { 100 | position: absolute; 101 | top: 0; 102 | left: 0; 103 | width: 100%; 104 | height: 100%; 105 | opacity: 0.75; 106 | background-color: #000; 107 | 108 | z-index: 10; 109 | } 110 | } 111 | 112 | .btools-hkm-option-selected { 113 | span { 114 | color: #fff !important; 115 | background-color: #f66 !important; 116 | } 117 | } 118 | 119 | .btools-user-select-none { 120 | * { 121 | user-select: none; 122 | pointer-events: none; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/assets/styles/module/StickerHistory.scss: -------------------------------------------------------------------------------- 1 | .btools-sticker-history { 2 | position: absolute; 3 | 4 | z-index: 100; 5 | 6 | .more::before { 7 | content: ''; 8 | position: absolute; 9 | top: 0; 10 | left: 0; 11 | width: 100%; 12 | height: 100%; 13 | border-radius: 5px; 14 | background-color: #000; 15 | opacity: 0.75; 16 | z-index: 0; 17 | } 18 | 19 | .more ul li span { 20 | color: #fff; 21 | } 22 | 23 | .btools-sticker-history-container { 24 | position: absolute; 25 | left: 0; 26 | width: 170px; 27 | height: 30px; 28 | overflow: hidden; 29 | 30 | ul > li { 31 | position: relative; 32 | top: 0; 33 | left: 0; 34 | margin: 0 2px; 35 | list-style: none; 36 | height: 30px; 37 | line-height: 30px; 38 | float: left; 39 | cursor: pointer; 40 | border-radius: 3px; 41 | 42 | &:hover { 43 | background-image: linear-gradient(120deg, #e0c3fc 0%, #8ec5fc 100%); 44 | span { 45 | color: #fff; 46 | } 47 | } 48 | 49 | span { 50 | padding: 0 2px; 51 | font-size: 12px; 52 | } 53 | 54 | img { 55 | width: 100%; 56 | height: 100%; 57 | } 58 | } 59 | 60 | ul { 61 | margin-right: -17px; 62 | overflow: auto; 63 | z-index: 999; 64 | 65 | .kaomoji { 66 | width: auto; 67 | } 68 | .img { 69 | width: 30px; 70 | } 71 | } 72 | 73 | ul::after { 74 | clear: both; 75 | } 76 | } 77 | 78 | .btools-sticker-history-show-more { 79 | position: absolute; 80 | left: 180px; 81 | width: 30px; 82 | height: 30px; 83 | background: none; 84 | border: 0; 85 | cursor: pointer; 86 | display: none; 87 | } 88 | } 89 | 90 | .customize-kaomoji { 91 | position: absolute; 92 | top: 9px; 93 | left: 60px; 94 | width: 200px; 95 | height: 26px; 96 | 97 | input { 98 | width: 100%; 99 | height: 100%; 100 | 101 | padding-left: 5px; 102 | 103 | border: 1px #ccc solid; 104 | border-radius: 2px; 105 | } 106 | } 107 | 108 | .btools-customize-kaomoji { 109 | color: $primary-color !important; 110 | 111 | padding: 5px 8px; 112 | line-height: 22px; 113 | 114 | display: inline-block; 115 | border-radius: 4px; 116 | cursor: pointer; 117 | 118 | &:hover { 119 | background-color: #e3e5e7; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/options/views/LiveRoomHelper.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 116 | -------------------------------------------------------------------------------- /src/options/views/SubscribeChannel.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 116 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "btools-vue", 3 | "version": "1.0.0", 4 | "description": "Btools chrome-extension", 5 | "main": "index.js", 6 | "scripts": { 7 | "c": "cross-env BROWSER_ENV=chrome cross-env NODE_ENV=development webpack --watch", 8 | "f": "cross-env BROWSER_ENV=firefox cross-env NODE_ENV=development webpack --watch", 9 | "build:chrome": "cross-env BROWSER_ENV=chrome cross-env NODE_ENV=production webpack && node dev/zip/index.js Chrome", 10 | "build:firefox": "cross-env BROWSER_ENV=firefox cross-env NODE_ENV=production webpack && node dev/zip/index.js Firefox", 11 | "if": "node dev/iconfont/index.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git@github.com:imba97/Btools-vue.git" 16 | }, 17 | "keywords": [ 18 | "Btools" 19 | ], 20 | "author": "imba97", 21 | "license": "MIT", 22 | "devDependencies": { 23 | "@babel/core": "^7.15.0", 24 | "@babel/plugin-transform-async-to-generator": "^7.14.5", 25 | "@babel/plugin-transform-runtime": "^7.15.0", 26 | "@babel/preset-env": "^7.15.0", 27 | "@babel/runtime": "^7.14.8", 28 | "@types/chrome": "0.0.107", 29 | "@types/color": "^3.0.3", 30 | "@types/jquery": "^3.5.6", 31 | "@types/lodash": "^4.14.172", 32 | "@types/node": "^13.13.52", 33 | "@types/qrcode": "^1.4.2", 34 | "@types/webpack": "^4.41.30", 35 | "@types/webpack-env": "^1.16.2", 36 | "@typescript-eslint/eslint-plugin": "^2.34.0", 37 | "@typescript-eslint/experimental-utils": "^2.34.0", 38 | "@typescript-eslint/parser": "^2.34.0", 39 | "@typescript-eslint/typescript-estree": "^2.34.0", 40 | "autoprefixer": "^9.8.6", 41 | "babel-loader": "^8.2.2", 42 | "bufferutil": "^4.0.3", 43 | "clean-webpack-plugin": "^3.0.0", 44 | "copy-webpack-plugin": "^5.1.2", 45 | "cross-env": "^7.0.3", 46 | "css-loader": "^3.6.0", 47 | "eslint": "^6.8.0", 48 | "eslint-config-alloy": "^3.10.0", 49 | "eslint-config-standard": "^14.1.1", 50 | "eslint-plugin-import": "^2.23.4", 51 | "eslint-plugin-node": "^11.1.0", 52 | "eslint-plugin-promise": "^4.3.1", 53 | "eslint-plugin-standard": "^4.1.0", 54 | "eslint-plugin-vue": "^6.2.2", 55 | "fibers": "^5.0.0", 56 | "file-loader": "^6.2.0", 57 | "html-webpack-plugin": "^4.5.2", 58 | "mini-css-extract-plugin": "^0.9.0", 59 | "node-sass": "^7.0.0", 60 | "postcss": "^8.3.6", 61 | "postcss-loader": "^3.0.0", 62 | "postcss-preset-env": "^6.7.0", 63 | "prettier": "^2.3.1", 64 | "sass": "^1.37.5", 65 | "sass-loader": "^8.0.2", 66 | "simple-iconfont-builder": "^1.0.1", 67 | "style-loader": "^1.3.0", 68 | "ts-loader": "^7.0.5", 69 | "ts-node": "^8.10.2", 70 | "typescript": "^3.9.10", 71 | "url-loader": "^4.1.1", 72 | "utf-8-validate": "^5.0.5", 73 | "vue-loader": "^15.9.8", 74 | "webextension-polyfill-ts": "^0.25.0", 75 | "webpack": "^4.46.0", 76 | "webpack-cli": "^3.3.12", 77 | "webpack-dev-server": "^3.11.2", 78 | "webpack-extension-reloader": "^1.1.4", 79 | "write-json-webpack-plugin": "^1.1.0", 80 | "zip-a-folder": "^1.1.0" 81 | }, 82 | "dependencies": { 83 | "@babel/runtime-corejs3": "^7.14.9", 84 | "axios": "^0.21.2", 85 | "color": "^4.2.1", 86 | "core-js": "^3.16.0", 87 | "jquery": "^3.6.0", 88 | "lodash": "^4.17.21", 89 | "moment": "^2.29.3", 90 | "qrcode": "^1.5.0", 91 | "vue": "^2.6.14", 92 | "vue-class-component": "^7.2.6", 93 | "vue-property-decorator": "^8.5.1", 94 | "vue-router": "^3.5.2", 95 | "vue-template-compiler": "^2.6.14", 96 | "vuex": "^3.6.2", 97 | "vuex-module-decorators": "^1.2.0" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Listener/ChannelListener.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 监听器:频道 3 | */ 4 | 5 | import ListenerBase from '@/Listener/ListenerBase' 6 | import { RequestApiType } from '@/scripts/base/enums/ContentJsType' 7 | import ExtStorage from '@/scripts/base/storage/ExtStorage' 8 | import { 9 | ISubscribeChannel, 10 | ISubscribeChannelOptions, 11 | IVideoData, 12 | TSubscribeChannel 13 | } from '@/scripts/base/storage/template' 14 | import { BilibiliApi } from '@/api' 15 | import _ from 'lodash' 16 | 17 | export class ChannelListener extends ListenerBase { 18 | private _localData: ISubscribeChannel = {} 19 | 20 | /** 21 | * 视频信息临时存储 22 | */ 23 | private _videos_temp: IVideoData[] = [] 24 | 25 | init() { 26 | this.urls = ['*://api.bilibili.com/x/series/archives*'] 27 | super.init() 28 | 29 | // 开启计时器 30 | this.startInterval() 31 | } 32 | 33 | async handle() { 34 | this.sendToContentJs( 35 | { 36 | type: RequestApiType.Channel, 37 | tabId: this.tabId 38 | }, 39 | (response) => {} 40 | ) 41 | } 42 | 43 | /** 44 | * 检测订阅频道的视频更新 45 | */ 46 | private async startInterval() { 47 | // 默认配置 48 | const defaultSetting: ISubscribeChannelOptions = { 49 | time: null 50 | } 51 | 52 | // 本地存储 53 | this._localData = await ExtStorage.Instance().getStorage< 54 | TSubscribeChannel, 55 | ISubscribeChannel 56 | >( 57 | new TSubscribeChannel({ 58 | channel: {}, 59 | channelVideos: {}, 60 | setting: defaultSetting 61 | }) 62 | ) 63 | 64 | // 先查询一次(打开浏览器时、刚安装插件时) 65 | await this.query() 66 | 67 | /** 68 | * 查询是否到获取频道视频时间 69 | */ 70 | const queryInterval = 71 | this._localData.setting?.time && 72 | this._localData.setting.time.current?.value && 73 | this._localData.setting.time.current.value >= 10 74 | ? this._localData.setting.time.current.value 75 | : 10 76 | 77 | // 开启计时器 78 | setInterval(() => { 79 | this.query() 80 | // 用户设置的时间(分钟) * 60 秒 81 | }, queryInterval * 60000) 82 | } 83 | 84 | /** 85 | * 查询最新数据 并跟本地存储中的数据对比 86 | * @returns 87 | */ 88 | private async query() { 89 | if (!this._localData.channel) return 90 | 91 | // 请求间隔 index 92 | let requestTimeout = 0 93 | 94 | // 循环订阅的频道 95 | _.forEach(this._localData.channel, (sids, uid) => { 96 | // 用户 ID 97 | const _uid = parseInt(uid) 98 | // 循环所有频道 ID 99 | _.forEach(sids, async (sid) => { 100 | // 延时执行 101 | setTimeout(async () => { 102 | // 清空视频信息临时存储数组 103 | this._videos_temp = [] 104 | // 获取最新频道视频 105 | await this.getChannelVideos(_uid, sid) 106 | 107 | // 与本地存储进行差异对比 取得新视频 108 | const newVideos = _.differenceBy( 109 | this._videos_temp, 110 | this._localData.channelVideos![uid][sid], 111 | 'bvid' 112 | ) 113 | 114 | // 如果有新视频 115 | if (newVideos.length !== 0) { 116 | // 合并数组 117 | this._localData.channelVideos![uid][sid] = _.union( 118 | this._localData.channelVideos![uid][sid], 119 | newVideos 120 | ) 121 | 122 | // 保存到本地存储 123 | this.save() 124 | } 125 | }, 3000 * ++requestTimeout) 126 | }) 127 | }) 128 | } 129 | 130 | /** 131 | * 获取频道视频 132 | * @param uid 用户 ID 133 | * @param sid 频道 ID 134 | * @param page 页数 135 | */ 136 | private async getChannelVideos(uid: number, sid: number, page: number = 1) { 137 | // 发送请求 138 | const result = await BilibiliApi.Instance().getChannelVideo(uid, sid, page) 139 | 140 | // 遍历视频列表 141 | if (result.data.archives.length !== 0) { 142 | result.data.archives.forEach((item: IVideoData) => { 143 | this._videos_temp.push({ 144 | bvid: item.bvid, 145 | title: item.title, 146 | pic: item.pic, 147 | // 所有视频默认为未读 148 | readed: false 149 | }) 150 | }) 151 | // 如果本页全满 说明可能有下一页 152 | if (result.data.archives.length === 100) { 153 | await this.getChannelVideos(uid, sid, ++page) 154 | } 155 | } 156 | } 157 | 158 | private save() { 159 | ExtStorage.Instance().setStorage( 160 | new TSubscribeChannel(this._localData) 161 | ) 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /webpack.config.ts: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack' 2 | 3 | const { resolve } = require('path') 4 | const VueLoaderPlugin = require('vue-loader/lib/plugin') 5 | const CopyWebpackPlugin = require('copy-webpack-plugin') 6 | const { CleanWebpackPlugin } = require('clean-webpack-plugin') 7 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 8 | const HtmlWebpackPlugin = require('html-webpack-plugin') 9 | const WriteJsonWebpackPlugin = require('write-json-webpack-plugin') 10 | const ExtensionReloader = require('webpack-extension-reloader') 11 | 12 | const isProduction = process.env.NODE_ENV === 'production' 13 | 14 | require('@babel/core').transform('code', { 15 | plugins: ['@babel/plugin-transform-runtime'] 16 | }) 17 | 18 | module.exports = (): webpack.Configuration => { 19 | let manifestJSON = require('./src/manifest.json') 20 | 21 | // 版本号 22 | manifestJSON.version = '2.1.2' 23 | 24 | let configs: webpack.Configuration = { 25 | node: false, 26 | mode: isProduction ? 'production' : 'development', // development production 27 | entry: { 28 | btools: './src/btools.ts', 29 | background: './src/background/background.ts', 30 | popup: './src/popup/popup.ts', 31 | options: './src/options/options.ts' 32 | }, 33 | output: { 34 | path: resolve('Build'), 35 | filename: '[name].js' 36 | }, 37 | resolve: { 38 | extensions: ['.js', '.ts', '.vue', '.scss', '.json'], 39 | alias: { 40 | vue$: 'vue/dist/vue.esm.js', 41 | '@': resolve('src'), 42 | '@styles': resolve('src/assets/styles') 43 | } 44 | }, 45 | module: { 46 | rules: [ 47 | { 48 | test: /\.vue$/, 49 | exclude: /mode_modules/, 50 | loader: 'vue-loader' 51 | }, 52 | 53 | { 54 | test: /\.tsx?$/, 55 | exclude: /mode_modules/, 56 | use: [ 57 | 'babel-loader', 58 | { 59 | loader: 'ts-loader', 60 | options: { 61 | appendTsSuffixTo: [/\.vue$/], 62 | appendTsxSuffixTo: [/\.vue$/], 63 | transpileOnly: true, 64 | happyPackMode: false 65 | } 66 | } 67 | ] 68 | }, 69 | 70 | { 71 | test: /\.(sa|sc|c)ss$/, 72 | exclude: /mode_modules/, 73 | use: [ 74 | MiniCssExtractPlugin.loader, 75 | 'css-loader', 76 | 'postcss-loader', 77 | 'sass-loader' 78 | ] 79 | }, 80 | 81 | { 82 | test: /\.(jpg|jpeg|png|gif|svg|webp)$/, 83 | exclude: /mode_modules/, 84 | loader: 'url-loader', 85 | options: { 86 | limit: 8 * 1024 87 | } 88 | }, 89 | 90 | { 91 | exclude: 92 | /\.(mode_modules|vue|js|tsx?|scss|html|jpg|jpeg|png|gif|svg|webp)/, 93 | loader: 'file-loader', 94 | options: { 95 | outputPath: 'media' 96 | } 97 | } 98 | ] 99 | }, 100 | 101 | plugins: [ 102 | new HtmlWebpackPlugin({ 103 | filename: 'popup.html', 104 | template: './src/popup/popup.html', 105 | minify: { 106 | collapseWhitespace: isProduction, 107 | removeComments: isProduction 108 | }, 109 | chunks: ['popup'] 110 | }), 111 | new HtmlWebpackPlugin({ 112 | filename: 'options.html', 113 | template: './src/options/options.html', 114 | minify: { 115 | collapseWhitespace: isProduction, 116 | removeComments: isProduction 117 | }, 118 | chunks: ['options'] 119 | }), 120 | new HtmlWebpackPlugin({ 121 | filename: 'background.html', 122 | template: './src/background/background.html', 123 | minify: { 124 | collapseWhitespace: isProduction, 125 | removeComments: isProduction 126 | }, 127 | chunks: ['background'] 128 | }), 129 | new MiniCssExtractPlugin({ 130 | filename: '[name].css' 131 | }), 132 | new VueLoaderPlugin(), 133 | new CopyWebpackPlugin([ 134 | { 135 | from: resolve('src/assets/icon'), 136 | to: resolve('Build/icon'), 137 | toType: 'dir' 138 | }, 139 | { 140 | from: resolve('src/_locales'), 141 | to: resolve('Build/_locales'), 142 | toType: 'dir' 143 | } 144 | ]), 145 | manifestJSON && 146 | new WriteJsonWebpackPlugin({ 147 | pretty: false, 148 | object: manifestJSON, 149 | path: '/', 150 | filename: 'manifest.json' 151 | }) 152 | ] 153 | } 154 | 155 | if (isProduction) { 156 | configs.plugins!.unshift(new CleanWebpackPlugin()) 157 | } 158 | 159 | if (!isProduction && process.env.BROWSER_ENV === 'chrome') { 160 | configs.plugins!.push( 161 | new ExtensionReloader({ 162 | reloadPage: true, 163 | entries: { 164 | // The entries used for the content/background scripts or extension pages 165 | contentScript: 'btools', 166 | background: 'background', 167 | extensionPage: ['popup', 'options'] 168 | } 169 | }) 170 | ) 171 | } 172 | 173 | return configs 174 | } 175 | -------------------------------------------------------------------------------- /src/assets/styles/module/RetrieveInvalidVideo.scss: -------------------------------------------------------------------------------- 1 | .btools-detail-box { 2 | position: fixed; 3 | top: 50%; 4 | left: 50%; 5 | transform: translate(-50%, -50%); 6 | width: 500px; 7 | height: 350px; 8 | 9 | border-radius: 10px; 10 | overflow: hidden; 11 | 12 | box-shadow: 0 12px 5px -10px rgba(0, 0, 0, 0.1), 0 0 4px 0 rgba(0, 0, 0, 0.1); 13 | 14 | z-index: 999999; 15 | 16 | .btools-close-btn { 17 | position: absolute; 18 | top: 20px; 19 | right: 20px; 20 | color: #fff; 21 | font-size: 30px; 22 | font-weight: 700; 23 | cursor: pointer; 24 | user-select: none; 25 | z-index: 10; 26 | } 27 | 28 | .btools-container { 29 | width: 100%; 30 | height: 100%; 31 | 32 | .btools-video-info { 33 | position: relative; 34 | padding-left: 30px; 35 | width: 100%; 36 | height: 100%; 37 | box-sizing: border-box; 38 | display: none; 39 | 40 | .btools-title { 41 | width: 400px; 42 | height: 62px; 43 | line-height: 62px; 44 | color: #fff; 45 | font-size: 20px; 46 | 47 | overflow: hidden; 48 | text-overflow: ellipsis; 49 | white-space: nowrap; 50 | } 51 | 52 | .btools-part { 53 | color: #fff; 54 | font-size: 14px; 55 | } 56 | 57 | .btools-list { 58 | width: 460px; 59 | height: 200px; 60 | 61 | overflow-y: auto; 62 | 63 | box-sizing: border-box; 64 | 65 | &::-webkit-scrollbar { 66 | width: 8px; 67 | } 68 | 69 | // 滚动条 外 70 | &::-webkit-scrollbar-track { 71 | background-image: linear-gradient( 72 | to top, 73 | lightgrey 0%, 74 | lightgrey 1%, 75 | #e0e0e0 26%, 76 | #efefef 48%, 77 | #d9d9d9 75%, 78 | #bcbcbc 100% 79 | ); 80 | -webkit-border-radius: 2em; 81 | -moz-border-radius: 2em; 82 | border-radius: 2em; 83 | } 84 | 85 | // 滚动条 内 86 | &::-webkit-scrollbar-thumb { 87 | background-image: linear-gradient(to right, #868f96 0%, #596164 100%); 88 | -webkit-border-radius: 2em; 89 | -moz-border-radius: 2em; 90 | border-radius: 2em; 91 | } 92 | 93 | li { 94 | margin: 5px 0; 95 | list-style-type: none; 96 | color: #fff; 97 | font-size: 18px; 98 | } 99 | } 100 | 101 | .btools-link { 102 | margin-top: 10px; 103 | color: #ccc; 104 | font-size: 20px; 105 | 106 | a { 107 | color: $primary-color; 108 | } 109 | } 110 | } 111 | } 112 | 113 | .btools-loading { 114 | position: absolute; 115 | top: 50%; 116 | left: 50%; 117 | transform: translate(-50%, -50%); 118 | 119 | width: 50px; 120 | height: 50px; 121 | 122 | .sk-chase { 123 | width: 100%; 124 | height: 100%; 125 | position: relative; 126 | animation: sk-chase 2.5s infinite linear both; 127 | } 128 | 129 | .sk-chase-dot { 130 | width: 100%; 131 | height: 100%; 132 | position: absolute; 133 | left: 0; 134 | top: 0; 135 | animation: sk-chase-dot 2s infinite ease-in-out both; 136 | } 137 | 138 | .sk-chase-dot:before { 139 | content: ''; 140 | display: block; 141 | width: 25%; 142 | height: 25%; 143 | background-color: #fff; 144 | border-radius: 100%; 145 | animation: sk-chase-dot-before 2s infinite ease-in-out both; 146 | } 147 | 148 | .sk-chase-dot:nth-child(1) { 149 | animation-delay: -1.1s; 150 | } 151 | .sk-chase-dot:nth-child(2) { 152 | animation-delay: -1s; 153 | } 154 | .sk-chase-dot:nth-child(3) { 155 | animation-delay: -0.9s; 156 | } 157 | .sk-chase-dot:nth-child(4) { 158 | animation-delay: -0.8s; 159 | } 160 | .sk-chase-dot:nth-child(5) { 161 | animation-delay: -0.7s; 162 | } 163 | .sk-chase-dot:nth-child(6) { 164 | animation-delay: -0.6s; 165 | } 166 | .sk-chase-dot:nth-child(1):before { 167 | animation-delay: -1.1s; 168 | } 169 | .sk-chase-dot:nth-child(2):before { 170 | animation-delay: -1s; 171 | } 172 | .sk-chase-dot:nth-child(3):before { 173 | animation-delay: -0.9s; 174 | } 175 | .sk-chase-dot:nth-child(4):before { 176 | animation-delay: -0.8s; 177 | } 178 | .sk-chase-dot:nth-child(5):before { 179 | animation-delay: -0.7s; 180 | } 181 | .sk-chase-dot:nth-child(6):before { 182 | animation-delay: -0.6s; 183 | } 184 | 185 | @keyframes sk-chase { 186 | 100% { 187 | transform: rotate(360deg); 188 | } 189 | } 190 | 191 | @keyframes sk-chase-dot { 192 | 80%, 193 | 100% { 194 | transform: rotate(360deg); 195 | } 196 | } 197 | 198 | @keyframes sk-chase-dot-before { 199 | 50% { 200 | transform: scale(0.4); 201 | } 202 | 100%, 203 | 0% { 204 | transform: scale(1); 205 | } 206 | } 207 | } 208 | 209 | .btools-background { 210 | position: absolute; 211 | top: 0; 212 | left: 0; 213 | width: 100%; 214 | height: 100%; 215 | opacity: 0.95; 216 | background-image: linear-gradient(60deg, #29323c 0%, #485563 100%); 217 | z-index: -1; 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/scripts/base/Util.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import Singleton from '@/scripts/base/singletonBase/Singleton' 3 | import ExtStorage from '@/scripts/base/storage/ExtStorage' 4 | import { TMultipleAccounts, IMultipleAccounts } from '@/scripts/base/storage/template/TMultipleAccounts' 5 | 6 | export default class Util extends Singleton { 7 | private bvTool = { 8 | table: [...'fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF'], 9 | s: [11, 10, 3, 8, 4, 6], 10 | xor: 177451812, 11 | add: 8728348608 12 | } 13 | 14 | /** 15 | * AV号 转 BV号 16 | * @param av AV号 17 | * @param isLower 开头的 BV 是否小写,默认为 false 18 | * @returns BV 号 19 | */ 20 | public av2bv(av: number, isLower = false): string { 21 | let num = NaN 22 | if (Object.prototype.toString.call(av) === '[object Number]') { 23 | num = av 24 | } else if (Object.prototype.toString.call(av) === '[object String]') { 25 | num = parseInt(av.toString().replace(/[^0-9]/gu, '')) 26 | } 27 | if (isNaN(num) || num <= 0) { 28 | throw new Error('¿你在想桃子?') 29 | } 30 | 31 | num = (num ^ this.bvTool.xor) + this.bvTool.add 32 | const result = [...`${isLower ? 'bv' : 'BV'}1 4 1 7 `] 33 | let i = 0 34 | while (i < 6) { 35 | result[this.bvTool.s[i]] = 36 | this.bvTool.table[Math.floor(num / 58 ** i) % 58] 37 | i += 1 38 | } 39 | return result.join('') 40 | } 41 | 42 | /** 43 | * BV号 转 AV号 44 | * @param bv AV号 45 | * @returns AV 号 46 | */ 47 | public bv2av(bv: string): number { 48 | let str = '' 49 | if (bv.length === 12) { 50 | str = bv 51 | } else if (bv.length === 10) { 52 | str = `BV${bv}` 53 | } else if (bv.length === 9) { 54 | str = `BV1${bv}` 55 | } else { 56 | throw new Error('¿你在想桃子?') 57 | } 58 | if ( 59 | !str.match( 60 | /[Bb][Vv][fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF]{10}/gu 61 | ) 62 | ) { 63 | throw new Error('¿你在想桃子?') 64 | } 65 | 66 | let result = 0 67 | let i = 0 68 | while (i < 6) { 69 | result += this.bvTool.table.indexOf(str[this.bvTool.s[i]]) * 58 ** i 70 | i += 1 71 | } 72 | return (result - this.bvTool.add) ^ this.bvTool.xor 73 | } 74 | 75 | public getElement(selector: string): Promise { 76 | // 先获取一次 77 | let element: HTMLElement | null = document.querySelector(selector) 78 | 79 | if (element) return Promise.resolve(element) 80 | 81 | // 如果没获取到 开启计时器 循环获取 82 | return new Promise((resolve, reject) => { 83 | let timeout = 120 84 | 85 | const timer = setInterval(() => { 86 | element = document.querySelector(selector) 87 | 88 | // 成功获取 89 | if (element !== null) { 90 | resolve(element) 91 | clearInterval(timer) 92 | } 93 | 94 | // timeout 95 | if (timeout === 0) { 96 | reject(new Error('Empty Element')) 97 | clearInterval(timer) 98 | } 99 | 100 | timeout-- 101 | }, 500) 102 | }) 103 | } 104 | 105 | /** 106 | * 获取页面上的元素,一分钟内如果没获取到则停止获取 107 | * @param selector 选择器 108 | */ 109 | public getElements(selector: string): Promise> { 110 | let elements: NodeListOf = document.querySelectorAll(selector) 111 | 112 | if (elements.length > 0) return Promise.resolve(elements) 113 | 114 | return new Promise((resolve, reject) => { 115 | let timeout = 120 116 | const timer = setInterval(() => { 117 | elements = document.querySelectorAll(selector) 118 | 119 | // 成功获取 120 | if (elements.length !== 0) { 121 | resolve(elements) 122 | clearInterval(timer) 123 | } 124 | 125 | // timeout 126 | if (timeout === 0) { 127 | reject(new Error('Empty Element')) 128 | clearInterval(timer) 129 | } 130 | 131 | timeout-- 132 | }, 500) 133 | }) 134 | } 135 | 136 | public random(length = 8, type?: string): string { 137 | let chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' 138 | 139 | if (typeof type !== 'undefined') { 140 | switch (type) { 141 | case 'string': 142 | chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' 143 | break 144 | 145 | case 'number': 146 | chars = '0123456789' 147 | break 148 | 149 | default: 150 | break 151 | } 152 | } 153 | 154 | let result = '' 155 | 156 | for (let i = length; i > 0; --i) { 157 | result += chars[Math.floor(Math.random() * chars.length)] 158 | } 159 | 160 | return result 161 | } 162 | 163 | public inNodeList( 164 | element: HTMLElement, 165 | nodeList: NodeListOf 166 | ): number { 167 | let result = -1 168 | 169 | nodeList.forEach((nodeItem, index) => { 170 | if (element === nodeItem) { 171 | result = index 172 | return false 173 | } 174 | }) 175 | 176 | return result 177 | } 178 | 179 | public position(element: HTMLElement, x: number, y: number) { 180 | element.style.top = y + 'px' 181 | element.style.left = x + 'px' 182 | } 183 | 184 | public console( 185 | message: any, 186 | type?: 'success' | 'waring' | 'error', 187 | prefix = 'Btools' 188 | ) { 189 | let css: string 190 | 191 | switch (type) { 192 | case 'success': 193 | prefix += ' Success:' 194 | css = 195 | 'background-image: linear-gradient(to top, #0ba360 0%, #3cba92 100%);' 196 | break 197 | case 'waring': 198 | prefix += ' Waring:' 199 | css = 200 | 'background-image: linear-gradient(to top, #e6b980 0%, #eacda3 100%);' 201 | break 202 | 203 | case 'error': 204 | prefix += ' Error:' 205 | css = 206 | 'background-image: linear-gradient(to top, #ff0844 0%, #ffb199 100%);' 207 | break 208 | 209 | default: 210 | prefix += ' Info:' 211 | css = 212 | 'background: linear-gradient(to bottom, rgba(255,255,255,0.15) 0%, rgba(0,0,0,0.15) 100%), radial-gradient(at top center, rgba(255,255,255,0.40) 0%, rgba(0,0,0,0.40) 120%) #989898; background-blend-mode: multiply,multiply;' 213 | } 214 | 215 | css += 'color: #FFF; padding: 2px 3px; border-radius: 3px;' 216 | 217 | console.log('%c' + prefix, css, message) 218 | } 219 | 220 | /** 221 | * 获取当前用户 cookie 222 | * @param key cookie key 223 | * @example 224 | * 225 | * ```javascript 226 | * const csrf = await Util.Instance().getUserCookie('csrf') 227 | * ``` 228 | */ 229 | public async getUserCookie(key: string): Promise { 230 | const userLocalData = await ExtStorage.Instance().getStorage< 231 | TMultipleAccounts, 232 | IMultipleAccounts 233 | >(new TMultipleAccounts({ 234 | currentAccount: '', 235 | userList: [] 236 | })) 237 | 238 | const user = _.find(userLocalData.userList, { 239 | uid: userLocalData.currentAccount 240 | }) 241 | 242 | if (!user) return Promise.resolve(null) 243 | 244 | return Promise.resolve(_.get(user, key, null)) 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /src/scripts/module/SubscribeChannel.ts: -------------------------------------------------------------------------------- 1 | import ModuleBase from '@/scripts/module/ModuleBase' 2 | import Util from '@/scripts/base/Util' 3 | import ExtStorage from '@/scripts/base/storage/ExtStorage' 4 | import { 5 | TSubscribeChannel, 6 | ISubscribeChannel, 7 | IVideoData 8 | } from '@/scripts/base/storage/template' 9 | import _ from 'lodash' 10 | import IconUtil from '@/scripts/base/IconUtil' 11 | import HKM from '@/scripts/base/HotKeyMenu' 12 | import { BilibiliApi } from '@/api' 13 | 14 | export class SubscribeChannel extends ModuleBase { 15 | private _localData: ISubscribeChannel = {} 16 | 17 | /** 18 | * 视频临时存储 19 | */ 20 | private _videos_temp: IVideoData[] = [] 21 | 22 | /** 23 | * 订阅按钮(快捷键菜单按钮) 24 | */ 25 | private _subscribeButton = document.createElement('a') 26 | 27 | /** 28 | * 是否已订阅 29 | */ 30 | private _isSubscribed = false 31 | 32 | /** 33 | * 频道信息 34 | */ 35 | private _channel_info: { uid?: number; sid?: number } = {} 36 | 37 | /** 38 | * 快捷键菜单 实例 39 | */ 40 | private _hkm?: HKM 41 | 42 | /** 43 | * 快捷键菜单类型 订阅 和 取消订阅 44 | */ 45 | private _hkm_type: { [key: string]: HotKeyMenuOption } = { 46 | subscribe: { 47 | key: 'S', 48 | title: '订阅频道', 49 | action: () => { 50 | this.doSubscribe(this._channel_info.uid!, this._channel_info.sid!) 51 | } 52 | }, 53 | unsubscribe: { 54 | key: 'S', 55 | title: '取消订阅', 56 | action: () => { 57 | this.doUnSubscribe(this._channel_info.uid!, this._channel_info.sid!) 58 | } 59 | } 60 | } 61 | 62 | /** 63 | * 订阅按钮颜色 64 | */ 65 | private _logo_color = { 66 | /** 67 | * 未订阅 68 | */ 69 | doNotSubscribe: '#00a1d6', 70 | 71 | /** 72 | * 订阅中(请求接口等待) 73 | */ 74 | subscribing: '#ccc', 75 | 76 | /** 77 | * 已订阅 78 | */ 79 | subscribed: '#13a813' 80 | } 81 | 82 | protected async handle() { 83 | // 防止重复加载 84 | if (document.querySelector('.btools-subscribe-button') !== null) return 85 | 86 | Util.Instance().console('订阅频道', 'success') 87 | 88 | // 读取本地数据 89 | this._localData = await ExtStorage.Instance().getStorage< 90 | TSubscribeChannel, 91 | ISubscribeChannel 92 | >( 93 | new TSubscribeChannel({ 94 | channel: {}, 95 | channelInfo: {}, 96 | channelVideos: {}, 97 | userInfo: {} 98 | }) 99 | ) 100 | 101 | const channel_action_row = await Util.Instance().getElement( 102 | '.channel-action-row' 103 | ) 104 | 105 | const channel_info_reg = 106 | /space\.bilibili\.com\/(\d+)\/channel\/seriesdetail\?sid=(\d+)/.exec( 107 | window.location.href 108 | ) 109 | 110 | if (channel_info_reg === null) return 111 | 112 | // 用户 ID 113 | this._channel_info.uid = parseInt(channel_info_reg[1]) 114 | // 频道 ID 115 | this._channel_info.sid = parseInt(channel_info_reg[2]) 116 | 117 | // 查询是否已订阅 118 | this._isSubscribed = this.isSubscribed( 119 | this._channel_info.uid, 120 | this._channel_info.sid 121 | ) 122 | 123 | // 订阅按钮 124 | this._subscribeButton.classList.add('btools-subscribe-button') 125 | this._subscribeButton.innerHTML = this._isSubscribed 126 | ? IconUtil.Instance().LOGO(this._logo_color.subscribed) 127 | : IconUtil.Instance().LOGO(this._logo_color.doNotSubscribe) 128 | 129 | // 创建快捷键菜单 130 | this._hkm = new HKM(this._subscribeButton).add([ 131 | this._isSubscribed ? this._hkm_type.unsubscribe : this._hkm_type.subscribe 132 | ]) 133 | 134 | channel_action_row.appendChild(this._subscribeButton) 135 | } 136 | 137 | /** 138 | * 订阅处理事件 139 | * @param uid 用户ID 140 | * @param sid 频道ID 141 | */ 142 | private async doSubscribe(uid: number, sid: number) { 143 | if (!this._localData.channel!.hasOwnProperty(uid)) { 144 | this._localData.channel![uid] = [] 145 | this._localData.userInfo![uid] = {} 146 | } 147 | 148 | if (this._localData.channel![uid].indexOf(sid) !== -1) return 149 | 150 | // 切换按钮颜色 151 | this._subscribeButton.innerHTML = IconUtil.Instance().LOGO( 152 | this._logo_color.subscribing 153 | ) 154 | 155 | // 频道数据 获取频道标题 和 作者名称 156 | const channelData = await BilibiliApi.Instance().getChannelInfo(sid) 157 | 158 | await this.getChannelVideos(uid, sid) 159 | 160 | const userInfo = await BilibiliApi.Instance().userCard(uid.toString()) 161 | 162 | // 频道信息 163 | this._localData.channelInfo![sid] = { 164 | title: _.get(channelData, 'data.meta.name', '获取失败') 165 | } 166 | 167 | // 用户信息 168 | this._localData.userInfo![uid] = { 169 | name: userInfo.data.card.name, 170 | face: userInfo.data.card.face 171 | } 172 | 173 | // 添加到本地存储 174 | this._localData.channel![uid].push(sid) 175 | 176 | // 添加频道视频 177 | if (!this._localData.channelVideos?.hasOwnProperty(uid)) 178 | this._localData.channelVideos![uid] = {} 179 | 180 | this._localData.channelVideos![uid][sid] = this._videos_temp 181 | 182 | this.save() 183 | 184 | // 更改快捷键菜单 185 | this._hkm 186 | ?.removeWithKey(this._hkm_type.subscribe.key) 187 | .add([this._hkm_type.unsubscribe]) 188 | 189 | // logo 颜色 190 | this._subscribeButton.innerHTML = IconUtil.Instance().LOGO( 191 | this._logo_color.subscribed 192 | ) 193 | } 194 | 195 | /** 196 | * 取消订阅事件 197 | * @param uid 用户ID 198 | * @param sid 频道ID 199 | */ 200 | private doUnSubscribe(uid: number, sid: number) { 201 | const channelIndex = this._localData.channel![uid].indexOf(sid) 202 | 203 | if (channelIndex === -1) { 204 | return 205 | } 206 | 207 | // 删除频道 208 | this._localData.channel![uid].splice(channelIndex, 1) 209 | 210 | // 删除频道视频 211 | delete this._localData.channelVideos![uid][sid] 212 | 213 | this.save() 214 | 215 | // 更改快捷键菜单 216 | this._hkm 217 | ?.removeWithKey(this._hkm_type.unsubscribe.key) 218 | .add([this._hkm_type.subscribe]) 219 | 220 | // logo 颜色 221 | this._subscribeButton.innerHTML = IconUtil.Instance().LOGO( 222 | this._logo_color.doNotSubscribe 223 | ) 224 | } 225 | 226 | /** 227 | * 获取频道所有视频 视频存入 _videos_temp 228 | * @param uid 用户ID 229 | * @param sid 频道 ID 230 | * @param page 页数 231 | * @returns 频道数据 232 | */ 233 | private async getChannelVideos(uid: number, sid: number, page: number = 1) { 234 | const result = await BilibiliApi.Instance().getChannelVideo(uid, sid, page) 235 | 236 | // 遍历视频列表 237 | if (result.data.archives.length !== 0) { 238 | result.data.archives.forEach((item: IVideoData) => { 239 | this._videos_temp.push({ 240 | bvid: item.bvid, 241 | title: item.title, 242 | pic: item.pic, 243 | readed: true 244 | }) 245 | }) 246 | // 如果本页全满 说明可能有下一页 247 | if (result.data.archives.length === 100) 248 | await this.getChannelVideos(uid, sid, ++page) 249 | } 250 | } 251 | 252 | /** 253 | * 查询是否已订阅 254 | * @param uid 用户ID 255 | * @param sid 频道ID 256 | * @returns 是否已订阅 257 | */ 258 | private isSubscribed(uid: number, sid: number): boolean { 259 | return ( 260 | // chanel 不是 undefined 261 | this._localData.channel !== undefined && 262 | // 频道中有 uid 263 | this._localData.channel.hasOwnProperty(uid) && 264 | // uid 下有 sid 265 | this._localData.channel[uid].indexOf(sid) !== -1 266 | // 则已订阅 267 | ) 268 | } 269 | 270 | /** 271 | * 存储本地数据 272 | */ 273 | private save() { 274 | ExtStorage.Instance().setStorage( 275 | new TSubscribeChannel(this._localData) 276 | ) 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /src/popup/views/SubscribeChannel.vue: -------------------------------------------------------------------------------- 1 | 149 | 150 | 191 | 192 | 328 | -------------------------------------------------------------------------------- /src/scripts/base/HotKeyMenu.ts: -------------------------------------------------------------------------------- 1 | import Util from '@/scripts/base/Util' 2 | import $ from 'jquery' 3 | import _ from 'lodash' 4 | 5 | export default class HKM { 6 | // 宿主元素,添加快捷键菜单的元素 7 | private overlordElement: NodeListOf | HTMLElement 8 | // 菜单 DIV 9 | private menuElement: HTMLElement = document.createElement('div') 10 | private $menuElement: JQuery = $(this.menuElement) 11 | 12 | // 所有选项 13 | private options: Array = [] 14 | // 当前选择的选项 15 | private selectedItem = -1 16 | // 键盘 17 | private keys: Array = [] 18 | 19 | private _height = 100 20 | 21 | private onKeyDownFunc: any 22 | private onMouseUpFunc: any 23 | 24 | constructor(elements: NodeListOf | HTMLElement) { 25 | // 生成快捷键菜单ID,每个快捷键菜单对应一个,与宿主元素绑定 26 | const hotKeyMenuID = Util.Instance().random() 27 | 28 | this.overlordElement = elements 29 | 30 | if (typeof elements['length'] !== 'undefined') { 31 | if (elements['length'] === 0) return this 32 | ; (this.overlordElement as NodeListOf).forEach( 33 | (overlordElement) => { 34 | this.setOverlordElement(overlordElement, hotKeyMenuID) 35 | } 36 | ) 37 | } else { 38 | this.setOverlordElement(elements as HTMLElement, hotKeyMenuID) 39 | } 40 | 41 | this.menuElement.setAttribute('class', 'Btools-hot-key-menu') 42 | this.menuElement.setAttribute('btools-hkm-id', hotKeyMenuID) 43 | 44 | // 标题 45 | const titleElement = document.createElement('p') 46 | titleElement.setAttribute('class', 'title') 47 | titleElement.innerText = '快捷键菜单' 48 | 49 | // 选项列表 50 | const optionsElement = document.createElement('ul') 51 | optionsElement.setAttribute('class', 'options') 52 | 53 | // 背景 54 | const backgroundElement = document.createElement('div') 55 | backgroundElement.setAttribute('class', 'background') 56 | 57 | // 加到菜单中 58 | this.menuElement.appendChild(titleElement) 59 | this.menuElement.appendChild(optionsElement) 60 | this.menuElement.appendChild(backgroundElement) 61 | 62 | document.body.appendChild(this.menuElement) 63 | } 64 | 65 | private setOverlordElement( 66 | overlordElement: HTMLElement, 67 | hotKeyMenuID: string 68 | ) { 69 | overlordElement.setAttribute('btools-bind-hkm-id', hotKeyMenuID) 70 | // 防止火狐拖拽图片等 71 | overlordElement.setAttribute('ondragstart', 'return false;') 72 | 73 | // 鼠标按下 74 | overlordElement.addEventListener( 75 | 'mousedown', 76 | (e: MouseEvent) => { 77 | e = e || window.event 78 | 79 | if (e.button !== 0) return false 80 | 81 | this.onMouseUpFunc = this.onMouseUp.bind(this, overlordElement) 82 | this.onKeyDownFunc = this.onKeyDown.bind(this, overlordElement) 83 | 84 | // 显示菜单 85 | this.$menuElement.show() 86 | 87 | let top = e.clientY - 65 88 | let left = e.clientX - this.menuElement.clientWidth / 2 89 | 90 | // 修正 top left 防止超出页面 91 | if (top < 0) top = 0 92 | if (top > document.body.clientHeight - this.menuElement.clientHeight) 93 | top = document.body.clientHeight - this.menuElement.clientHeight 94 | 95 | if (left < 0) left = 0 96 | if (left > document.body.clientWidth - this.menuElement.clientWidth) 97 | left = document.body.clientWidth - this.menuElement.clientWidth 98 | 99 | // 设置位移 X 中心在鼠标位置 Y 鼠标位置 - 60 也就是默认选中第一个 100 | this.$menuElement.css({ 101 | top, 102 | left 103 | }) 104 | 105 | // 监听鼠标抬起 106 | window.addEventListener('mouseup', this.onMouseUpFunc) 107 | 108 | // 监听键盘事件 109 | window.addEventListener('keydown', this.onKeyDownFunc) 110 | }, 111 | true 112 | ) 113 | } 114 | 115 | private onMouseUp() { 116 | // 获取参数 117 | const args: Array = Array.prototype.slice.call(arguments) 118 | const overlordElement = args[0] 119 | // 隐藏菜单 120 | this.menuElement.style.display = 'none' 121 | // 有 被选中的选项 并且 有 action 函数 则执行 122 | if ( 123 | this.selectedItem !== -1 && 124 | typeof this.options[this.selectedItem].action === 'function' 125 | ) { 126 | this.options[this.selectedItem].action(overlordElement) 127 | } 128 | // 删除事件监听 129 | this.removeEventListener() 130 | } 131 | 132 | private onKeyDown() { 133 | // 获取参数 134 | const args: Array = Array.prototype.slice.call(arguments) 135 | const overlordElement = args[0] 136 | const e = args[1] || window.event 137 | // 获取按键对应的数组 index 138 | const index: number = this.keys.indexOf(e.key.toUpperCase()) 139 | // 如果这个按键不在快捷键菜单内 直接 return 140 | if (index === -1) return 141 | // 隐藏菜单 142 | this.menuElement.style.display = 'none' 143 | // 有 action 函数 则执行 144 | if (typeof this.options[index].action === 'function') { 145 | this.options[index].action(overlordElement) 146 | } 147 | // 删除事件监听 148 | this.removeEventListener() 149 | } 150 | 151 | private removeEventListener() { 152 | // 删除键盘事件 153 | if (this.onKeyDownFunc !== null) { 154 | window.removeEventListener('keydown', this.onKeyDownFunc) 155 | this.onKeyDownFunc = null 156 | } 157 | 158 | // 删除鼠标事件 159 | if (this.onMouseUpFunc !== null) { 160 | window.removeEventListener('mouseup', this.onMouseUpFunc) 161 | this.onMouseUpFunc = null 162 | } 163 | } 164 | 165 | /** 166 | * 添加按键 167 | * @param options 快捷键菜单选项 168 | * @returns 当前类 169 | */ 170 | public add(options: Array): this { 171 | options.forEach((option) => { 172 | // 如果没设置 position 则默认 push 到数组末尾 173 | if (typeof option.position === 'undefined') { 174 | this.options.push(option) 175 | this.keys.push(option.key) 176 | return true 177 | } 178 | 179 | // 有的话如果是 first 则用 unshift 放在数组开头 180 | switch (option.position) { 181 | case 'first': 182 | this.options.unshift(option) 183 | this.keys.unshift(option.key) 184 | break 185 | 186 | default: 187 | this.options.push(option) 188 | this.keys.push(option.key) 189 | break 190 | } 191 | }) 192 | 193 | this.restructure() 194 | return this 195 | } 196 | 197 | /** 198 | * 根据 keyCode 删除选项 199 | * @param key 按键值 200 | */ 201 | public removeWithKey(key: string): this { 202 | const index = _.findIndex(this.keys, key) 203 | 204 | // 根据 index 删除选项 和 keys 205 | this.options.splice(index, 1) 206 | this.keys.splice(index, 1) 207 | 208 | // 重新构建菜单 209 | this.restructure() 210 | 211 | return this 212 | } 213 | 214 | /** 215 | * 设置元素 CSS 216 | * @param target 目标元素 217 | * @param css CSS 218 | */ 219 | public setCss(target: HKMElement, css: any) { 220 | switch (target) { 221 | case HKMElement.OverlordElements: 222 | $(this.overlordElement!).css(css) 223 | break 224 | case HKMElement.MenuElement: 225 | this.$menuElement.css(css) 226 | break 227 | } 228 | } 229 | 230 | public getElement(target: HKMElement) { 231 | switch (target) { 232 | case HKMElement.OverlordElements: 233 | return this.overlordElement 234 | case HKMElement.MenuElement: 235 | return this.menuElement 236 | } 237 | } 238 | 239 | private restructure() { 240 | // 获取第一个子元素 ul 241 | const optionsElement = this.menuElement.children[1] 242 | 243 | // 清空 HTML 244 | optionsElement.innerHTML = '' 245 | 246 | this.setHeightWithOptionsLength(this.options.length) 247 | 248 | this.options.forEach((option, index) => { 249 | const optionElement = document.createElement('li') 250 | 251 | const optionKeyElement = document.createElement('span') 252 | const optionTitleElement = document.createElement('p') 253 | 254 | // key 和 标题 255 | optionKeyElement.innerText = option.key.toUpperCase() 256 | optionTitleElement.innerText = option.title 257 | 258 | optionElement.addEventListener('mouseover', () => { 259 | optionElement.classList.add('btools-hkm-option-selected') 260 | // 设置当前被选中的元素 261 | this.selectedItem = index 262 | }) 263 | optionElement.addEventListener('mouseout', () => { 264 | optionElement.classList.remove('btools-hkm-option-selected') 265 | // 设置当前被选中的元素为空 266 | this.selectedItem = -1 267 | }) 268 | 269 | optionElement.appendChild(optionKeyElement) 270 | optionElement.appendChild(optionTitleElement) 271 | 272 | // 将选项 li 放入 ul 273 | optionsElement.appendChild(optionElement) 274 | }) 275 | } 276 | 277 | private setHeightWithOptionsLength(optionsLength: number) { 278 | const optionsTitleElementStyleHeight = 40 279 | const optionsElementStylePadding = optionsLength * 10 280 | const optionsElementStyleHeight = optionsLength * 30 281 | this.height = 282 | optionsTitleElementStyleHeight + 283 | optionsElementStylePadding + 284 | optionsElementStyleHeight 285 | } 286 | 287 | private set height(setHeight: number) { 288 | this._height = setHeight 289 | this.$menuElement.height(setHeight) 290 | } 291 | 292 | private get height() { 293 | return this._height 294 | } 295 | } 296 | 297 | export enum HKMElement { 298 | OverlordElements, 299 | MenuElement 300 | } 301 | -------------------------------------------------------------------------------- /src/Watcher/GetPicWatcher.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 获取封面 3 | */ 4 | 5 | import Util from '@/scripts/base/Util' 6 | import { WatcherBase, HandleOptions } from '@/Watcher/WatcherBase' 7 | import $ from 'jquery' 8 | import IconUtil from '@/scripts/base/IconUtil' 9 | import { default as HKM, HKMElement } from '@/scripts/base/HotKeyMenu' 10 | import ExtStorage from '@/scripts/base/storage/ExtStorage' 11 | import { 12 | IMultipleAccounts, 13 | TMultipleAccounts 14 | } from '@/scripts/base/storage/template/TMultipleAccounts' 15 | 16 | export class GetPicWatcher extends WatcherBase { 17 | protected init(): void { 18 | this.urls[GetPicEnum.Video] = /bilibili\.com\/video/ 19 | this.urls[GetPicEnum.Bangumi] = /bilibili\.com\/bangumi/ 20 | this.urls[GetPicEnum.Medialist] = /bilibili\.com\/medialist/ 21 | this.urls[GetPicEnum.Read] = /bilibili\.com\/read/ 22 | this.urls[GetPicEnum.LiveRoom] = /live\.bilibili\.com/ 23 | } 24 | 25 | protected handle(options: HandleOptions): void { 26 | Util.Instance().console('获取图片', 'success') 27 | 28 | switch (options.index) { 29 | case GetPicEnum.Video: 30 | this.video() 31 | break 32 | case GetPicEnum.Bangumi: 33 | this.bangumi() 34 | break 35 | case GetPicEnum.Medialist: 36 | this.medialist() 37 | break 38 | 39 | case GetPicEnum.Read: 40 | window.addEventListener('load', () => { 41 | this.read() 42 | }) 43 | break 44 | case GetPicEnum.LiveRoom: 45 | this.liveRoom() 46 | break 47 | } 48 | } 49 | 50 | private async video(selector?: string): Promise { 51 | // 获取 Btools 按钮 父元素 52 | const btools_box = await Util.Instance().getElement(selector || 'body') 53 | 54 | // 添加 Btools 按钮 55 | $(btools_box).append( 56 | ` 57 |
58 | ${IconUtil.Instance().LOGO()} 59 |
60 | ` 61 | ) 62 | 63 | const btools_button = $('.btools-get-pic-video') 64 | 65 | // 添加 快捷键菜单 66 | const hkm = new HKM(btools_button[0]).add([ 67 | { 68 | key: 'S', 69 | title: '打开封面', 70 | action: () => { 71 | this.executeScript(` 72 | if( 73 | window.__INITIAL_STATE__ && 74 | window.__INITIAL_STATE__.videoData 75 | ) 76 | window.open(window.__INITIAL_STATE__.videoData.pic) 77 | `) 78 | } 79 | } 80 | ]) 81 | 82 | btools_button.hide() 83 | 84 | $(() => { 85 | this.resetHkmPosition(hkm) 86 | btools_button.show() 87 | }) 88 | 89 | window.addEventListener('resize', () => { 90 | this.resetHkmPosition(hkm) 91 | }) 92 | 93 | return Promise.resolve(hkm) 94 | } 95 | 96 | private async bangumi() { 97 | // 有 video 98 | const hkm = await this.video() 99 | hkm.removeWithKey('S').add([ 100 | { 101 | key: 'S', 102 | title: '打开封面', 103 | action: () => { 104 | this.executeScript(` 105 | if( 106 | window.__INITIAL_STATE__ && 107 | window.__INITIAL_STATE__.epInfo 108 | ) 109 | window.open(window.__INITIAL_STATE__.epInfo.cover) 110 | `) 111 | } 112 | }, 113 | { 114 | key: 'D', 115 | title: '打开海报', 116 | action: () => { 117 | this.executeScript(` 118 | if( 119 | window.__INITIAL_STATE__ && 120 | window.__INITIAL_STATE__.mediaInfo 121 | ) 122 | window.open(window.__INITIAL_STATE__.mediaInfo.cover) 123 | `) 124 | } 125 | } 126 | ]) 127 | } 128 | 129 | private async medialist() { 130 | // 有 video 131 | 132 | await Util.Instance().getElement( 133 | '.player-auxiliary-playlist-item-active img' 134 | ) 135 | 136 | const hkm = await this.video('#video-player') 137 | $(hkm.getElement(HKMElement.OverlordElements)!).show() 138 | hkm.removeWithKey('S').add([ 139 | { 140 | key: 'S', 141 | title: '打开封面', 142 | action: async () => { 143 | const img = await Util.Instance().getElement( 144 | '.player-auxiliary-playlist-item-active img' 145 | ) 146 | window.open(this.getOriginalDrawing(img as HTMLImageElement)) 147 | } 148 | } 149 | ]) 150 | } 151 | 152 | private async read() { 153 | const imgs = await Util.Instance().getElements('#article-content img') 154 | 155 | new HKM(imgs).add([ 156 | { 157 | key: 'S', 158 | title: '打开原图', 159 | action: (img) => { 160 | this.openOriginalDrawing(img as HTMLImageElement) 161 | } 162 | }, 163 | { 164 | key: 'D', 165 | title: '新窗口打开', 166 | action: (img) => { 167 | window.open(this.getOriginalDrawing(img as HTMLImageElement)) 168 | } 169 | } 170 | ]) 171 | } 172 | 173 | private async liveRoom() { 174 | // 获取 Btools 按钮 父元素 175 | const btools_box = await Util.Instance().getElement( 176 | '.supporting-info .live-skin-coloration-area' 177 | ) 178 | 179 | // 添加 Btools 按钮 180 | const btools_button = $(btools_box) 181 | .prepend( 182 | ` 183 |
184 | ${IconUtil.Instance().LOGO()} 185 | Btools 186 |
187 | ` 188 | ) 189 | .find('.btools-get-pic-live-room') 190 | 191 | // 添加 快捷键菜单 192 | new HKM(btools_button[0]).add([ 193 | { 194 | key: 'S', 195 | title: '打开封面', 196 | action: () => { 197 | this.executeScript(` 198 | if( 199 | window.__NEPTUNE_IS_MY_WAIFU__ && 200 | window.__NEPTUNE_IS_MY_WAIFU__.roomInfoRes 201 | ) 202 | window.open(window.__NEPTUNE_IS_MY_WAIFU__.roomInfoRes.data.room_info.cover) 203 | `) 204 | } 205 | } 206 | ]) 207 | } 208 | 209 | /** 210 | * 重置快捷键菜单的位置,到播放器左上角 211 | * @param hkm 快捷键菜单 212 | */ 213 | private resetHkmPosition(hkm: HKM) { 214 | const player = $('#playerWrap') 215 | hkm.setCss(HKMElement.OverlordElements, { 216 | top: (player.offset()?.top || 0) + 10, 217 | left: (player.offset()?.left || 0) - 40 218 | }) 219 | } 220 | 221 | /** 222 | * 获取图片原图链接 223 | * @param img 图片 元素 224 | * @returns 原图链接 225 | */ 226 | private getOriginalDrawing(img: HTMLImageElement) { 227 | const srcSplit = img.src.split('@') 228 | if (srcSplit.length > 1) { 229 | return srcSplit[0] 230 | } else { 231 | return img.src 232 | } 233 | } 234 | 235 | /** 236 | * 打开原图 237 | * @param img 238 | */ 239 | private async openOriginalDrawing(img: HTMLImageElement) { 240 | // 获取页面上的打开原图容器 241 | let container = document.querySelector('.btools-origina-drawing-container') 242 | let content = document.querySelector( 243 | '.btools-origina-drawing-container .btools-img' 244 | ) 245 | let image = document.querySelector( 246 | '.btools-origina-drawing-container .btools-content img' 247 | ) 248 | // 如果没则创建 249 | if (container === null) { 250 | // 容器 251 | container = document.createElement('div') 252 | container.setAttribute('class', 'btools-origina-drawing-container') 253 | 254 | // 内容 255 | image = document.createElement('img') 256 | 257 | content = document.createElement('div') 258 | content.setAttribute('class', 'btools-content') 259 | 260 | // 背景 261 | const background = document.createElement('div') 262 | background.setAttribute('class', 'btools-background') 263 | 264 | content.appendChild(image) 265 | 266 | container.appendChild(content) 267 | container.appendChild(background) 268 | 269 | document.body.appendChild(container) 270 | 271 | // 图片已加载 时间监听 272 | image.addEventListener('load', () => { 273 | const imgDom = $(image!) 274 | const containerDom = $(container!) 275 | // 显示容器 276 | containerDom.show() 277 | 278 | // 滚动条复原 279 | $(content!).scrollTop(0).scrollLeft(0) 280 | 281 | const imageWidth = imgDom.outerWidth() 282 | const imageHeight = imgDom.outerHeight() 283 | 284 | const imageViewWidth = containerDom.outerWidth() 285 | const imageViewHeight = containerDom.outerHeight() 286 | 287 | if ( 288 | !imageViewWidth || 289 | !imageViewHeight || 290 | !imageWidth || 291 | !imageHeight 292 | ) { 293 | return 294 | } 295 | 296 | const imageTop = 297 | imageHeight < imageViewHeight 298 | ? imageViewHeight / 2 - imageHeight / 2 299 | : 0 300 | const imageLeft = 301 | imageWidth < imageViewWidth ? imageViewWidth / 2 - imageWidth / 2 : 0 302 | 303 | $(image!).css({ 304 | top: imageTop, 305 | left: imageLeft, 306 | opacity: 1 307 | }) 308 | }) 309 | } 310 | 311 | container.addEventListener('click', function () { 312 | $(image!).attr('src', '') 313 | $(container!).hide() 314 | }) 315 | 316 | // 获取原图链接 317 | const src = this.getOriginalDrawing(img) 318 | // 显示 319 | $(image!).css('opacity', 0).attr('src', src) 320 | } 321 | 322 | /** 323 | * 执行脚本(因为插件不能直接获取到页面 window) 324 | * @param code 代码 325 | */ 326 | private executeScript(code: string) { 327 | $('body') 328 | .append(``) 329 | .find('#openLiveRoomImg') 330 | .remove() 331 | } 332 | } 333 | 334 | enum GetPicEnum { 335 | /** 336 | * 视频页面 337 | */ 338 | Video, 339 | 340 | /** 341 | * 番剧、电影 342 | */ 343 | Bangumi, 344 | 345 | /** 346 | * 稍后再看、收藏夹的播放全部 347 | */ 348 | Medialist, 349 | 350 | /** 351 | * 专栏 352 | */ 353 | Read, 354 | 355 | /** 356 | * 直播间 357 | */ 358 | LiveRoom 359 | } 360 | -------------------------------------------------------------------------------- /src/scripts/base/IconUtil.ts: -------------------------------------------------------------------------------- 1 | import Singleton from './singletonBase/Singleton' 2 | 3 | export default class IconUtil extends Singleton { 4 | private _defaultColor = '#00a1d6' 5 | 6 | public LOGO( 7 | color: string = this._defaultColor, 8 | className: string = '' 9 | ): string { 10 | return `Btools` 11 | } 12 | 13 | public SHOW_MORE(color: string = this._defaultColor) { 14 | return `` 15 | } 16 | 17 | public EMPRY_DATA(color: string = this._defaultColor) { 18 | return `` 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/popup/views/MultipleAccounts.vue: -------------------------------------------------------------------------------- 1 | 139 | 140 | 188 | 189 | 494 | -------------------------------------------------------------------------------- /src/scripts/module/RetrieveInvalidVideo.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 模块:找回失效视频 3 | */ 4 | 5 | import _ from 'lodash' 6 | import $ from 'jquery' 7 | import moment from 'moment' 8 | 9 | import HKM from '@/scripts/base/HotKeyMenu' 10 | import Util from '@/scripts/base/Util' 11 | import ModuleBase from '@/scripts/module/ModuleBase' 12 | 13 | import ExtStorage from '@/scripts/base/storage/ExtStorage' 14 | import { 15 | TRetrieveInvalidVideo, 16 | IRetrieveInvalidVideo, 17 | IVideoInfo, 18 | IVideoDetail 19 | } from '@/scripts/base/storage/template' 20 | import { BilibiliApi, BiliPlus, JijiDown } from '@/api' 21 | 22 | export class RetrieveInvalidVideo extends ModuleBase { 23 | private _notFoundTitle = '未查询到视频信息' 24 | 25 | private _localData: IRetrieveInvalidVideo = { 26 | videoInfo: {}, 27 | videoDetail: {}, 28 | notInvalidVideoInfo: {} 29 | } 30 | 31 | /** 32 | * 未失效视频 已请求视频信息的 bvid 33 | */ 34 | private _notInvalidVideoBvids: string[] = [] 35 | 36 | protected async handle() { 37 | const videoList = Util.Instance().getElements( 38 | '.fav-video-list>li>a.cover,.fav-video-list>li>a.title' 39 | ) 40 | 41 | // 获取本地数据 42 | const localData = ExtStorage.Instance().getStorage< 43 | TRetrieveInvalidVideo, 44 | IRetrieveInvalidVideo 45 | >( 46 | new TRetrieveInvalidVideo({ 47 | videoInfo: {}, 48 | videoDetail: {}, 49 | notInvalidVideoInfo: {} 50 | }) 51 | ) 52 | 53 | // 初始化 54 | const res = await Promise.all([videoList, localData]) 55 | 56 | this.Init(res[0], res[1]) 57 | } 58 | 59 | private async Init( 60 | elements: NodeListOf, 61 | localData: IRetrieveInvalidVideo 62 | ) { 63 | // 设置读取到的视频信息 64 | this._localData = localData 65 | 66 | // 每次初始化都清空 67 | this._notInvalidVideoBvids = [] 68 | 69 | // 视频 AV 号 70 | const aids: number[] = [] 71 | 72 | // 循环失效视频标签 73 | _.forEach(elements, async (element) => { 74 | // 获取 BV 号 75 | const bvid = element.parentElement?.getAttribute('data-aid') 76 | if (!bvid) return true 77 | 78 | // 获取 AV 号 79 | const aid = Util.Instance().bv2av(bvid) 80 | 81 | // 是否是失效的 82 | const isDisabled = 83 | _.indexOf(element.parentElement?.classList, 'disabled') !== -1 84 | 85 | // 没有失效的视频 86 | if (!isDisabled) { 87 | await this.notInvalidVideoHandle(element, bvid) 88 | return true 89 | } 90 | 91 | // 如果在本地储存中 则用本地信息 否则获取相应的 AV 号 92 | if (isDisabled && localData.videoInfo.hasOwnProperty(bvid)) { 93 | if (!localData.videoInfo[bvid].hasOwnProperty('mid')) { 94 | if (aids.indexOf(aid) === -1) aids.push(aid) 95 | } 96 | 97 | this.setInvalidVideoInfo( 98 | element.parentElement!, 99 | localData.videoInfo[bvid].title, 100 | localData.videoInfo[bvid].pic 101 | ) 102 | 103 | // 给找到的视频添加快捷键菜单 104 | if (localData.videoInfo[bvid].title !== this._notFoundTitle) 105 | this.setHMK(element, aid.toString(), localData.videoInfo[bvid]) 106 | 107 | return true 108 | } 109 | 110 | // 不在本地存储中 添加到待查询的 aid 数组内 111 | if (aids.indexOf(aid) === -1) aids.push(aid) 112 | }) 113 | 114 | // 保存一下 115 | this.save() 116 | 117 | // 如果没有则不查询 118 | if (aids.length === 0) return 119 | 120 | // 已找到的视频 AV 号,与 aids 做对比,找出未找到的视频 121 | const findAids: number[] = [] 122 | 123 | // 请求 biliplus 查询失效视频信息 124 | const json = await BiliPlus.Instance().videoInfo(aids.join(',')) 125 | 126 | if (json.code === 0) { 127 | // 构造以找到 aid 128 | _.keys(json.data).map((str) => findAids.push(parseInt(str))) 129 | 130 | // 循环找到的视频 131 | _.forEach(json.data, async (data: IVideoInfo, _aid: string) => { 132 | const aid = parseInt(_aid) 133 | const bvid = Util.Instance().av2bv(aid) 134 | 135 | // 标题 不等于 未找到的标题 就创建快捷键菜单 136 | this.foundInvalidVideoHandle( 137 | aid, 138 | bvid, 139 | data, 140 | data.title !== this._notFoundTitle 141 | ) 142 | }) 143 | } 144 | 145 | // 处理未找到的视频 146 | _.forEach(_.difference(aids, findAids), async (aid) => { 147 | // biliplus 未找到的 发到 jijidown 继续查找 148 | const jijidownData = await JijiDown.Instance().videoInfo(`${aid}`) 149 | 150 | const bvid = Util.Instance().av2bv(aid) 151 | 152 | // 查到则保存 153 | if (jijidownData.upid !== -1) { 154 | const data = { 155 | mid: jijidownData.upid, 156 | title: jijidownData.title, 157 | pic: jijidownData.img 158 | } 159 | 160 | this.foundInvalidVideoHandle(aid, bvid, data, true) 161 | } else { 162 | // jijidown 也未找到 添加到本地存储 163 | this.addLocalData(bvid, { 164 | mid: '', 165 | title: this._notFoundTitle, 166 | pic: '' 167 | }) 168 | 169 | await Util.Instance() 170 | .getElement(`.fav-video-list>li.disabled[data-aid=${bvid}]`) 171 | .then((element) => { 172 | this.setInvalidVideoInfo(element, this._notFoundTitle) 173 | }) 174 | } 175 | }) 176 | 177 | // 不知为何 _localData 赋值慢半拍 178 | setTimeout(() => { 179 | // 数据保存到本地 180 | this.save() 181 | }, 1000) 182 | } 183 | 184 | /** 185 | * 设置失效视频信息 186 | * @param title 标题 187 | * @param cover 封面 188 | */ 189 | private setInvalidVideoInfo( 190 | element: HTMLElement, 191 | title: string, 192 | cover?: string 193 | ) { 194 | // 视频标题 195 | const color = title === this._notFoundTitle ? '#CCC' : '#F66' 196 | $(element).find('a.title').text(title).css({ 197 | color: color, 198 | 'font-weight': 700 199 | }) 200 | 201 | if (cover && cover !== '') { 202 | // 视频封面 203 | $(element) 204 | .find('a.disabled .cover-img') 205 | .attr({ 206 | src: cover, 207 | alt: '封面已被删除' 208 | }) 209 | .css('-webkit-filter', 'none') 210 | 211 | // 移除遮挡 212 | $(element).find('.disabled-cover').remove() 213 | } 214 | } 215 | 216 | /** 217 | * 已找到视频处理 218 | * @param aid AV号 219 | * @param bvid BV号 220 | * @param data 视频数据 221 | * @param isCreateHKM 是否创建快捷键菜单 222 | */ 223 | private async foundInvalidVideoHandle( 224 | aid: number, 225 | bvid: string, 226 | data: IVideoInfo, 227 | isCreateHKM: boolean 228 | ) { 229 | // 添加到本地数据对象 230 | this.addLocalData(bvid, data) 231 | 232 | // 拿到失效视频 Element 233 | const element = await Util.Instance().getElement( 234 | `.fav-video-list>li.disabled[data-aid=${bvid}]` 235 | ) 236 | 237 | this.setInvalidVideoInfo(element, data.title, data.pic) 238 | 239 | if (!isCreateHKM) return 240 | 241 | // 为失效视频创建快捷键菜单 242 | const a = await Util.Instance().getElements( 243 | `.fav-video-list>li.disabled[data-aid=${bvid}]>a.cover,.fav-video-list>li.disabled[data-aid=${bvid}]>a.title` 244 | ) 245 | 246 | a.forEach((aEle) => { 247 | this.setHMK(aEle, `${aid}`, data) 248 | }) 249 | } 250 | 251 | /** 252 | * 未失效视频处理 253 | * @param element 254 | * @param bvid 255 | */ 256 | private async notInvalidVideoHandle(element: HTMLElement, bvid: string) { 257 | // 判断是否有本地存储 258 | if (this._localData.notInvalidVideoInfo.hasOwnProperty(bvid)) { 259 | this.setNotInvalidVideoHMK(element, bvid) 260 | return 261 | } 262 | 263 | const videoInfo = await BilibiliApi.Instance().videoInfo(bvid) 264 | 265 | if (videoInfo.code === 0) { 266 | this._localData.notInvalidVideoInfo[bvid] = { 267 | mid: videoInfo.data.owner.mid, 268 | aid: videoInfo.data.aid 269 | } 270 | 271 | // 视频信息 272 | this._localData.videoInfo[bvid] = { 273 | mid: videoInfo.data.owner.mid, 274 | title: videoInfo.data.title, 275 | pic: videoInfo.data.pic 276 | } 277 | 278 | // 分P信息 279 | const partNames: string[] = [] 280 | _.forEach(videoInfo.data.pages, (item) => { 281 | partNames.push(item.part) 282 | }) 283 | 284 | // 详情信息 285 | this._localData.videoDetail[bvid] = { 286 | desc: videoInfo.data.desc, 287 | author: videoInfo.data.owner.name, 288 | partNames, 289 | created_at: moment(videoInfo.data.pubdate * 1000).format('YYYY-MM-DD HH:mm:ss') 290 | } 291 | 292 | this.setNotInvalidVideoHMK(element, bvid) 293 | } 294 | } 295 | 296 | private setNotInvalidVideoHMK(element: HTMLElement, bvid: string) { 297 | new HKM(element).add([ 298 | { 299 | key: 'S', 300 | title: '打开视频', 301 | action: () => { 302 | window.open(`https://b23.tv/${bvid}`) 303 | } 304 | }, 305 | { 306 | key: 'E', 307 | title: '打开UP主空间', 308 | action: () => { 309 | window.open( 310 | `https://space.bilibili.com/${this._localData.notInvalidVideoInfo[bvid].mid}` 311 | ) 312 | } 313 | }, 314 | { 315 | key: 'D', 316 | title: '详细信息', 317 | action: () => { 318 | this.detailInfo(this._localData.notInvalidVideoInfo[bvid].aid) 319 | } 320 | } 321 | ]) 322 | } 323 | 324 | private setHMK(element: HTMLElement, aid: string, data: IVideoInfo) { 325 | $(element).addClass('btools-user-select-none') 326 | new HKM(element).add([ 327 | { 328 | key: 'S', 329 | title: '用百度搜索', 330 | action: () => { 331 | window.open(`https://www.baidu.com/s?ie=UTF-8&wd=${data.title}`) 332 | } 333 | }, 334 | { 335 | key: 'E', 336 | title: '打开UP主空间', 337 | action: () => { 338 | if (data['mid'] !== undefined) 339 | window.open(`https://space.bilibili.com/${data.mid}`) 340 | } 341 | }, 342 | { 343 | key: 'D', 344 | title: '详细信息', 345 | action: () => { 346 | this.detailInfo(aid) 347 | } 348 | } 349 | ]) 350 | } 351 | 352 | private addLocalData(bvid: string, data: IVideoInfo) { 353 | const saveData: { [key: string]: IVideoInfo } = {} 354 | saveData[bvid] = { 355 | mid: data.mid, 356 | title: data.title, 357 | pic: data.pic 358 | } 359 | 360 | this._localData.videoInfo = _.assign(this._localData.videoInfo, saveData) 361 | } 362 | 363 | private async detailInfo(aid: string) { 364 | const bvid = Util.Instance().av2bv(parseInt(aid)) 365 | // 如果存在本地存储中 366 | if (this._localData.videoDetail.hasOwnProperty(bvid)) { 367 | this.showDetail() 368 | this.setDetailInfo(aid, bvid, this._localData.videoDetail[bvid]) 369 | return 370 | } 371 | 372 | // 显示详情窗口 此时会 loading 373 | this.showDetail() 374 | const detail = await BiliPlus.Instance().videoDetail(aid) 375 | 376 | const partNames: string[] = [] 377 | // 取出分P标题 378 | _.forEach(detail.list, (item) => { 379 | partNames.push(item.part) 380 | }) 381 | 382 | const detailInfo: IVideoDetail = { 383 | desc: detail.description, 384 | partNames: partNames, 385 | author: detail.author, 386 | created_at: detail.created_at 387 | } 388 | 389 | // 请求完成后 展示信息 390 | this.setDetailInfo(aid, bvid, detailInfo) 391 | 392 | this._localData.videoDetail[bvid] = detailInfo 393 | 394 | this.save() 395 | } 396 | 397 | private showDetail() { 398 | let detailBox = $('.btools-detail-box') 399 | if (detailBox.length === 0) { 400 | $('body').append( 401 | ` 402 |
403 | × 404 |
405 |
406 |

407 |

分P信息

408 |
    409 | 414 |
    415 |
    416 |
    417 |
    418 |
    419 |
    420 |
    421 |
    422 |
    423 |
    424 |
    425 |
    426 |
    427 |
    428 | ` 429 | ) 430 | 431 | detailBox = $('.btools-detail-box') 432 | 433 | detailBox.on('click', '.btools-close-btn', () => { 434 | detailBox.hide() 435 | }) 436 | 437 | return 438 | } 439 | 440 | // 展示 并先隐藏 视频信息 显示 loading 441 | if (detailBox.is(':hidden')) { 442 | detailBox.show() 443 | detailBox.find('.btools-video-info').hide() 444 | detailBox.find('.btools-loading').show() 445 | } 446 | } 447 | 448 | private setDetailInfo(aid: string, bvid: string, detailInfo: IVideoDetail) { 449 | const detailBox = $('.btools-detail-box') 450 | 451 | const getPartNames = 452 | detailInfo.partNames.length === 1 && detailInfo.partNames[0] === '' 453 | ? '
  • ' 454 | : this.getDetailPartNamesLi(detailInfo.partNames) 455 | 456 | // 添加分P信息 457 | detailBox.find('.btools-list').html(getPartNames) 458 | // 设置标题 459 | detailBox.find('.btools-title').text(this._localData.videoInfo[bvid].title) 460 | // 设置分P滚动条到最顶部 461 | detailBox.find('.btools-list').html(getPartNames).scrollTop(0) 462 | 463 | // 设置缓存网站 链接 464 | detailBox 465 | .find('.btools-link-jijidown') 466 | .attr('href', `https://www.jijidown.com/video/av${aid}`) 467 | detailBox 468 | .find('.btools-link-biliplus') 469 | .attr('href', `https://www.biliplus.com/video/av${aid}`) 470 | 471 | // 展示 视频信息 隐藏 loading 472 | detailBox.find('.btools-video-info').show() 473 | detailBox.find('.btools-loading').hide() 474 | } 475 | 476 | private getDetailPartNamesLi(partNames: string[]) { 477 | let result = '' 478 | _.forEach(partNames, (item, index) => { 479 | result += `
  • ${index + 1}. ${item}
  • ` 480 | }) 481 | 482 | return result 483 | } 484 | 485 | private save() { 486 | ExtStorage.Instance().setStorage< 487 | TRetrieveInvalidVideo, 488 | IRetrieveInvalidVideo 489 | >(new TRetrieveInvalidVideo(this._localData)) 490 | } 491 | } 492 | -------------------------------------------------------------------------------- /src/scripts/module/StickerHistory.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 模块 3 | * - 历史表情 4 | * - 自定义颜文字 5 | * 6 | * 移除原因:评论输入框没法通过获取 dom 修改 value 7 | */ 8 | 9 | import Util from '@/scripts/base/Util' 10 | import { TComment, IComment } from '@/scripts/base/storage/template' 11 | import ModuleBase from '@/scripts/module/ModuleBase' 12 | import $ from 'jquery' 13 | import _ from 'lodash' 14 | import { IEmoteItem, IEmotePackage, IStickerHistory } from '@/scripts/base/storage/template/TComment' 15 | import ExtStorage from '@/scripts/base/storage/ExtStorage' 16 | import IconUtil from '@/scripts/base/IconUtil' 17 | import { BilibiliApi } from '@/api' 18 | import { TMultipleAccounts, IMultipleAccounts } from '@/scripts/base/storage/template/TMultipleAccounts' 19 | 20 | export class StickerHistory extends ModuleBase { 21 | private _div!: HTMLDivElement 22 | private _container!: HTMLDivElement 23 | private _list!: HTMLUListElement 24 | private _showMore!: HTMLElement 25 | private _textarea: HTMLTextAreaElement | null = null 26 | 27 | private _addedListener = false 28 | private _localData: IComment = {} 29 | 30 | private _emoteList: IEmotePackage[] = [] 31 | 32 | private _isAddedCustomizeKaomoji = false 33 | 34 | protected async handle() { 35 | if (document.querySelector('.btools-sticker-history') !== null) return 36 | 37 | Util.Instance().console('历史表情', 'success') 38 | 39 | // 读取存储表情 40 | this._localData = await ExtStorage.Instance().getStorage< 41 | TComment, 42 | IComment 43 | >( 44 | new TComment({ 45 | stickerHistory: [], 46 | customizeKaomoji: [] 47 | }) 48 | ) 49 | 50 | // 添加历史表情页面元素 51 | this.addStickerHistoryElement() 52 | 53 | // 开启初始化 54 | this.Init() 55 | 56 | // 创建历史表情列表 57 | this.createList(this._localData) 58 | } 59 | 60 | private async addStickerHistoryElement() { 61 | this._div = document.createElement('div') 62 | this._div.classList.add('btools-sticker-history') 63 | 64 | // 历史表情 容器 用于定位 65 | this._container = document.createElement('div') 66 | this._container.classList.add( 67 | 'btools-sticker-history-container' 68 | ) 69 | 70 | // ul 71 | this._list = document.createElement('ul') 72 | this._container.appendChild(this._list) 73 | 74 | // 显示更多 按钮 75 | this._showMore = document.createElement('button') 76 | this._showMore.classList.add('btools-sticker-history-show-more') 77 | this._showMore.setAttribute('data-added-listener', 'false') 78 | this._showMore.innerHTML = IconUtil.Instance().SHOW_MORE('#CCC') 79 | this._div.append(this._showMore) 80 | 81 | this._div.appendChild(this._container) 82 | document.body.appendChild(this._div) 83 | } 84 | 85 | private async Init() { 86 | // 防止重复添加监听 87 | if (this._addedListener) return 88 | this._addedListener = true 89 | 90 | $('body').on('focus', '.reply-box-warp textarea', (e) => { 91 | this._textarea = (e.target) as HTMLTextAreaElement 92 | const textarea = $(this._textarea) 93 | $(this._div).css({ 94 | top: (textarea.offset()?.top || 0) + 66, 95 | left: (textarea.offset()?.left || 0) + 80 96 | }).show() 97 | }) 98 | 99 | $('body').on('blur', '.reply-box-warp textarea', (e) => { 100 | $(this._div).hide() 101 | }) 102 | 103 | // // 评论类型点击事件 104 | // $('body').on('click', '.clearfix li', async () => { 105 | // // 2.1. 楼中楼默认展开的情况,调用一次 replyListener 并指定 this 为 回复按钮 106 | // this.resetViewStatus() 107 | // }) 108 | 109 | // // 给 reply 按钮添加监听 110 | // $('body').on('click', '.list-item .reply', () => { 111 | // this.resetViewStatus() 112 | // }) 113 | 114 | // 表情按钮点击事件 115 | $('body').on('click', '.reply-box .emoji-btn', async () => { 116 | // 表情盒子 title 117 | const emojiBoxTitle = await Util.Instance().getElement( 118 | '.emoji-panel .emoji-title' 119 | ) 120 | 121 | this._isAddedCustomizeKaomoji = false 122 | 123 | // 监听 表情类型 title 内容变化 124 | let customizeKaomojiElement: JQuery 125 | 126 | $(emojiBoxTitle).on('DOMNodeInserted', async (e) => { 127 | if (!this._isAddedCustomizeKaomoji) { 128 | const emoji_box = await Util.Instance().getElement('.emoji-panel') 129 | // 自定义颜文字 输入框 130 | customizeKaomojiElement = $(emoji_box) 131 | .append( 132 | ` 133 |
    134 | 135 |
    136 | ` 137 | ) 138 | .find('.customize-kaomoji') 139 | .on('keyup', (e) => { 140 | // 过滤回车 141 | if (e.key !== 'Enter') return 142 | 143 | // 不能为空 144 | if ((e.target as HTMLInputElement).value.trim() === '') return 145 | 146 | const isAdded = this.addCustomizeKaomoji( 147 | (e.target as HTMLInputElement).value 148 | ) 149 | // 添加成功 150 | if (isAdded) { 151 | // 删除文本框内容 152 | (e.target as HTMLInputElement).value = '' 153 | } 154 | }) 155 | this._isAddedCustomizeKaomoji = true 156 | } 157 | 158 | if (!customizeKaomojiElement) return 159 | 160 | if ($(emojiBoxTitle).text() === '颜文字') { 161 | this.createCustomizeKaomoji() 162 | customizeKaomojiElement?.show() 163 | } else { 164 | customizeKaomojiElement?.hide() 165 | $('.btools-customize-kaomoji').remove() 166 | } 167 | }) 168 | }) 169 | 170 | // 表情点击事件 171 | $(document).on('click', '.emoji-content .emoji-info', async (e) => { 172 | const currentTarget = $(e.currentTarget) 173 | const img = currentTarget.find('img') 174 | 175 | let stickerHistory: IStickerHistory 176 | 177 | // 没图片则是颜文字 178 | if (img.length === 0) { 179 | stickerHistory = { 180 | isKaomoji: true, 181 | text: currentTarget.text(), 182 | src: '' 183 | } 184 | } else { 185 | // 获取当前表情包的类型图片 url 186 | const currentTypeSrc = $('.emoji-tab .current-type img').attr('src') 187 | // 根据 url 搜索表情包列表 188 | const currentEmote = _.find(this._emoteList, item => item.url.indexOf(currentTypeSrc!)) as IEmotePackage 189 | 190 | // 点击表情的图片 url 191 | const src = img.attr('src') || '' 192 | 193 | if (!currentEmote || src === '') { 194 | return 195 | } 196 | 197 | // 再根据表情图片 url 搜表情,最终获取到表情对应的文本 198 | const emote = _.find(currentEmote.emote, item => item.url.indexOf(src)) as IEmoteItem 199 | 200 | stickerHistory = { 201 | isKaomoji: false, 202 | text: emote.text, 203 | src 204 | } 205 | } 206 | 207 | const index = _.findIndex(this._localData.stickerHistory, stickerHistory) 208 | 209 | // 如果存在则只排序 210 | if (index !== -1) { 211 | this.removeAndAddToFirst(this._localData.stickerHistory!, index) 212 | this.save(true) 213 | 214 | return 215 | } 216 | 217 | this._localData.stickerHistory?.unshift(stickerHistory) 218 | 219 | await ExtStorage.Instance().setStorage( 220 | new TComment({ 221 | stickerHistory: this._localData.stickerHistory 222 | }) 223 | ) 224 | 225 | this.createList(this._localData) 226 | }) 227 | 228 | // 请求表情列表 229 | this._emoteList = await BilibiliApi.Instance().emoteList().then(res => { 230 | return _.get(res, 'data.packages', []) 231 | }) 232 | 233 | } 234 | 235 | private createList(data: IComment) { 236 | let isShowMore = false 237 | 238 | const ul = $(this._list) 239 | ul.html('') 240 | 241 | if (!data.stickerHistory || data.stickerHistory.length === 0) { 242 | return 243 | } 244 | 245 | data.stickerHistory.forEach((item, index) => { 246 | const li = document.createElement('li') 247 | li.setAttribute('class', item.isKaomoji ? 'kaomoji' : 'img') 248 | 249 | li.innerHTML = item.isKaomoji 250 | ? `${item.text}` 251 | : `` 252 | 253 | li.addEventListener( 254 | 'click', 255 | (e: MouseEvent) => { 256 | e.preventDefault() 257 | e.stopPropagation() 258 | 259 | // 左键发送表情 260 | if (e.button === 0) { 261 | // 插入文字 262 | this.insertText(item.text) 263 | 264 | // 获取是否点击 265 | const isClick = li.getAttribute('data-is-click') 266 | // 如果已经点击 则不重复添加监听 267 | if (isClick === 'true') return 268 | li.setAttribute('data-is-click', 'true') 269 | // 添加监听 鼠标移出后把表情排到第一个 270 | li.addEventListener( 271 | 'mouseleave', 272 | () => { 273 | this.removeAndAddToFirst( 274 | this._localData.stickerHistory!, 275 | index 276 | ) 277 | 278 | this.save(true) 279 | }, 280 | { 281 | once: true 282 | } 283 | ) 284 | } 285 | }, 286 | false 287 | ) 288 | 289 | li.addEventListener('mousedown', (e: MouseEvent) => { 290 | e.preventDefault() 291 | e.stopPropagation() 292 | // 中键删除表情 293 | if (e.button === 1) { 294 | // 删除对应表情 295 | this._localData.stickerHistory?.splice(index, 1) 296 | this.save(true) 297 | } 298 | }) 299 | 300 | ul.append(li) 301 | }) 302 | 303 | // 高度大于两行则显示 “显示更多” 按钮 304 | const ulHeight = ul.height() 305 | if (ulHeight && ulHeight > 30) { 306 | isShowMore = true 307 | } 308 | 309 | if (isShowMore) { 310 | // 显示 311 | $(this._showMore).show() 312 | 313 | // 防止重复添加点击事件 314 | if (this._showMore.getAttribute('data-added-listener') === 'true') return 315 | this._showMore.setAttribute('data-added-listener', 'true') 316 | this._showMore.setAttribute('data-is-show', 'false') 317 | 318 | // 添加点击事件 319 | this._showMore.addEventListener('click', (e: MouseEvent) => { 320 | e.preventDefault() 321 | e.stopPropagation() 322 | 323 | const ul = $(this._list) 324 | const row = (ul.height() || 1) / 30 325 | const isShow = this._showMore.getAttribute('data-is-show') === 'true' 326 | 327 | if (isShow) { 328 | // 隐藏 329 | $(this._container).removeClass('more').css({ 330 | height: 30 331 | }) 332 | } else { 333 | // 显示 334 | $(this._container) 335 | .addClass('more') 336 | .css({ 337 | height: 30 * row 338 | }) 339 | } 340 | 341 | this._showMore.setAttribute('data-is-show', isShow ? 'false' : 'true') 342 | }) 343 | } else { 344 | $(this._container).removeClass('more').css({ 345 | height: 30 346 | }) 347 | 348 | $(this._showMore).hide() 349 | } 350 | } 351 | 352 | private createCustomizeKaomoji() { 353 | if (this._localData.customizeKaomoji?.length === 0) return 354 | 355 | _.forEachRight(this._localData.customizeKaomoji, (item) => { 356 | this.addCustomizeKaomojiToEmojiBox(item.text) 357 | }) 358 | } 359 | 360 | /** 361 | * 保存本地存储 362 | * @param isStickerHistory 是否是历史表情 363 | */ 364 | private save(isStickerHistory: boolean) { 365 | const storage = ExtStorage.Instance().setStorage( 366 | new TComment(this._localData) 367 | ) 368 | 369 | if (isStickerHistory) { 370 | storage.then((resData) => { 371 | // 重新创建表情列表 372 | this.createList(resData) 373 | }) 374 | } 375 | } 376 | 377 | private async insertText(text: string) { 378 | // 插入文字 379 | const $t = this._textarea! 380 | 381 | if ($t.selectionStart || $t.selectionStart === 0) { 382 | const startPos = $t.selectionStart 383 | const endPos = $t.selectionEnd 384 | const scrollTop = $t.scrollTop 385 | $t.value = `${$t.value.substring( 386 | 0, 387 | startPos 388 | )}${text}${$t.value.substring(endPos, $t.value.length)}` 389 | $t.focus() 390 | $t.selectionStart = startPos + text.length 391 | $t.selectionEnd = startPos + text.length 392 | $t.scrollTop = scrollTop 393 | } else { 394 | $t.value += text 395 | $t.focus() 396 | } 397 | } 398 | 399 | /** 400 | * 添加颜文字 401 | * @param val 颜文字 402 | */ 403 | private addCustomizeKaomoji(val: string): boolean { 404 | // 检查是否已存在 405 | const index = _.findIndex(this._localData.customizeKaomoji, { 406 | text: val 407 | }) 408 | 409 | if (index !== -1) return false 410 | 411 | // 添加界面 412 | this.addCustomizeKaomojiToEmojiBox(val) 413 | 414 | // 本地存储 415 | this._localData.customizeKaomoji!.unshift({ 416 | isBig: false, 417 | text: val 418 | }) 419 | 420 | this.save(false) 421 | 422 | return true 423 | } 424 | 425 | /** 426 | * 添加颜文字到颜文字框(添加到界面上) 427 | * @param val 颜文字 428 | */ 429 | private async addCustomizeKaomojiToEmojiBox(val: string) { 430 | const emojiElement = document.createElement('div') 431 | emojiElement.setAttribute( 432 | 'class', 433 | 'emoji-info btools-customize-kaomoji' 434 | ) 435 | emojiElement.setAttribute('data-emoji-text', val) 436 | emojiElement.setAttribute('data-is-click', 'false') 437 | emojiElement.innerHTML = ` 438 |
    ${val}
    439 | ` 440 | 441 | const enmoji_box = await Util.Instance().getElement( 442 | '.emoji-panel .emoji-content' 443 | ) 444 | $(enmoji_box).prepend(emojiElement) 445 | 446 | emojiElement.addEventListener('mousedown', (e) => { 447 | e.preventDefault() 448 | e.stopPropagation() 449 | 450 | const isClick = emojiElement.getAttribute('data-is-click') 451 | 452 | const index = _.findIndex(this._localData.customizeKaomoji, { 453 | text: val 454 | }) 455 | 456 | if (isClick !== 'true') { 457 | emojiElement.addEventListener( 458 | 'mouseleave', 459 | () => { 460 | // 界面 461 | emojiElement.setAttribute('data-is-click', 'false') 462 | emojiElement.remove() 463 | $(enmoji_box).prepend(emojiElement) 464 | // 存储 465 | this.removeAndAddToFirst(this._localData.customizeKaomoji!, index) 466 | this.save(false) 467 | }, 468 | { 469 | once: true 470 | } 471 | ) 472 | } 473 | 474 | // 左键 475 | if (e.button === 0) { 476 | emojiElement.setAttribute('data-is-click', 'true') 477 | return 478 | } 479 | 480 | // 中键 删除 481 | if (e.button === 1) { 482 | this._localData.customizeKaomoji!.splice(index, 1) 483 | 484 | this.save(false) 485 | 486 | emojiElement.remove() 487 | } 488 | }) 489 | } 490 | 491 | private removeAndAddToFirst(arr: Array, index: number) { 492 | // 先删除 493 | const removedItem = arr.splice(index, 1) 494 | // 添加到第一个 495 | if (removedItem && removedItem.length === 1) arr.unshift(removedItem[0]) 496 | } 497 | 498 | // private resetViewStatus() { 499 | // if ($('.open-reply .comment-send').length === 0) { 500 | // $( 501 | // this._sticker_history_dom_info!.btools_sticker_history['list-item'] 502 | // ).hide() 503 | // return 504 | // } 505 | 506 | // const btoolsStickerHistory = $( 507 | // this._sticker_history_dom_info!.btools_sticker_history['list-item'] 508 | // ) 509 | 510 | // btoolsStickerHistory.show().remove() 511 | 512 | // $('.open-reply').append(btoolsStickerHistory) 513 | // } 514 | } 515 | --------------------------------------------------------------------------------