├── .cursor
└── rules
│ ├── command-rules.mdc
│ ├── document-rules.mdc
│ ├── i18n-rules.mdc
│ ├── settings-rules.mdc
│ └── styles-rules.mdc
├── .github
├── ISSUE_TEMPLATE
│ └── bug_issue_template.yaml
└── workflows
│ └── ci.yml
├── .gitignore
├── .markdownlint.yaml
├── .npmrc
├── .release-it.json
├── .vscode
├── extensions.json
└── settings.json
├── LICENSE
├── README.md
├── assets
├── anime-timetable-icons.png
├── broken-image.png
├── empty.png
├── icon-512-flat.png
├── icon-512.png
├── loading.gif
├── logo-cat.svg
└── rules.json
├── docs
├── CONTRIBUTING-cmn_CN.md
├── CONTRIBUTING-cmn_TW.md
└── CONTRIBUTING.md
├── eslint.config.mjs
├── knip.json
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── scripts
├── client.ts
├── manifest.ts
├── prepare.ts
└── utils.ts
├── shim.d.ts
├── src
├── _locales
│ ├── cmn-CN.yml
│ ├── cmn-TW.yml
│ ├── en.yml
│ └── jyut.yml
├── auto-imports.d.ts
├── background
│ ├── index.ts
│ ├── messageListeners
│ │ ├── api
│ │ │ ├── anime.ts
│ │ │ ├── auth.ts
│ │ │ ├── favorite.ts
│ │ │ ├── history.ts
│ │ │ ├── index.ts
│ │ │ ├── live.ts
│ │ │ ├── moment.ts
│ │ │ ├── notification.ts
│ │ │ ├── ranking.ts
│ │ │ ├── search.ts
│ │ │ ├── user.ts
│ │ │ ├── video.ts
│ │ │ └── watchLater.ts
│ │ └── tabs.ts
│ └── utils.ts
├── components
│ ├── ALink.vue
│ ├── AppBackground.vue
│ ├── BangumiCard
│ │ ├── BangumiCard.vue
│ │ └── BangumiCardSkeleton.vue
│ ├── Button.vue
│ ├── CodeEditor.vue
│ ├── Dialog.vue
│ ├── Dock
│ │ ├── Dock.vue
│ │ └── types.ts
│ ├── Empty.vue
│ ├── HorizontalScrollView.vue
│ ├── IframeDrawer.vue
│ ├── IframePage.vue
│ ├── Input.vue
│ ├── List
│ │ ├── List.vue
│ │ └── ListItem.vue
│ ├── Loading.vue
│ ├── Logo.vue
│ ├── OverlayScrollbarsComponent.ts
│ ├── Picture.vue
│ ├── PipWindow.vue
│ ├── Progress.vue
│ ├── README.md
│ ├── Radio.vue
│ ├── SearchBar
│ │ ├── SearchBar.vue
│ │ └── searchHistoryProvider.ts
│ ├── Select.vue
│ ├── Settings
│ │ ├── About
│ │ │ └── About.vue
│ │ ├── Appearance
│ │ │ └── Appearance.vue
│ │ ├── BIlibiliSettings
│ │ │ └── BilibiliSettings.vue
│ │ ├── BewlyPages
│ │ │ ├── BewlyPages.vue
│ │ │ ├── Home
│ │ │ │ ├── Home.vue
│ │ │ │ └── components
│ │ │ │ │ ├── FilterByTitleTable.vue
│ │ │ │ │ └── FilterByUserTable.vue
│ │ │ └── SearchPage
│ │ │ │ └── SearchPage.vue
│ │ ├── Compatibility
│ │ │ └── Compatibility.vue
│ │ ├── DesktopAndDock
│ │ │ └── DesktopAndDock.vue
│ │ ├── General
│ │ │ └── General.vue
│ │ ├── Settings.vue
│ │ ├── Shortcuts
│ │ │ └── Shortcuts.vue
│ │ ├── VolumeBalance
│ │ │ └── VolumeBalance.vue
│ │ ├── components
│ │ │ ├── ChangeWallpaper.vue
│ │ │ ├── SettingsItem.vue
│ │ │ └── SettingsItemGroup.vue
│ │ └── types.ts
│ ├── SideBar
│ │ ├── SideBar.vue
│ │ └── types.ts
│ ├── Slider.vue
│ ├── Tooltip.vue
│ ├── TopBar
│ │ ├── BewlyOrBiliTopBarSwitcher.vue
│ │ ├── TopBar.vue
│ │ ├── components
│ │ │ ├── BewlyOrBiliPageSwitcher.vue
│ │ │ ├── NotificationsDrawer.vue
│ │ │ ├── TopBarHeader.vue
│ │ │ ├── TopBarLogo.vue
│ │ │ ├── TopBarRight.vue
│ │ │ ├── TopBarSearch.vue
│ │ │ └── pops
│ │ │ │ ├── ChannelsPop.vue
│ │ │ │ ├── FavoritesPop.vue
│ │ │ │ ├── HistoryPop.vue
│ │ │ │ ├── MomentsPop.vue
│ │ │ │ ├── MorePop.vue
│ │ │ │ ├── NotificationsPop.vue
│ │ │ │ ├── UploadPop.vue
│ │ │ │ ├── UserPanelPop.vue
│ │ │ │ └── WatchLaterPop.vue
│ │ ├── composables
│ │ │ └── useTopBarInteraction.ts
│ │ ├── constants
│ │ │ └── urls.ts
│ │ ├── notify.ts
│ │ ├── styles
│ │ │ └── index.scss
│ │ └── types.ts
│ ├── VideoCard
│ │ ├── VideoCard.vue
│ │ ├── VideoCardAuthor
│ │ │ └── components
│ │ │ │ ├── VideoCardAuthorAvatar.vue
│ │ │ │ └── VideoCardAuthorName.vue
│ │ ├── VideoCardContextMenu
│ │ │ ├── VideoCardContextMenu.vue
│ │ │ └── components
│ │ │ │ └── DislikeDialog.vue
│ │ ├── VideoCardSkeleton.vue
│ │ ├── types.ts
│ │ └── utils.ts
│ └── index.ts
├── composables
│ ├── useAppProvider.ts
│ ├── useDark.ts
│ ├── useDelayedHover.ts
│ ├── useFilter.ts
│ └── useStorageLocal.ts
├── constants
│ ├── globalEvents.ts
│ └── imgs.ts
├── contentScripts
│ ├── index.ts
│ └── views
│ │ ├── Anime
│ │ ├── Anime.vue
│ │ └── components
│ │ │ └── AnimeTimeTable.vue
│ │ ├── App.vue
│ │ ├── Favorites
│ │ └── Favorites.vue
│ │ ├── History
│ │ └── History.vue
│ │ ├── Home
│ │ ├── Home.vue
│ │ ├── components
│ │ │ ├── Following.vue
│ │ │ ├── ForYou.vue
│ │ │ ├── Live.vue
│ │ │ ├── Ranking.vue
│ │ │ ├── SubscribedSeries.vue
│ │ │ └── Trending.vue
│ │ └── types.ts
│ │ ├── Moments
│ │ └── Moments.vue
│ │ ├── Search
│ │ └── Search.vue
│ │ ├── WatchLater
│ │ └── WatchLater.vue
│ │ └── necessarySettingsWatchers.ts
├── enums
│ └── appEnums.ts
├── global.d.ts
├── inject
│ ├── README.md
│ └── index.ts
├── logic
│ ├── common-setup.ts
│ ├── index.ts
│ └── storage.ts
├── manifest.ts
├── models
│ ├── anime
│ │ ├── popular.ts
│ │ ├── recommendation.ts
│ │ ├── timeTable.ts
│ │ └── watchList.ts
│ ├── history
│ │ └── history.ts
│ ├── live
│ │ └── getFollowingLiveList.ts
│ ├── moment
│ │ └── moment.ts
│ └── video
│ │ ├── appForYou.ts
│ │ ├── favorite.ts
│ │ ├── favoriteCategory.ts
│ │ ├── forYou.ts
│ │ ├── historySearch.ts
│ │ ├── ranking.ts
│ │ ├── rankingPgc.ts
│ │ ├── trending.ts
│ │ ├── videoInfo.ts
│ │ ├── videoPreview.ts
│ │ └── watchLater.ts
├── options
│ ├── Options.vue
│ ├── index.html
│ └── main.ts
├── popup
│ ├── Popup.vue
│ ├── index.html
│ └── main.ts
├── stores
│ ├── mainStore.ts
│ ├── settingsStore.ts
│ └── topBarStore.ts
├── styles
│ ├── adaptedStyles
│ │ ├── adaptedStyles-cmn_CN.md
│ │ ├── adaptedStyles-cmn_TW.md
│ │ ├── adaptedStyles-jyut.md
│ │ ├── adaptedStyles.md
│ │ ├── common
│ │ │ ├── btn.scss
│ │ │ ├── comments.scss
│ │ │ ├── common.scss
│ │ │ ├── footer.scss
│ │ │ ├── index.ts
│ │ │ ├── loginDialog.scss
│ │ │ ├── modal.scss
│ │ │ ├── topBar.scss
│ │ │ ├── userCard.scss
│ │ │ └── videoPlayer.scss
│ │ ├── forceDark.scss
│ │ ├── index.ts
│ │ ├── pages
│ │ │ ├── accountSettingsPage.scss
│ │ │ ├── animePage.scss
│ │ │ ├── animePlayback&MoviePage.scss
│ │ │ ├── articlesPage.scss
│ │ │ ├── channelPage.scss
│ │ │ ├── creativeCenterPage.scss
│ │ │ ├── error404Page.scss
│ │ │ ├── historyPage.scss
│ │ │ ├── homePage.scss
│ │ │ ├── loginPage.scss
│ │ │ ├── momentsPage.scss
│ │ │ ├── notePage.scss
│ │ │ ├── notificationsPage.scss
│ │ │ ├── premiumPage.scss
│ │ │ ├── searchPage.scss
│ │ │ ├── topicPage.scss
│ │ │ ├── userSpacePage.scss
│ │ │ ├── videoPage.scss
│ │ │ └── watchLaterPage.scss
│ │ ├── shadowDom
│ │ │ ├── comments.scss
│ │ │ ├── index.ts
│ │ │ └── userProfile.scss
│ │ └── thirdParties
│ │ │ ├── bilibiliEnhanceVideoList.scss
│ │ │ ├── bilibiliEvolved.scss
│ │ │ └── index.ts
│ ├── blockAds.scss
│ ├── fonts.scss
│ ├── index.ts
│ ├── main.scss
│ ├── removeTopBar.scss
│ ├── reset.css
│ ├── transitionAndTransitionGroup.scss
│ └── variables.scss
├── tests
│ ├── demo.spec.ts
│ └── uriParse.spec.ts
└── utils
│ ├── api.ts
│ ├── appSign.ts
│ ├── authProvider.ts
│ ├── dataFormatter.ts
│ ├── element.ts
│ ├── i18n.ts
│ ├── lazyLoad.ts
│ ├── localWallpaper.ts
│ ├── lvIcons.ts
│ ├── main.ts
│ ├── mitt.ts
│ ├── player.ts
│ ├── shortcuts.ts
│ ├── svgIcons.ts
│ ├── tabs.ts
│ ├── timer.ts
│ ├── transformer.ts
│ ├── uriParse.ts
│ ├── volumeBalance.ts
│ └── volumeSliders.ts
├── tsconfig.json
├── tsup.config.ts
├── unocss.config.ts
├── vite.config.content.ts
├── vite.config.inject.ts
└── vite.config.ts
/.cursor/rules/command-rules.mdc:
--------------------------------------------------------------------------------
1 | ---
2 | description:
3 | globs:
4 | alwaysApply: true
5 | ---
6 | 1. The project uses pnpm as the package manager.
7 | 2. Do not use the command line for validation or testing after making changes.
--------------------------------------------------------------------------------
/.cursor/rules/document-rules.mdc:
--------------------------------------------------------------------------------
1 | ---
2 | description:
3 | globs:
4 | alwaysApply: true
5 | ---
6 | 1. Do not create summary/test documents after each modification.
7 | 2. Do not modify markdown (.md) files in the project unless explicitly requested.
8 | 3. Do not create markdown (.md) files unless explicitly requested.
--------------------------------------------------------------------------------
/.cursor/rules/i18n-rules.mdc:
--------------------------------------------------------------------------------
1 | ---
2 | description:
3 | globs:
4 | alwaysApply: true
5 | ---
6 | 1. The plugin currently uses i18n, and the correct import method is import { t } from '~/utils/dataFormatter'.
7 | 2. Only use the content of src/_locales/cmn-CN.yml as a reference.
8 | 3. Do not modify any i18n configuration files; it’s acceptable to output the translations that need to be added to cmn-CN.yml.
--------------------------------------------------------------------------------
/.cursor/rules/settings-rules.mdc:
--------------------------------------------------------------------------------
1 | ---
2 | description: The following are the requirements related to the Settings section of the browser extension.
3 | globs:
4 | alwaysApply: false
5 | ---
6 | 1. The definitions and initial values of the settings are in src/logic/storage.ts.
7 | 2. The settings components are located under src/components/Settings.
8 | 3. Related configuration items should be grouped within a SettingsItemGroup.
--------------------------------------------------------------------------------
/.cursor/rules/styles-rules.mdc:
--------------------------------------------------------------------------------
1 | ---
2 | description: Guidelines for defining component styles using CSS and SCSS in the project.
3 | globs:
4 | alwaysApply: false
5 | ---
6 | The project has already defined BEM-related styles.
7 | 1. The definition file is src/styles/variables.scss.
8 | 2. Theme styles should be used as a priority to ensure visual consistency.
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_issue_template.yaml:
--------------------------------------------------------------------------------
1 | name: 问题报告
2 | description: 遇到错误请在此报告。
3 | labels: [bug]
4 |
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | 标题不填或直接随便写类似“bug”“错误”“有问题”简单带过的 issue 直接 close + lock 不解释,如果是旧版本的问题或是已经有人提问过的问题将会关闭。
10 |
11 | 功能请求不是在问题报告里面写的,请[开启空 issue](https://github.com/keleus/BewlyCat/issues/new)。
12 |
13 | 若遇到页面相关问题(比如某页面下出现了不该出现的元素),我们建议一并附上发生问题的页面链接。
14 |
15 | - type: textarea
16 | attributes:
17 | label: 环境信息
18 | description: 【请勿修改 issue 模版。】扩展版本、浏览器版本、以及你做出的自定义设置。
19 | placeholder: |
20 | - 浏览器(如 Google Chrome):
21 | - 浏览器版本(如 126.0.6478.126):
22 | - BewlyCat 版本(如 1.0.3):
23 |
24 | 如果你修改了 BewlyCat 的设置,请写在下面以方便我們排查問題(可粗略写成类似“设置了××后出现这个问题”〔将“××”替换为你的设置项〕):
25 |
26 | value: |
27 | - 浏览器(如 Google Chrome):
28 | - 浏览器版本(如 126.0.6478.126):
29 | - BewlyCat 版本(如 0.20.1):
30 |
31 | 如果你修改了 BewlyCat 的设置,请写在下面以方便我們排查問題(可粗略写成类似“设置了××后出现这个问题”〔将“××”替换为你的设置项〕):
32 |
33 | validations:
34 | required: true
35 |
36 | - type: textarea
37 | attributes:
38 | label: 问题描述
39 | description: 如何重现,最好带有截图或视频以便排查。
40 | placeholder: |
41 | 请预先搜索此问题是否在其他 issue 中出现过
42 | validations:
43 | required: true
44 |
45 | - type: textarea
46 | attributes:
47 | label: 预期行为
48 | description: 你认为应该是什么行为。
49 | validations:
50 | required: false
51 |
52 | - type: checkboxes
53 | attributes:
54 | label: 最终确认
55 | description: 请确认以下所有内容,否则将被 close。
56 | options:
57 | - label: 我确认在停用 BewlyCat 并强制刷新(按住 Shift 键的同时按刷新键)后,问题不再出现。
58 | required: false
59 | - label: 我确认此问题未在其他 issue 中出现过。
60 | required: false
61 | - label: 我确认我正在使用最新的 BewlyCat 版本。
62 | required: false
63 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - dev
7 | - main
8 | paths-ignore:
9 | - LICENSE
10 | - README-cmn_CN.md
11 | - README-cmn_TW.md
12 | - docs/**
13 |
14 | pull_request:
15 | branches:
16 | - dev
17 | - main
18 | paths-ignore:
19 | - LICENSE
20 | - README-cmn_CN.md
21 | - README-cmn_TW.md
22 | - docs/**
23 |
24 | jobs:
25 | test:
26 | name: Test
27 | strategy:
28 | matrix:
29 | node: [lts/*, lts/-1]
30 | os: [ubuntu-latest, windows-latest]
31 | fail-fast: false
32 | runs-on: ${{ matrix.os }}
33 | timeout-minutes: 10
34 |
35 | steps:
36 | - uses: actions/checkout@v4
37 |
38 | - name: Set node ${{ matrix.node }}
39 | uses: actions/setup-node@v4
40 | with:
41 | node-version: ${{ matrix.node }}
42 |
43 | - name: Install pnpm
44 | uses: pnpm/action-setup@v4
45 | with:
46 | run_install: |
47 | - args: [--frozen-lockfile]
48 |
49 | - name: Lint
50 | if: ${{ matrix.os == 'ubuntu-latest' }}
51 | run: pnpm run lint
52 |
53 | - name: Type check
54 | if: ${{ matrix.os == 'ubuntu-latest' }}
55 | run: pnpm run typecheck
56 |
57 | - name: Knip
58 | if: ${{ matrix.os == 'ubuntu-latest' }}
59 | run: pnpm run knip
60 |
61 | - name: Build Extension
62 | run: pnpm build
63 |
64 | - name: Upload Zip
65 | if: ${{ matrix.os == 'ubuntu-latest' && matrix.node == 'lts/*' && github.ref_name == 'main' }}
66 | uses: actions/upload-artifact@v4.3.1
67 | with:
68 | name: BewlyCat Zip
69 | path: extension
70 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .idea/
3 | .vite-ssg-dist
4 | .vite-ssg-temp
5 | *.crx
6 | *.local
7 | *.log
8 | *.pem
9 | *.xpi
10 | *.zip
11 | dist
12 | dist-ssr
13 | extension/
14 | extension-firefox/
15 | extension-safari/
16 | node_modules
17 | web-ext-profile
18 | extension-safari-macos/
19 | # src/old-extension/
--------------------------------------------------------------------------------
/.markdownlint.yaml:
--------------------------------------------------------------------------------
1 | MD013: false
2 |
3 | MD028: false
4 |
5 | MD029: false
6 |
7 | MD033:
8 | allowed_elements: [a, br, img, p, h1, details, summary]
9 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | shamefully-hoist=true
2 | auto-install-peers=true
3 | strict-peer-dependencies=false
4 |
--------------------------------------------------------------------------------
/.release-it.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": {
3 | "release-it-pnpm": {
4 | "publishCommand": "echo 'skipping publish'"
5 | }
6 | },
7 | "git": {
8 | "commitMessage": "chore: release v${version}",
9 | "tagName": "v${version}"
10 | },
11 | "hooks": {
12 | "before:init": [
13 | "pnpm run lint",
14 | "pnpm run typecheck",
15 | "pnpm run test --run"
16 | ],
17 | "after:bump": [
18 | "pnpm run build",
19 | "pnpm run build-firefox",
20 | "pnpm run pack:zip",
21 | "pnpm run pack:zip-firefox",
22 | "pnpm run pack:zip-firefox-sources"
23 | ],
24 | "after:release": [
25 | "gh release upload v${version} extension.zip extension-firefox.zip",
26 | "pnpm run submit"
27 | ]
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "vue.volar",
4 | "antfu.iconify",
5 | "dbaeumer.vscode-eslint",
6 | "antfu.unocss",
7 | "csstools.postcss",
8 | "lokalise.i18n-ally",
9 | "streetsidesoftware.code-spell-checker",
10 | "dbaeumer.vscode-eslint"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | // Enable the ESlint flat config support
3 | "eslint.useFlatConfig": true,
4 | "typescript.enablePromptUseWorkspaceTsdk": true,
5 | // Disable the default formatter, use eslint instead
6 | "prettier.enable": false,
7 | "editor.formatOnSave": false,
8 |
9 | // Auto fix
10 | "editor.codeActionsOnSave": {
11 | "source.fixAll.eslint": "explicit",
12 | "source.organizeImports": "never"
13 | },
14 |
15 | // Silent the stylistic rules in you IDE, but still auto fix them
16 | "eslint.rules.customizations": [
17 | { "rule": "style/*", "severity": "off" },
18 | { "rule": "format/*", "severity": "off" },
19 | { "rule": "*-indent", "severity": "off" },
20 | { "rule": "*-spacing", "severity": "off" },
21 | { "rule": "*-spaces", "severity": "off" },
22 | { "rule": "*-order", "severity": "off" },
23 | { "rule": "*-dangle", "severity": "off" },
24 | { "rule": "*-newline", "severity": "off" },
25 | { "rule": "*quotes", "severity": "off" },
26 | { "rule": "*semi", "severity": "off" }
27 | ],
28 |
29 | // Enable eslint for all supported languages
30 | "eslint.validate": [
31 | "javascript",
32 | "javascriptreact",
33 | "typescript",
34 | "typescriptreact",
35 | "vue",
36 | "html",
37 | "markdown",
38 | "json",
39 | "jsonc",
40 | "yaml",
41 | "toml",
42 | "xml",
43 | "gql",
44 | "graphql",
45 | "astro",
46 | "css",
47 | "less",
48 | "scss",
49 | "pcss",
50 | "postcss"
51 | ],
52 |
53 | "cSpell.words": [
54 | "bangumi",
55 | "bewly",
56 | "bili",
57 | "bilibili",
58 | "bvid",
59 | "Danmaku",
60 | "danmakus",
61 | "iconfont",
62 | "lightoff",
63 | "notrigger",
64 | "owari",
65 | "pagefullscreen",
66 | "pinia",
67 | "prismjs",
68 | "pubdate",
69 | "squirtle",
70 | "topbar",
71 | "unocss",
72 | "Vitesse",
73 | "vitesse-webext",
74 | "vueuse",
75 | "WATCHLATER",
76 | "webext",
77 | "webextension"
78 | ],
79 | "typescript.tsdk": "node_modules/typescript/lib",
80 | "vite.autoStart": false,
81 | "files.associations": {
82 | "*.css": "postcss"
83 | },
84 | "i18n-ally.localesPaths": [
85 | "src/_locales"
86 | ],
87 | "i18n-ally.keystyle": "nested"
88 | }
89 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Hakadao(hakadao2000@gmail.com)
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 |
--------------------------------------------------------------------------------
/assets/anime-timetable-icons.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keleus/BewlyCat/dc5f436803f9220d04270d3694ab8c0023b706f1/assets/anime-timetable-icons.png
--------------------------------------------------------------------------------
/assets/broken-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keleus/BewlyCat/dc5f436803f9220d04270d3694ab8c0023b706f1/assets/broken-image.png
--------------------------------------------------------------------------------
/assets/empty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keleus/BewlyCat/dc5f436803f9220d04270d3694ab8c0023b706f1/assets/empty.png
--------------------------------------------------------------------------------
/assets/icon-512-flat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keleus/BewlyCat/dc5f436803f9220d04270d3694ab8c0023b706f1/assets/icon-512-flat.png
--------------------------------------------------------------------------------
/assets/icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keleus/BewlyCat/dc5f436803f9220d04270d3694ab8c0023b706f1/assets/icon-512.png
--------------------------------------------------------------------------------
/assets/loading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keleus/BewlyCat/dc5f436803f9220d04270d3694ab8c0023b706f1/assets/loading.gif
--------------------------------------------------------------------------------
/assets/logo-cat.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/rules.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 1,
4 | "priority": 1,
5 | "action": {
6 | "type": "modifyHeaders",
7 | "requestHeaders": [
8 | {
9 | "header": "origin",
10 | "operation": "set",
11 | "value": "https://www.bilibili.com"
12 | },
13 | {
14 | "header": "referer",
15 | "operation": "set",
16 | "value": "https://www.bilibili.com"
17 | }
18 | ]
19 | },
20 | "condition": {
21 | "domainType": "thirdParty",
22 | "urlFilter": "||api.bilibili.com",
23 | "resourceTypes": ["xmlhttprequest"],
24 | "requestMethods": ["post"]
25 | }
26 | },
27 | {
28 | "id": 2,
29 | "priority": 1,
30 | "action": {
31 | "type": "modifyHeaders",
32 | "requestHeaders": [
33 | {
34 | "header": "origin",
35 | "operation": "set",
36 | "value": "https://www.bilibili.com"
37 | },
38 | {
39 | "header": "referer",
40 | "operation": "set",
41 | "value": "https://www.bilibili.com"
42 | }
43 | ]
44 | },
45 | "condition": {
46 | "domainType": "thirdParty",
47 | "urlFilter": "||passport.bilibili.com",
48 | "resourceTypes": ["xmlhttprequest"],
49 | "requestMethods": ["post"]
50 | }
51 | }
52 | ]
53 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import antfu from '@antfu/eslint-config'
2 | import simpleImportSort from 'eslint-plugin-simple-import-sort'
3 |
4 | export default antfu(
5 | {
6 | formatters: {
7 | css: 'prettier',
8 | prettierOptions: {
9 | printWidth: 120,
10 | singleQuote: false,
11 | },
12 | },
13 | rules: {
14 | 'vue/max-attributes-per-line': [
15 | 'error',
16 | {
17 | singleline: {
18 | max: 5,
19 | },
20 | multiline: {
21 | max: 5,
22 | },
23 | },
24 | ],
25 | 'no-alert': 'off',
26 | 'style/quote-props': 'off',
27 | },
28 | eslint: {
29 | ignorePatterns: [
30 | 'dist',
31 | 'node_modules',
32 | 'public',
33 | 'extension',
34 | 'extension-firefox',
35 | ],
36 | },
37 | },
38 | {
39 | plugins: {
40 | 'simple-import-sort': simpleImportSort,
41 | },
42 | rules: {
43 | 'import/order': 'off',
44 | 'sort-imports': 'off',
45 | 'simple-import-sort/imports': 'error',
46 | 'simple-import-sort/exports': 'error',
47 | 'no-console': ['error', { allow: ['debug', 'warn', 'error', 'log'] }],
48 | },
49 | },
50 | )
51 |
--------------------------------------------------------------------------------
/knip.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/knip@5/schema.json",
3 | "entry": [
4 | "src/contentScripts/index.ts",
5 | "src/background/index.ts",
6 | "src/options/main.ts",
7 | "src/popup/main.ts",
8 | "src/components/**",
9 | "scripts/*.ts",
10 | "*.config.*"
11 | ],
12 | "ignore": ["src/components/Settings/**", "src/inject/**"],
13 | "ignoreDependencies": [
14 | "@iconify/json",
15 | "uno.css",
16 | "lint-staged"
17 | ],
18 | "ignoreBinaries": ["gh", "xcrun"],
19 | "rules": {
20 | "types": "off",
21 | "enumMembers": "off",
22 | "exports": "off"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | onlyBuiltDependencies:
2 | - dtrace-provider
3 | - esbuild
4 | - simple-git-hooks
5 | - spawn-sync
6 | - vue-demi
7 |
--------------------------------------------------------------------------------
/scripts/manifest.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs-extra'
2 |
3 | import { getManifest } from '../src/manifest'
4 | import { isFirefox, isSafari, log, r } from './utils'
5 |
6 | export async function writeManifest() {
7 | await fs.writeJSON(r(
8 | isFirefox
9 | ? 'extension-firefox/manifest.json'
10 | : isSafari ? 'extension-safari/manifest.json' : 'extension/manifest.json',
11 | ), await getManifest(), { spaces: 2 })
12 | log('PRE', 'write manifest.json')
13 | }
14 |
15 | writeManifest()
16 |
--------------------------------------------------------------------------------
/scripts/prepare.ts:
--------------------------------------------------------------------------------
1 | // generate stub index.html files for dev entry
2 | import { execSync } from 'node:child_process'
3 |
4 | import chokidar from 'chokidar'
5 | import fs from 'fs-extra'
6 |
7 | import { isDev, isFirefox, isSafari, log, r } from './utils'
8 |
9 | /**
10 | * Stub index.html to use Vite in development
11 | */
12 | async function stubIndexHtml() {
13 | const views = [
14 | 'options',
15 | 'popup',
16 | ]
17 |
18 | for (const view of views) {
19 | await fs.ensureDir(r(
20 | isFirefox
21 | ? `extension-firefox/dist/${view}`
22 | : isSafari ? `extension-safari/dist/${view}` : `extension/dist/${view}`,
23 | ))
24 | let data = await fs.readFile(r(`src/${view}/index.html`), 'utf-8')
25 | data = data
26 | .replace('"./main.ts"', `"/${view}/main.ts.js"`)
27 | .replace('
', 'Vite server did not start
')
28 | await fs.writeFile(r(
29 | isFirefox
30 | ? `extension-firefox/dist/${view}/index.html`
31 | : isSafari ? `extension-safari/dist/${view}/index.html` : `extension/dist/${view}/index.html`,
32 | ), data, 'utf-8')
33 | log('PRE', `stub ${view}`)
34 | }
35 | }
36 |
37 | function writeManifest() {
38 | execSync('npx esno ./scripts/manifest.ts', { stdio: 'inherit' })
39 | }
40 |
41 | fs.ensureDirSync(r(isFirefox ? 'extension-firefox' : isSafari ? 'extension-safari' : 'extension'))
42 | fs.copySync(r('assets'), r(isFirefox ? 'extension-firefox/assets' : isSafari ? 'extension-safari/assets' : 'extension/assets'))
43 | writeManifest()
44 |
45 | if (isDev) {
46 | stubIndexHtml()
47 | chokidar.watch(r('src/**/*.html'))
48 | .on('change', () => {
49 | stubIndexHtml()
50 | })
51 | chokidar.watch([r('src/manifest.ts'), r('package.json')])
52 | .on('change', () => {
53 | writeManifest()
54 | })
55 | }
56 |
--------------------------------------------------------------------------------
/scripts/utils.ts:
--------------------------------------------------------------------------------
1 | import { dirname, resolve } from 'node:path'
2 | import process from 'node:process'
3 | import { fileURLToPath } from 'node:url'
4 |
5 | import { bgCyan, black } from 'kolorist'
6 |
7 | export const port = Number.parseInt(process.env.PORT || '') || 3303
8 | export const r = (...args: string[]) => resolve(dirname(fileURLToPath(import.meta.url)), '..', ...args)
9 | export const isDev = process.env.NODE_ENV !== 'production'
10 | export const isWin = process.platform === 'win32'
11 | export const isFirefox = process.env.FIREFOX === 'true'
12 | export const isSafari = process.env.SAFARI === 'true'
13 |
14 | export function log(name: string, message: string) {
15 | console.log(black(bgCyan(` ${name} `)), message)
16 | }
17 |
--------------------------------------------------------------------------------
/shim.d.ts:
--------------------------------------------------------------------------------
1 | import type { AttributifyAttributes } from '@unocss/preset-attributify'
2 | import type { ProtocolWithReturn } from 'webext-bridge'
3 |
4 | declare module 'webext-bridge' {
5 | export interface ProtocolMap {
6 | // define message protocol types
7 | // see https://github.com/antfu/webext-bridge#type-safe-protocols
8 | 'tab-prev': { title: string | undefined }
9 | 'get-current-tab': ProtocolWithReturn<{ tabId: number }, { title?: string }>
10 | }
11 | }
12 |
13 | declare module '@vue/runtime-dom' {
14 | interface HTMLAttributes extends AttributifyAttributes {}
15 | }
16 |
--------------------------------------------------------------------------------
/src/auto-imports.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /* prettier-ignore */
3 | // @ts-nocheck
4 | // noinspection JSUnusedGlobalSymbols
5 | // Generated by unplugin-auto-import
6 | export {}
7 | declare global {
8 | const EffectScope: typeof import('vue')['EffectScope']
9 | const browser: typeof import('webextension-polyfill')
10 | const computed: typeof import('vue')['computed']
11 | const createApp: typeof import('vue')['createApp']
12 | const customRef: typeof import('vue')['customRef']
13 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
14 | const defineComponent: typeof import('vue')['defineComponent']
15 | const effectScope: typeof import('vue')['effectScope']
16 | const getCurrentInstance: typeof import('vue')['getCurrentInstance']
17 | const getCurrentScope: typeof import('vue')['getCurrentScope']
18 | const h: typeof import('vue')['h']
19 | const inject: typeof import('vue')['inject']
20 | const isProxy: typeof import('vue')['isProxy']
21 | const isReactive: typeof import('vue')['isReactive']
22 | const isReadonly: typeof import('vue')['isReadonly']
23 | const isRef: typeof import('vue')['isRef']
24 | const markRaw: typeof import('vue')['markRaw']
25 | const nextTick: typeof import('vue')['nextTick']
26 | const onActivated: typeof import('vue')['onActivated']
27 | const onBeforeMount: typeof import('vue')['onBeforeMount']
28 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
29 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
30 | const onDeactivated: typeof import('vue')['onDeactivated']
31 | const onErrorCaptured: typeof import('vue')['onErrorCaptured']
32 | const onMounted: typeof import('vue')['onMounted']
33 | const onRenderTracked: typeof import('vue')['onRenderTracked']
34 | const onRenderTriggered: typeof import('vue')['onRenderTriggered']
35 | const onScopeDispose: typeof import('vue')['onScopeDispose']
36 | const onServerPrefetch: typeof import('vue')['onServerPrefetch']
37 | const onUnmounted: typeof import('vue')['onUnmounted']
38 | const onUpdated: typeof import('vue')['onUpdated']
39 | const provide: typeof import('vue')['provide']
40 | const reactive: typeof import('vue')['reactive']
41 | const readonly: typeof import('vue')['readonly']
42 | const ref: typeof import('vue')['ref']
43 | const resolveComponent: typeof import('vue')['resolveComponent']
44 | const shallowReactive: typeof import('vue')['shallowReactive']
45 | const shallowReadonly: typeof import('vue')['shallowReadonly']
46 | const shallowRef: typeof import('vue')['shallowRef']
47 | const toRaw: typeof import('vue')['toRaw']
48 | const toRef: typeof import('vue')['toRef']
49 | const toRefs: typeof import('vue')['toRefs']
50 | const toValue: typeof import('vue')['toValue']
51 | const triggerRef: typeof import('vue')['triggerRef']
52 | const unref: typeof import('vue')['unref']
53 | const useAttrs: typeof import('vue')['useAttrs']
54 | const useCssModule: typeof import('vue')['useCssModule']
55 | const useCssVars: typeof import('vue')['useCssVars']
56 | const useSlots: typeof import('vue')['useSlots']
57 | const watch: typeof import('vue')['watch']
58 | const watchEffect: typeof import('vue')['watchEffect']
59 | const watchPostEffect: typeof import('vue')['watchPostEffect']
60 | const watchSyncEffect: typeof import('vue')['watchSyncEffect']
61 | }
62 | // for type re-export
63 | declare global {
64 | // @ts-ignore
65 | export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
66 | import('vue')
67 | }
68 |
--------------------------------------------------------------------------------
/src/background/index.ts:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill'
2 |
3 | import { setupApiMsgLstnrs } from './messageListeners/api'
4 | import { setupTabMsgLstnrs } from './messageListeners/tabs'
5 |
6 | // Initialize extension and set up message handlers
7 | browser.runtime.onInstalled.addListener(async () => {
8 | console.log('Extension installed')
9 | })
10 |
11 | function isExtensionUri(url: string) {
12 | return new URL(url).origin === new URL(browser.runtime.getURL('')).origin
13 | }
14 |
15 | // Firefox specific header handling
16 | // eslint-disable-next-line node/prefer-global/process
17 | if (process.env.FIREFOX) {
18 | browser.webRequest.onBeforeSendHeaders.addListener(
19 | async (details: any) => {
20 | const requestHeaders: browser.WebRequest.HttpHeaders = []
21 | if (details.documentUrl) {
22 | const url = new URL(details.documentUrl)
23 | const extensionUri = isExtensionUri(details.documentUrl)
24 | details.requestHeaders = details.requestHeaders || []
25 | for (let i = 0; i < details.requestHeaders.length; i++) {
26 | if (details.requestHeaders[i].name.toLowerCase() === 'origin' || details.requestHeaders[i].name.toLowerCase() === 'referer')
27 | requestHeaders.push({ name: details.requestHeaders[i].name, value: extensionUri ? 'https://www.bilibili.com' : url.origin })
28 | else
29 | requestHeaders.push(details.requestHeaders[i])
30 |
31 | if (details.requestHeaders[i].name === 'firefox-multi-account-cookie') {
32 | requestHeaders.push({ name: 'cookie', value: details.requestHeaders[i].value })
33 | }
34 | }
35 |
36 | return { ...details, requestHeaders }
37 | }
38 | },
39 | { urls: [''] },
40 | ['blocking', 'requestHeaders'],
41 | )
42 | }
43 |
44 | // Setup all message listeners
45 | // 只设置一次消息监听器
46 | setupApiMsgLstnrs()
47 | setupTabMsgLstnrs()
48 |
--------------------------------------------------------------------------------
/src/background/messageListeners/api/anime.ts:
--------------------------------------------------------------------------------
1 | import type { APIMAP } from '../../utils'
2 | import { AHS } from '../../utils'
3 |
4 | const API_ANIME = {
5 | // https://github.com/SocialSisterYi/bilibili-API-collect/blob/36e250090800793b41b223b55eefdcbb9391b53e/user/space.md#%E6%9F%A5%E8%AF%A2%E7%94%A8%E6%88%B7%E8%BF%BD%E7%95%AA%E8%BF%BD%E5%89%A7%E6%98%8E%E7%BB%86
6 | getPopularAnimeList: {
7 | url: 'https://api.bilibili.com/pgc/web/rank/list',
8 | _fetch: {
9 | method: 'get',
10 | },
11 | params: {
12 | season_type: 1,
13 | day: 3,
14 | },
15 | afterHandle: AHS.J_D,
16 | },
17 | // https://github.com/SocialSisterYi/bilibili-API-collect/blob/36e250090800793b41b223b55eefdcbb9391b53e/user/space.md#%E6%9F%A5%E8%AF%A2%E7%94%A8%E6%88%B7%E8%BF%BD%E7%95%AA%E8%BF%BD%E5%89%A7%E6%98%8E%E7%BB%86
18 | getAnimeWatchList: {
19 | url: 'https://api.bilibili.com/x/space/bangumi/follow/list',
20 | _fetch: {
21 | method: 'get',
22 | },
23 | params: {
24 | pn: 1,
25 | ps: 15,
26 | type: 1,
27 | follow_status: 0, // 0: 全部, 1: 想看, 2: 在看, 3: 看过
28 | vmid: '',
29 | },
30 | afterHandle: AHS.J_D,
31 | },
32 | getRecommendAnimeList: {
33 | url: 'https://api.bilibili.com/pgc/page/web/v3/feed',
34 | _fetch: {
35 | method: 'get',
36 | },
37 | params: {
38 | coursor: 0,
39 | name: 'anime',
40 | },
41 | afterHandle: AHS.J_D,
42 | },
43 | // https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/bangumi/timeline.md#%E7%95%AA%E5%89%A7%E6%88%96%E5%BD%B1%E8%A7%86%E6%97%B6%E9%97%B4%E7%BA%BF
44 | getAnimeTimeTable: {
45 | url: 'https://api.bilibili.com/pgc/web/timeline',
46 | _fetch: {
47 | method: 'get',
48 | },
49 | params: {
50 | types: 1,
51 | before: 6,
52 | after: 6,
53 | },
54 | afterHandle: AHS.J_D,
55 | },
56 | getAnimeDetail: {
57 | url: 'https://api.bilibili.com/pgc/view/web/season',
58 | _fetch: {
59 | method: 'get',
60 | },
61 | params: {
62 | // ep_id: '234406',
63 | },
64 | afterHandle: AHS.J_D,
65 | },
66 | } satisfies APIMAP
67 |
68 | export default API_ANIME
69 |
--------------------------------------------------------------------------------
/src/background/messageListeners/api/auth.ts:
--------------------------------------------------------------------------------
1 | // 由于 sendResponse 复杂, 所以使用自定义的函数
2 | import type { APIMAP } from '../../utils'
3 | import { AHS } from '../../utils'
4 |
5 | const API_AUTH = {
6 | // biliJct 似乎没有使用
7 | logout: {
8 | url: 'https://passport.bilibili.com/login/exit/v2',
9 | _fetch: {
10 | method: 'post',
11 | headers: {
12 | 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
13 | },
14 | body: {
15 | biliCSRF: '',
16 | // biliJct: '',
17 | },
18 | },
19 | params: {
20 | biliCSRF: '',
21 | },
22 | afterHandle: AHS.J_S,
23 | },
24 | getLoginQRCode: {
25 | url: 'https://passport.bilibili.com/x/passport-tv-login/qrcode/auth_code',
26 | _fetch: {
27 | method: 'post',
28 | headers: {
29 | 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
30 | },
31 | },
32 | params: {
33 | appkey: '4409e2ce8ffd12b8',
34 | local_id: '0',
35 | ts: '0',
36 | sign: 'e134154ed6add881d28fbdf68653cd9c',
37 | },
38 | afterHandle: AHS.J_S,
39 | },
40 | qrCodeLogin: {
41 | url: 'https://passport.bilibili.com/x/passport-tv-login/qrcode/auth_code',
42 | _fetch: {
43 | method: 'post',
44 | headers: {
45 | 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
46 | },
47 | },
48 | params: {
49 | appkey: '4409e2ce8ffd12b8',
50 | auth_code: '',
51 | local_id: '0',
52 | ts: '0',
53 | sign: 'e134154ed6add881d28fbdf68653cd9c',
54 | },
55 | afterHandle: AHS.J_S,
56 | },
57 | } satisfies APIMAP
58 |
59 | export default API_AUTH
60 |
--------------------------------------------------------------------------------
/src/background/messageListeners/api/favorite.ts:
--------------------------------------------------------------------------------
1 | import type { APIMAP } from '../../utils'
2 | import { AHS } from '../../utils'
3 |
4 | const API_FAVORITE = {
5 | // https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/fav/info.md#%E8%8E%B7%E5%8F%96%E6%8C%87%E5%AE%9A%E7%94%A8%E6%88%B7%E5%88%9B%E5%BB%BA%E7%9A%84%E6%89%80%E6%9C%89%E6%94%B6%E8%97%8F%E5%A4%B9%E4%BF%A1%E6%81%AF
6 | getFavoriteCategories: {
7 | url: 'https://api.bilibili.com/x/v3/fav/folder/created/list-all',
8 | _fetch: {
9 | method: 'get',
10 | },
11 | params: {
12 | up_mid: '',
13 | },
14 | afterHandle: AHS.J_D,
15 | },
16 | // https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/fav/list.md#%E8%8E%B7%E5%8F%96%E6%94%B6%E8%97%8F%E5%A4%B9%E5%86%85%E5%AE%B9%E6%98%8E%E7%BB%86%E5%88%97%E8%A1%A8
17 | getFavoriteResources: {
18 | url: 'https://api.bilibili.com/x/v3/fav/resource/list',
19 | _fetch: {
20 | method: 'get',
21 | },
22 | params: {
23 | media_id: -1,
24 | pn: 1,
25 | ps: 20,
26 | keyword: '',
27 | order: 'mtime',
28 | type: 0,
29 | tid: 0,
30 | platform: 'web',
31 | },
32 | afterHandle: AHS.J_D,
33 | },
34 | // https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/fav/action.md#%E6%89%B9%E9%87%8F%E5%88%A0%E9%99%A4%E5%86%85%E5%AE%B9
35 | patchDelFavoriteResources: {
36 | url: 'https://api.bilibili.com/x/v3/fav/resource/batch-del',
37 | _fetch: {
38 | method: 'post',
39 | },
40 | params: {
41 | resources: '',
42 | media_id: 0,
43 | csrf: '',
44 | },
45 | afterHandle: AHS.J_D,
46 | },
47 | } satisfies APIMAP
48 |
49 | export default API_FAVORITE
50 |
--------------------------------------------------------------------------------
/src/background/messageListeners/api/history.ts:
--------------------------------------------------------------------------------
1 | import type { APIMAP } from '../../utils'
2 | import { AHS } from '../../utils'
3 |
4 | const API_HISTORY = {
5 | // https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/history&toview/history.md#%E8%8E%B7%E5%8F%96%E5%8E%86%E5%8F%B2%E8%AE%B0%E5%BD%95%E5%88%97%E8%A1%A8_web%E7%AB%AF
6 | getHistoryList: {
7 | url: 'https://api.bilibili.com/x/web-interface/history/cursor',
8 | _fetch: {
9 | method: 'get',
10 | },
11 | params: {
12 | ps: 20,
13 | type: '',
14 | view_at: 0,
15 | },
16 | afterHandle: AHS.J_D,
17 | },
18 | searchHistoryList: {
19 | url: 'https://api.bilibili.com/x/web-interface/history/search',
20 | _fetch: {
21 | method: 'get',
22 | },
23 | params: {
24 | pn: 1,
25 | keyword: '',
26 | business: 'all',
27 | },
28 | afterHandle: AHS.J_D,
29 | },
30 | // https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/history&toview/history.md#%E5%88%A0%E9%99%A4%E5%8E%86%E5%8F%B2%E8%AE%B0%E5%BD%95
31 | deleteHistoryItem: {
32 | url: 'https://api.bilibili.com/x/v2/history/delete',
33 | _fetch: {
34 | method: 'post',
35 | headers: {
36 | 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
37 | },
38 | body: {
39 | kid: '',
40 | csrf: '',
41 | },
42 | },
43 | afterHandle: AHS.J_D,
44 | },
45 | // https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/history&toview/history.md#%E6%B8%85%E7%A9%BA%E5%8E%86%E5%8F%B2%E8%AE%B0%E5%BD%95
46 | clearAllHistory: {
47 | url: 'https://api.bilibili.com/x/v2/history/clear',
48 | _fetch: {
49 | method: 'post',
50 | headers: {
51 | 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
52 | },
53 | body: {
54 | csrf: '',
55 | },
56 | },
57 | afterHandle: AHS.J_D,
58 | },
59 | // https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/history&toview/history.md#%E6%9F%A5%E8%AF%A2%E5%8E%86%E5%8F%B2%E8%AE%B0%E5%BD%95%E5%81%9C%E7%94%A8%E7%8A%B6%E6%80%81
60 | getHistoryPauseStatus: {
61 | url: 'https://api.bilibili.com/x/v2/history/shadow',
62 | _fetch: {
63 | method: 'get',
64 | },
65 | params: {},
66 | afterHandle: AHS.J_D,
67 | },
68 | // https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/history&toview/history.md#%E5%81%9C%E7%94%A8%E5%8E%86%E5%8F%B2%E8%AE%B0%E5%BD%95
69 | setHistoryPauseStatus: {
70 | url: 'https://api.bilibili.com/x/v2/history/shadow/set',
71 | _fetch: {
72 | method: 'post',
73 | headers: {
74 | 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
75 | },
76 | body: {
77 | switch: false,
78 | csrf: '',
79 | },
80 | },
81 | afterHandle: AHS.J_D,
82 | },
83 | } satisfies APIMAP
84 |
85 | export default API_HISTORY
86 |
--------------------------------------------------------------------------------
/src/background/messageListeners/api/index.ts:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill'
2 |
3 | import { apiListenerFactory } from '../../utils'
4 | import API_ANIME from './anime'
5 | import API_AUTH from './auth'
6 | import API_FAVORITE from './favorite'
7 | import API_HISTORY from './history'
8 | import API_LIVE from './live'
9 | import API_MOMENT from './moment'
10 | import API_NOTIFICATION from './notification'
11 | import API_RANKING from './ranking'
12 | import API_SEARCH from './search'
13 | import API_USER from './user'
14 | import API_VIDEO from './video'
15 | import API_WATCHLATER from './watchLater'
16 |
17 | export const API_COLLECTION = {
18 | AUTH: API_AUTH,
19 | ANIME: API_ANIME,
20 | HISTORY: API_HISTORY,
21 | FAVORITE: API_FAVORITE,
22 | MOMENT: API_MOMENT,
23 | NOTIFICATION: API_NOTIFICATION,
24 | RANKING: API_RANKING,
25 | SEARCH: API_SEARCH,
26 | USER: API_USER,
27 | VIDEO: API_VIDEO,
28 | WATCHLATER: API_WATCHLATER,
29 | LIVE: API_LIVE,
30 |
31 | [Symbol.iterator]() {
32 | return Object.values(this).values()
33 | },
34 | }
35 |
36 | // Merge all API objects into one
37 | const FullAPI = Object.assign({}, ...API_COLLECTION)
38 | // Create a message listener for each API
39 | const handleMessage = apiListenerFactory(FullAPI)
40 |
41 | export function setupApiMsgLstnrs() {
42 | browser.runtime.onConnect.removeListener(handleConnect)
43 | browser.runtime.onConnect.addListener(handleConnect)
44 | }
45 |
46 | function handleConnect() {
47 | browser.runtime.onMessage.removeListener(handleMessage)
48 | browser.runtime.onMessage.addListener(handleMessage)
49 | }
50 |
--------------------------------------------------------------------------------
/src/background/messageListeners/api/live.ts:
--------------------------------------------------------------------------------
1 | import type { APIMAP } from '../../utils'
2 | import { AHS } from '../../utils'
3 |
4 | const API_LIVE = {
5 | // https://socialsisteryi.github.io/bilibili-API-collect/docs/live/follow_up_live.html#%E7%94%A8%E6%88%B7%E5%85%B3%E6%B3%A8%E7%9A%84%E6%89%80%E6%9C%89up%E7%9A%84%E7%9B%B4%E6%92%AD%E6%83%85%E5%86%B5
6 | getFollowingLiveList: {
7 | url: 'https://api.live.bilibili.com/xlive/web-ucenter/user/following',
8 | _fetch: {
9 | method: 'get',
10 | },
11 | params: {
12 | page: 1,
13 | page_size: 9,
14 | ignoreRecord: 1,
15 | hit_ab: true,
16 | },
17 | afterHandle: AHS.J_D,
18 | },
19 |
20 | } satisfies APIMAP
21 |
22 | export default API_LIVE
23 |
--------------------------------------------------------------------------------
/src/background/messageListeners/api/moment.ts:
--------------------------------------------------------------------------------
1 | import type { APIMAP } from '../../utils'
2 | import { AHS } from '../../utils'
3 |
4 | const API_MOMENT = {
5 | getTopBarNewMomentsCount: {
6 | url: 'https://api.bilibili.com/x/web-interface/dynamic/entrance',
7 | _fetch: {
8 | method: 'get',
9 | },
10 | params: {},
11 | afterHandle: AHS.J_D,
12 | },
13 | getTopBarMoments: {
14 | url: 'https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/nav',
15 | _fetch: {
16 | method: 'get',
17 | },
18 | params: {
19 | type: 'video',
20 | update_baseline: '',
21 | offset: '',
22 | },
23 | afterHandle: AHS.J_D,
24 | },
25 | getTopBarLiveMoments: {
26 | url: 'https://api.live.bilibili.com/xlive/web-ucenter/v1/xfetter/FeedList',
27 | _fetch: {
28 | method: 'get',
29 | },
30 | params: {
31 | page: 1,
32 | pagesize: 10,
33 | },
34 | afterHandle: AHS.J_D,
35 | },
36 | getMoments: {
37 | url: 'https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/all',
38 | _fetch: {
39 | method: 'get',
40 | },
41 | params: {
42 | type: 'all',
43 | offset: 0,
44 | update_baseline: '',
45 | },
46 | afterHandle: AHS.J_D,
47 | },
48 | getMomentsUpdate: {
49 | url: 'https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/all/update',
50 | _fetch: {
51 | method: 'get',
52 | },
53 | params: {
54 | type: 'video',
55 | offset: 0,
56 | update_baseline: '0',
57 | },
58 | afterHandle: AHS.J_D,
59 | },
60 | } satisfies APIMAP
61 |
62 | export default API_MOMENT
63 |
--------------------------------------------------------------------------------
/src/background/messageListeners/api/notification.ts:
--------------------------------------------------------------------------------
1 | import type { APIMAP } from '../../utils'
2 | import { AHS } from '../../utils'
3 |
4 | const API_NOTIFICATION = {
5 | getUnreadMsg: {
6 | url: 'https://api.bilibili.com/x/msgfeed/unread',
7 | _fetch: {
8 | method: 'get',
9 | },
10 | params: {
11 | build: 0,
12 | mobi_app: 'web',
13 | },
14 | afterHandle: AHS.J_D,
15 | },
16 | getUnreadDm: {
17 | url: 'https://api.vc.bilibili.com/session_svr/v1/session_svr/single_unread',
18 | _fetch: {
19 | method: 'get',
20 | },
21 | params: {
22 | build: 0,
23 | mobi_app: 'web',
24 | unread_type: 0,
25 | },
26 | afterHandle: AHS.J_D,
27 | },
28 | } satisfies APIMAP
29 |
30 | export default API_NOTIFICATION
31 |
--------------------------------------------------------------------------------
/src/background/messageListeners/api/ranking.ts:
--------------------------------------------------------------------------------
1 | import type { APIMAP } from '../../utils'
2 | import { AHS } from '../../utils'
3 |
4 | const API_RANKING = {
5 | // https://github.com/SocialSisterYi/bilibili-API-collect/blob/7873a79022a5606e2391d93b411a05576a0df111/docs/video_ranking/ranking.md#%E8%8E%B7%E5%8F%96%E5%88%86%E5%8C%BA%E8%A7%86%E9%A2%91%E6%8E%92%E8%A1%8C%E6%A6%9C%E5%88%97%E8%A1%A8
6 | getRankingVideos: {
7 | url: 'https://api.bilibili.com/x/web-interface/ranking/v2',
8 | _fetch: {
9 | method: 'get',
10 | },
11 | params: {
12 | rid: 0,
13 | type: 'all',
14 | },
15 | afterHandle: AHS.J_D,
16 | },
17 | getRankingPgc: {
18 | url: 'https://api.bilibili.com/pgc/season/rank/web/list',
19 | _fetch: {
20 | method: 'get',
21 | },
22 | params: {
23 | season_type: 1,
24 | day: 3,
25 | },
26 | afterHandle: AHS.J_D,
27 | },
28 | } satisfies APIMAP
29 |
30 | export default API_RANKING
31 |
--------------------------------------------------------------------------------
/src/background/messageListeners/api/search.ts:
--------------------------------------------------------------------------------
1 | import type { APIMAP } from '../../utils'
2 | import { AHS } from '../../utils'
3 |
4 | const API_SEARCH = {
5 | getSearchSuggestion: {
6 | url: 'https://s.search.bilibili.com/main/suggest',
7 | _fetch: {
8 | method: 'get',
9 | },
10 | params: {
11 | term: '',
12 | highlight: '',
13 | },
14 | afterHandle: AHS.J_D,
15 | },
16 | } satisfies APIMAP
17 |
18 | export default API_SEARCH
19 |
--------------------------------------------------------------------------------
/src/background/messageListeners/api/user.ts:
--------------------------------------------------------------------------------
1 | import type { APIMAP } from '../../utils'
2 | import { AHS } from '../../utils'
3 |
4 | const API_USER = {
5 | // https://github.com/SocialSisterYi/bilibili-API-collect/blob/e379d904c2753fa30e9083f59016f07e89d19467/docs/login/login_info.md#%E5%AF%BC%E8%88%AA%E6%A0%8F%E7%94%A8%E6%88%B7%E4%BF%A1%E6%81%AF
6 | getUserInfo: {
7 | url: 'https://api.bilibili.com/x/web-interface/nav',
8 | _fetch: {
9 | method: 'get',
10 | },
11 | afterHandle: AHS.J_D,
12 | },
13 | getUserStat: {
14 | url: 'https://api.bilibili.com/x/web-interface/nav/stat',
15 | _fetch: {
16 | method: 'get',
17 | },
18 | afterHandle: AHS.J_D,
19 | },
20 | // https://github.com/SocialSisterYi/bilibili-API-collect/blob/ed9ac01b6769430aa3f12ad02c2ed337a96924eb/docs/user/relation.md#操作用户关系
21 | relationModify: {
22 | url: 'https://api.bilibili.com/x/relation/modify',
23 | _fetch: {
24 | method: 'post',
25 | },
26 | params: {
27 | // access_key: '', // app only
28 | fid: '',
29 | act: 1,
30 | re_src: 11,
31 | },
32 | afterHandle: AHS.J_D,
33 | },
34 | getPrivilegeInfo: {
35 | url: 'https://api.bilibili.com/x/vip/privilege/my',
36 | _fetch: {
37 | method: 'get',
38 | },
39 | afterHandle: AHS.J_D,
40 | },
41 | } satisfies APIMAP
42 |
43 | export default API_USER
44 |
--------------------------------------------------------------------------------
/src/background/messageListeners/api/watchLater.ts:
--------------------------------------------------------------------------------
1 | import type { APIMAP } from '../../utils'
2 | import { AHS } from '../../utils'
3 |
4 | const API_WATCHLATER = {
5 | // https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/history&toview/toview.md#%E8%A7%86%E9%A2%91%E6%B7%BB%E5%8A%A0%E7%A8%8D%E5%90%8E%E5%86%8D%E7%9C%8B
6 | saveToWatchLater: {
7 | url: 'https://api.bilibili.com/x/v2/history/toview/add',
8 | _fetch: {
9 | method: 'post',
10 | headers: {
11 | 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
12 | },
13 | body: {
14 | aid: 0,
15 | csrf: '',
16 | },
17 | },
18 | afterHandle: AHS.J_D,
19 | },
20 | // https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/history&toview/toview.md#%E5%88%A0%E9%99%A4%E7%A8%8D%E5%90%8E%E5%86%8D%E7%9C%8B%E8%A7%86%E9%A2%91
21 | removeFromWatchLater: {
22 | url: 'https://api.bilibili.com/x/v2/history/toview/del',
23 | _fetch: {
24 | method: 'post',
25 | headers: {
26 | 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
27 | },
28 | body: {
29 | viewed: false,
30 | csrf: '',
31 | },
32 | },
33 | params: {
34 | aid: 0,
35 | },
36 | afterHandle: AHS.J_D,
37 | },
38 | // https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/history&toview/toview.md#%E8%8E%B7%E5%8F%96%E7%A8%8D%E5%90%8E%E5%86%8D%E7%9C%8B%E8%A7%86%E9%A2%91%E5%88%97%E8%A1%A8
39 | getAllWatchLaterList: {
40 | url: 'https://api.bilibili.com/x/v2/history/toview',
41 | _fetch: {
42 | method: 'get',
43 | },
44 | afterHandle: AHS.J_D,
45 | },
46 | // https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/history&toview/toview.md#%E6%B8%85%E7%A9%BA%E7%A8%8D%E5%90%8E%E5%86%8D%E7%9C%8B%E8%A7%86%E9%A2%91%E5%88%97%E8%A1%A8
47 | clearAllWatchLater: {
48 | url: 'https://api.bilibili.com/x/v2/history/toview/clear',
49 | _fetch: {
50 | method: 'post',
51 | headers: {
52 | 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
53 | },
54 | body: {
55 | csrf: '',
56 | },
57 | },
58 | afterHandle: AHS.J_D,
59 | },
60 | } satisfies APIMAP
61 |
62 | export default API_WATCHLATER
63 |
--------------------------------------------------------------------------------
/src/background/messageListeners/tabs.ts:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill'
2 |
3 | interface Message {
4 | contentScriptQuery: string
5 | [key: string]: any
6 | }
7 |
8 | export enum TABS_MESSAGE {
9 | OPEN_LINK_IN_BACKGROUND = 'openLinkInBackground',
10 | }
11 |
12 | function handleMessage(message: Message) {
13 | if (message.contentScriptQuery === TABS_MESSAGE.OPEN_LINK_IN_BACKGROUND) {
14 | // 处理以 // 开头的 URL
15 | const url = message.url.startsWith('//') ? `https:${message.url}` : message.url
16 | return browser.tabs.create({ url, active: false })
17 | }
18 | }
19 |
20 | export function setupTabMsgLstnrs() {
21 | browser.runtime.onMessage.removeListener(handleMessage)
22 | browser.runtime.onMessage.addListener(handleMessage)
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/ALink.vue:
--------------------------------------------------------------------------------
1 |
91 |
92 |
93 |
100 |
101 |
102 |
103 |
--------------------------------------------------------------------------------
/src/components/BangumiCard/BangumiCardSkeleton.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
15 |
20 |
21 |
26 |
27 |
31 | 0.0
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/src/components/CodeEditor.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
15 |
16 |
17 |
22 |
--------------------------------------------------------------------------------
/src/components/Dock/types.ts:
--------------------------------------------------------------------------------
1 | export interface HoveringDockItem {
2 | themeMode: boolean
3 | settings: boolean
4 | }
5 |
--------------------------------------------------------------------------------
/src/components/Empty.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
![]()
14 |
{{ props.description }}
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/components/HorizontalScrollView.vue:
--------------------------------------------------------------------------------
1 |
42 |
43 |
44 |
45 |
67 |
68 |
78 |
79 |
80 |
81 |
82 |
83 |
89 |
--------------------------------------------------------------------------------
/src/components/Input.vue:
--------------------------------------------------------------------------------
1 |
40 |
41 |
42 |
49 |
54 |
55 |
67 |
68 |
73 |
74 |
75 |
76 |
82 |
--------------------------------------------------------------------------------
/src/components/List/List.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
30 |
--------------------------------------------------------------------------------
/src/components/List/ListItem.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
14 |
15 |
16 |
17 |
18 |
29 |
--------------------------------------------------------------------------------
/src/components/Loading.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
16 |
![loading]()
23 | {{ $t('common.loading') }}
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/components/OverlayScrollbarsComponent.ts:
--------------------------------------------------------------------------------
1 | import 'overlayscrollbars/overlayscrollbars.css'
2 |
3 | export default defineAsyncComponent(async () => {
4 | const { OverlayScrollbarsComponent } = await import('overlayscrollbars-vue')
5 | return OverlayScrollbarsComponent
6 | })
7 |
--------------------------------------------------------------------------------
/src/components/Picture.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
13 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/components/Progress.vue:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/components/README.md:
--------------------------------------------------------------------------------
1 | # Icons
2 |
3 | You can use icons from almost any icon set by the power of [Iconify](https://iconify.design/).
4 |
5 | It will only bundle the icons you use. Check out [vite-plugin-icons](https://github.com/antfu/vite-plugin-icons) for more details.
6 |
--------------------------------------------------------------------------------
/src/components/Radio.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
23 |
24 |
25 |
66 |
--------------------------------------------------------------------------------
/src/components/Select.vue:
--------------------------------------------------------------------------------
1 |
49 |
50 |
51 |
56 |
86 |
87 |
109 |
110 |
111 |
112 |
113 |
115 |
--------------------------------------------------------------------------------
/src/components/Settings/BIlibiliSettings/BilibiliSettings.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 | WIP...
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/components/Settings/BewlyPages/BewlyPages.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | -
43 |
44 |
45 |
{{ page.title }}
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/src/components/Settings/Compatibility/Compatibility.vue:
--------------------------------------------------------------------------------
1 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
55 |
56 |
57 |
58 |
59 |
60 |
63 |
--------------------------------------------------------------------------------
/src/components/Settings/components/SettingsItem.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | {{ title }}
15 |
16 |
17 |
18 |
22 |
23 | {{ desc }}
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
48 |
--------------------------------------------------------------------------------
/src/components/Settings/components/SettingsItemGroup.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 | {{ title }}
12 |
13 |
14 | {{ desc }}
15 |
16 |
17 |
23 |
24 |
25 |
26 |
27 |
28 |
33 |
--------------------------------------------------------------------------------
/src/components/Settings/types.ts:
--------------------------------------------------------------------------------
1 | export enum MenuType {
2 | General = 'General',
3 | DesktopAndDock = 'DesktopAndDock',
4 | Appearance = 'Appearance',
5 | BewlyPages = 'BewlyPages',
6 | Shortcuts = 'Shortcuts',
7 | Compatibility = 'Compatibility',
8 | VolumeBalance = 'VolumeBalance',
9 | BilibiliSettings = 'BilibiliSettings',
10 | About = 'About',
11 | }
12 |
13 | export enum BewlyPage {
14 | Home = 'Home',
15 | Search = 'Search',
16 | }
17 |
18 | export interface MenuItem {
19 | value: MenuType
20 | icon: string
21 | iconActivated: string
22 | title: string
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/SideBar/types.ts:
--------------------------------------------------------------------------------
1 | export interface HoveringDockItem {
2 | themeMode: boolean
3 | settings: boolean
4 | }
5 |
--------------------------------------------------------------------------------
/src/components/Slider.vue:
--------------------------------------------------------------------------------
1 |
38 |
39 |
40 |
49 |
50 |
51 |
80 |
--------------------------------------------------------------------------------
/src/components/Tooltip.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
20 |
25 | {{ content }}
26 |
27 |
28 |
29 |
30 |
31 |
80 |
--------------------------------------------------------------------------------
/src/components/TopBar/BewlyOrBiliTopBarSwitcher.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
20 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/components/TopBar/components/BewlyOrBiliPageSwitcher.vue:
--------------------------------------------------------------------------------
1 |
52 |
53 |
54 |
62 |
79 |
80 |
81 |
82 |
103 |
--------------------------------------------------------------------------------
/src/components/TopBar/components/TopBarHeader.vue:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
27 |
28 |
37 |
38 |
51 |
52 |
53 |
54 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/src/components/TopBar/components/TopBarSearch.vue:
--------------------------------------------------------------------------------
1 |
24 |
25 |
26 |
27 |
28 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/src/components/TopBar/components/pops/MorePop.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
32 |
46 |
47 | {{ item.name }}
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/src/components/TopBar/components/pops/UploadPop.vue:
--------------------------------------------------------------------------------
1 |
36 |
37 |
38 |
68 |
69 |
70 |
73 |
--------------------------------------------------------------------------------
/src/components/TopBar/composables/useTopBarInteraction.ts:
--------------------------------------------------------------------------------
1 | import { reactive, ref } from 'vue'
2 |
3 | import { useDelayedHover } from '~/composables/useDelayedHover'
4 | import { settings } from '~/logic'
5 | import { useTopBarStore } from '~/stores/topBarStore'
6 | import { createTransformer } from '~/utils/transformer'
7 |
8 | export function useTopBarInteraction() {
9 | const topBarStore = useTopBarStore()
10 | const { closeAllPopups } = topBarStore
11 | const topBarItemElements = reactive({})
12 | const topBarTransformers = reactive({})
13 |
14 | const isMouseOverPopup = reactive>({})
15 |
16 | // 当前点击的顶栏项
17 | const currentClickedTopBarItem = ref(null)
18 |
19 | // 设置顶栏项悬停事件
20 | function setupTopBarItemHoverEvent(key: string) {
21 | const element = useDelayedHover({
22 | enterDelay: 320,
23 | leaveDelay: 320,
24 | beforeEnter: () => closeAllPopups(key),
25 | enter: () => {
26 | topBarStore.popupVisible[key] = true
27 | },
28 | leave: () => {
29 | // 只有当鼠标不在弹窗上时才隐藏
30 | setTimeout(() => {
31 | if (!isMouseOverPopup[key]) {
32 | topBarStore.popupVisible[key] = false
33 | }
34 | }, 200)
35 | },
36 | })
37 |
38 | topBarItemElements[key] = element
39 | return element
40 | }
41 |
42 | // 设置顶栏项变换器
43 | function setupTopBarItemTransformer(key: string) {
44 | const transformer = createTransformer(topBarItemElements[key], {
45 | x: '0px',
46 | y: '50px',
47 | centerTarget: {
48 | x: true,
49 | },
50 | })
51 |
52 | topBarTransformers[key] = transformer
53 | return transformer
54 | }
55 |
56 | // 处理顶栏项点击
57 | function handleClickTopBarItem(event: MouseEvent, key: string) {
58 | if (settings.value.touchScreenOptimization) {
59 | event.preventDefault()
60 | closeAllPopups(key)
61 | topBarStore.popupVisible[key] = !topBarStore.popupVisible[key]
62 | currentClickedTopBarItem.value = key
63 | }
64 | }
65 |
66 | return {
67 | currentClickedTopBarItem,
68 | setupTopBarItemHoverEvent,
69 | setupTopBarItemTransformer,
70 | handleClickTopBarItem,
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/components/TopBar/constants/urls.ts:
--------------------------------------------------------------------------------
1 | // 搜索页面
2 | export const SEARCH_PAGE_URL = /https?:\/\/search.bilibili.com\/.*$/
3 |
4 | // 视频相关页面
5 | export const VIDEO_PAGE_URL = /https?:\/\/(?:www.)?bilibili.com\/video.*/
6 | export const VIDEO_LIST_URL = /https?:\/\/(?:www.)?bilibili.com\/(?:video|list)\/.*/
7 | export const BANGUMI_PLAY_URL = /https?:\/\/(?:www.)?bilibili.com\/bangumi\/play\/.*/
8 |
9 | // 频道页面
10 | export const CHANNEL_PAGE_URL = /https?:\/\/(?:www.)?bilibili.com\/(?:c|v|anime|guochuang|tv|movie|variety|mooc).*/
11 |
12 | // 用户相关页面
13 | export const SPACE_URL = /https?:\/\/space.bilibili\.com\..*/
14 | export const ACCOUNT_URL = /https?:\/\/account\.bilibili\.com\/big.*$/
15 |
16 | // 动态页面
17 | export const MOMENTS_URL = /https?:\/\/t.bilibili.com.*/
18 |
19 | // 阅读页面
20 | export const READ_HOME_URL = /https?:\/\/(?:www.)?bilibili.com\/read\/home.*/
21 | export const READ_PREVIEW_URL = /https?:\/\/(?:www\.)?bilibili\.com\/read\/(?:preview|pcpreview).*/
22 |
23 | // 创作中心
24 | export const CREATOR_PLATFORM_URL = /https?:\/\/member.bilibili.com\/platform.*/
25 |
26 | // 消息页面
27 | export const MESSAGE_URL = /https?:\/\/message.bilibili.com.*$/
28 |
--------------------------------------------------------------------------------
/src/components/TopBar/notify.ts:
--------------------------------------------------------------------------------
1 | // https://github.dev/the1812/Bilibili-Evolved/blob/8a4e422612a7bd0b42da9aa50c21c7bf3ea401b8/src/components/feeds/notify.ts#L1
2 |
3 | // import { getCookie, getUserID, setCookie } from '~/utils/main'
4 |
5 | /** Update the time interval of topbar notifications and moments counts */
6 | export const updateInterval = 1000 * 60 * 5 // Updated every 5 minutes
7 |
8 | // const getLastID = (): string => `${getCookie(`bp_t_offset_${getUserID()}`)}`
9 |
10 | // function compareID(currentID: string, lastOffsetID: string): boolean {
11 | // if (currentID === lastOffsetID)
12 | // return false
13 | // else if (Number(currentID) > Number(lastOffsetID))
14 | // return true
15 | // else
16 | // return false
17 | // }
18 |
19 | // export function setLastId(id: string) {
20 | // if (id === null || id === undefined)
21 | // return
22 |
23 | // if (compareID(id))
24 | // return
25 |
26 | // setCookie(`bp_t_offset_${getUserID()}`, id, 30)
27 | // }
28 |
29 | // export const isNewId = (id: string): boolean => compareID(id, getLastID())
30 |
--------------------------------------------------------------------------------
/src/components/TopBar/types.ts:
--------------------------------------------------------------------------------
1 | // https://github.com/SocialSisterYi/bilibili-API-collect/blob/e379d904c2753fa30e9083f59016f07e89d19467/docs/login/login_info.md#%E5%AF%BC%E8%88%AA%E6%A0%8F%E7%94%A8%E6%88%B7%E4%BF%A1%E6%81%AF
2 | export interface UserInfo {
3 | face: string // avatar
4 | level_info: {
5 | current_level: number
6 | current_min: number
7 | current_exp: number
8 | next_exp: number
9 | }
10 | mid: number
11 | money: number // 硬幣
12 | uname: string // username
13 | vip: {
14 | status: number // 1 is vip
15 | due_date: number
16 | }
17 | wallet: {
18 | mid: number
19 | bcoin_balance: number // b幣
20 | coupon_balance: number // 每个月可领数量
21 | }
22 | wbi_img: {
23 | img_url: string
24 | sub_url: string
25 | }
26 | is_senior_member: boolean
27 | }
28 |
29 | /**
30 | * Number of follower, following and published posts by user
31 | */
32 | export interface UserStat {
33 | dynamic_count: number
34 | follower: number
35 | following: number
36 | }
37 |
38 | // https://github.com/SocialSisterYi/bilibili-API-collect/blob/63da4454309e2599269125e24a6940b1feecedef/message/msg.md#%E6%9C%AA%E8%AF%BB%E6%B6%88%E6%81%AF%E6%95%B0
39 | export interface UnReadMessage {
40 | at: number
41 | chat: number
42 | like: number
43 | reply: number
44 | sys_msg: number
45 | up: number
46 | }
47 |
48 | export interface UnReadDm {
49 | // https://api.vc.bilibili.com/session_svr/v1/session_svr/single_unread?build=0&mobi_app=web&unread_type=0
50 | unfollow_unread: number
51 | follow_unread: number
52 | unfollow_push_msg: number
53 | dustbin_push_msg: number
54 | dustbin_unread: number
55 | biz_msg_unfollow_unread: number
56 | biz_msg_follow_unread: number
57 | }
58 |
59 | export enum MomentType {
60 | Video = 8,
61 | Article = 64,
62 | Bangumi = 512,
63 | PGC = 4097,
64 | Movie = 4098,
65 | TvShow = 4099,
66 | ChineseAnime = 4100,
67 | Documentary = 4101,
68 | }
69 |
70 | export interface FavoriteCategory {
71 | id: number
72 | fid: number
73 | mid: number
74 | attr: number
75 | title: string
76 | fav_state: number
77 | media_count: number
78 | }
79 |
80 | // https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/fav/list.md#%E8%8E%B7%E5%8F%96%E6%94%B6%E8%97%8F%E5%A4%B9%E5%86%85%E5%AE%B9%E6%98%8E%E7%BB%86%E5%88%97%E8%A1%A8
81 | export interface FavoriteResource {
82 | id: number
83 | type: number // 2:视频稿件 12:音频 21:视频合集
84 | title: string
85 | cover: string
86 | intro: string
87 | page: number // 视频分P数
88 | duration: number // 音频/视频时长
89 | /** UP主信息 */
90 | upper: {
91 | mid: number
92 | name: string
93 | face: string
94 | }
95 | /** 状态数 */
96 | cnt_info: {
97 | collect: number // 收藏数
98 | play: number // 播放数
99 | danmaku: number // 弹幕数
100 | }
101 | link: string
102 | ctime: number // 投稿时间
103 | pubtime: number // 发布时间
104 | fav_time: number // 收藏时间
105 | bv_id: string
106 | bvid: string
107 | }
108 |
109 | export interface PopupVisibleState {
110 | channels: boolean
111 | userPanel: boolean
112 | notifications: boolean
113 | moments: boolean
114 | favorites: boolean
115 | history: boolean
116 | watchLater: boolean
117 | upload: boolean
118 | more: boolean
119 | }
120 |
121 | export interface TopBarItemElements {
122 | [key: string]: Ref
123 | }
124 |
125 | export interface TopBarTransformers {
126 | [key: string]: Ref
127 | }
128 |
129 | // B币领取状态相关类型定义
130 | export interface PrivilegeItem {
131 | type: number
132 | state: number // 0:未兑换 1:已兑换 2:未完成(若需要完成)
133 | expire_time: number
134 | vip_type: number
135 | next_receive_days: number
136 | period_end_unix: number
137 | }
138 |
139 | export interface PrivilegeInfo {
140 | list: PrivilegeItem[]
141 | is_vip: boolean
142 | vip_status: number
143 | vip_type: number
144 | }
145 |
--------------------------------------------------------------------------------
/src/components/VideoCard/VideoCardAuthor/components/VideoCardAuthorAvatar.vue:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 |
93 |
94 |
95 |
100 |
--------------------------------------------------------------------------------
/src/components/VideoCard/VideoCardAuthor/components/VideoCardAuthorName.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
20 |
21 |
22 | {{ $t('video_card.group_contribution', { firstAuthor: author[0].name, num: author.length }) }}
23 |
24 |
25 | {{ Array.isArray(author) ? author[0].name : author?.name }}
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/components/VideoCard/VideoCardSkeleton.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
13 |
14 |
15 |
19 |
20 |
24 |
28 |
32 | hello world
33 |
34 |
35 |
36 |
37 |
38 |
43 |
44 |
49 |
50 |
54 |
55 |
58 |
59 |
60 |
68 |
69 |
74 | hello world
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
91 |
--------------------------------------------------------------------------------
/src/components/VideoCard/types.ts:
--------------------------------------------------------------------------------
1 | import type { ThreePointV2 } from '~/models/video/appForYou'
2 |
3 | export interface Video {
4 | id: number
5 | duration?: number
6 | durationStr?: string
7 | title: string
8 | desc?: string
9 | cover: string
10 |
11 | /** `author` for individual submissions by UP; `authorList` for collaborative submissions by UP */
12 | author?: Author | Author[]
13 |
14 | view?: number
15 | viewStr?: string
16 | danmaku?: number
17 | danmakuStr?: string
18 |
19 | publishedTimestamp?: number
20 | capsuleText?: string
21 |
22 | bvid?: string
23 | aid?: number
24 | // used for live
25 | roomid?: number
26 | epid?: number
27 | goto?: string
28 | /** After set the `url`, clicking the video will navigate to this url. It won't be affected by aid, bvid or epid */
29 | url?: string
30 | /** Better to provide cid, otherwise video preview will need to call another API to get it */
31 | cid?: number
32 |
33 | followed?: boolean
34 | liveStatus?: number
35 |
36 | tag?: string
37 | rank?: number
38 | type?: 'horizontal' | 'vertical' | 'bangumi'
39 | threePointV2: ThreePointV2[]
40 |
41 | badge?: {
42 | bgColor: string
43 | color: string
44 | iconUrl?: string
45 | text: string
46 | }
47 | }
48 |
49 | export interface Author {
50 | name?: string
51 | /** After set the `authorUrl`, clicking the author's name or avatar will navigate to this url. It won't be affected by mid */
52 | authorUrl?: string
53 | authorFace: string
54 | followed?: boolean | undefined
55 | mid?: number
56 | }
57 |
--------------------------------------------------------------------------------
/src/components/VideoCard/utils.ts:
--------------------------------------------------------------------------------
1 | import type { Author, Video } from './types'
2 |
3 | export function getAuthorJumpUrl(author?: Author) {
4 | if (!author)
5 | return ''
6 |
7 | return author.authorUrl || (author.mid ? `//space.bilibili.com/${author.mid}` : '')
8 | }
9 |
10 | export function getCurrentTime(videoElement: Ref) {
11 | if (videoElement.value) {
12 | return videoElement.value.currentTime
13 | }
14 | return null
15 | }
16 |
17 | export function getCurrentVideoUrl(video: Video, videoCurrentTime: Ref) {
18 | const baseUrl = `https://www.bilibili.com/video/${video.bvid ?? `av${video.aid}`}`
19 | const currentTime = videoCurrentTime.value
20 | return currentTime && currentTime > 5 ? `${baseUrl}/?t=${currentTime}` : `${baseUrl}`
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/index.ts:
--------------------------------------------------------------------------------
1 | import type { App, Plugin } from 'vue'
2 |
3 | const paths: Record = import.meta.glob(['./*/*.vue', './*.vue', './OverlayScrollbarsComponent.ts'], { eager: true })
4 |
5 | export default {
6 | install: (app: App) => {
7 | for (const path in paths) {
8 | const splitPath = path.split('/')
9 | const name = splitPath[splitPath.length - 1].replace('.vue', '').replace('.ts', '')
10 | app.component(name, paths[path].default)
11 | }
12 | },
13 | } as Plugin
14 |
--------------------------------------------------------------------------------
/src/composables/useAppProvider.ts:
--------------------------------------------------------------------------------
1 | import type { Ref } from 'vue'
2 |
3 | import type { AppPage } from '~/enums/appEnums'
4 |
5 | export interface BewlyAppProvider {
6 | activatedPage: Ref
7 | scrollbarRef: Ref
8 | reachTop: Ref
9 | mainAppRef: Ref
10 | handleReachBottom: Ref<(() => void) | undefined>
11 | handlePageRefresh: Ref<(() => void) | undefined>
12 | // 添加撤销刷新的处理函数
13 | handleUndoRefresh: Ref<(() => void) | undefined>
14 | handleForwardRefresh: Ref<(() => void) | undefined>
15 | // 添加控制撤销按钮显示的状态
16 | showUndoButton: Ref
17 | handleBackToTop: (targetScrollTop?: number) => void
18 | haveScrollbar: () => Promise
19 | openIframeDrawer: (url: string) => void
20 | }
21 |
22 | export function useBewlyApp(): BewlyAppProvider {
23 | const provider = inject('BEWLY_APP')
24 |
25 | if (import.meta.env.DEV && !provider)
26 | throw new Error('AppProvider is not injected')
27 |
28 | return provider!
29 | }
30 |
--------------------------------------------------------------------------------
/src/composables/useDelayedHover.ts:
--------------------------------------------------------------------------------
1 | import { settings } from '~/logic'
2 |
3 | // DISABLED WHEN IN TOUCHSCREEN OPTIMIZATION IS ENABLED IN SETTINGS
4 | export function useDelayedHover({ enterDelay = 300, leaveDelay = 300, beforeEnter, enter, beforeLeave, leave }:
5 | { enterDelay?: number, leaveDelay?: number, beforeEnter?: Function, enter: Function, beforeLeave?: Function, leave: Function }) {
6 | const el = ref()
7 |
8 | let enterTimer: any | undefined
9 | let leaveTimer: any | undefined
10 |
11 | function handleMouseEnter() {
12 | if (beforeEnter)
13 | beforeEnter()
14 |
15 | if (enterTimer) {
16 | clearTimeout(enterTimer)
17 | enterTimer = undefined
18 | }
19 | if (leaveTimer) {
20 | clearTimeout(leaveTimer)
21 | leaveTimer = undefined
22 | }
23 | enterTimer = setTimeout(() => {
24 | enter()
25 | }, enterDelay)
26 | }
27 | function handleMouseLeave() {
28 | if (beforeLeave)
29 | beforeLeave()
30 |
31 | if (enterTimer) {
32 | clearTimeout(enterTimer)
33 | enterTimer = undefined
34 | }
35 | if (leaveTimer) {
36 | clearTimeout(leaveTimer)
37 | leaveTimer = undefined
38 | }
39 | leaveTimer = setTimeout(() => {
40 | leave()
41 | }, leaveDelay)
42 | }
43 |
44 | watch(el, (el, _, onCleanup) => {
45 | if (el) {
46 | if (!settings.value.touchScreenOptimization) {
47 | el.addEventListener('mouseenter', handleMouseEnter)
48 | el.addEventListener('mouseleave', handleMouseLeave)
49 | }
50 | }
51 |
52 | onCleanup(() => {
53 | if (el) {
54 | el.removeEventListener('mouseenter', handleMouseEnter)
55 | el.removeEventListener('mouseleave', handleMouseLeave)
56 | }
57 | })
58 | }, { flush: 'post' })
59 |
60 | watch(() => settings.value.touchScreenOptimization, (newValue) => {
61 | if (newValue) {
62 | el.value?.removeEventListener('mouseenter', handleMouseEnter)
63 | el.value?.removeEventListener('mouseleave', handleMouseLeave)
64 | }
65 | else {
66 | el.value?.addEventListener('mouseenter', handleMouseEnter)
67 | el.value?.addEventListener('mouseleave', handleMouseLeave)
68 | }
69 | }, { immediate: true })
70 |
71 | return el
72 | }
73 |
--------------------------------------------------------------------------------
/src/constants/globalEvents.ts:
--------------------------------------------------------------------------------
1 | export const TOP_BAR_VISIBILITY_CHANGE = 'topBarVisibilityChange'
2 | export const OVERLAY_SCROLL_BAR_SCROLL = 'overlayScrollBarScroll'
3 | export const BEWLY_MOUNTED = 'bewlyMounted'
4 | export const DRAWER_VIDEO_ENTER_PAGE_FULL = 'drawerVideoEnterPageFull'
5 | export const DRAWER_VIDEO_EXIT_PAGE_FULL = 'drawerVideoExitPageFull'
6 | export const IFRAME_PAGE_SWITCH_BEWLY = 'iframePageSwitchBewly'
7 | export const IFRAME_PAGE_SWITCH_BILI = 'iframePageSwitchBili'
8 | export const IFRAME_DARK_MODE_CHANGE = 'iframeDarkModeChange'
9 |
--------------------------------------------------------------------------------
/src/constants/imgs.ts:
--------------------------------------------------------------------------------
1 | export const SEARCH_BAR_CHARACTERS: { name: string, url: string }[] = [
2 | { name: '22 娘', url: 'https://cdn.jsdelivr.net/gh/BewlyBewly/Imgs/searchBarCharacters/22chan-1.png' },
3 | { name: '33 娘', url: 'https://cdn.jsdelivr.net/gh/BewlyBewly/Imgs/searchBarCharacters/33chan-1.png' },
4 | { name: '22 娘', url: 'https://cdn.jsdelivr.net/gh/BewlyBewly/Imgs/searchBarCharacters/22chan-2.png' },
5 | { name: '33 娘', url: 'https://cdn.jsdelivr.net/gh/BewlyBewly/Imgs/searchBarCharacters/33chan-2.png' },
6 | ]
7 |
8 | export interface wallpaperItem {
9 | name: string
10 | url: string
11 | thumbnail?: string
12 | // 本地壁纸引用字段
13 | id?: string
14 | isLocal?: boolean
15 | }
16 |
17 | export const WALLPAPERS: wallpaperItem[] = [
18 | // {
19 | // name: 'Unsplash Random Nature Image',
20 | // url: 'https://source.unsplash.com/1920x1080/?nature',
21 | // thumbnail: 'https://source.unsplash.com/1920x1080/?nature',
22 | // },
23 | // {
24 | // name: 'Unsplash Random Building Image',
25 | // url: 'https://source.unsplash.com/1920x1080/?building',
26 | // thumbnail: 'https://source.unsplash.com/1920x1080/?building',
27 | // },
28 | // {
29 | // name: 'Unsplash Random Night Scene Image',
30 | // url: 'https://source.unsplash.com/1920x1080/?night-scene',
31 | // thumbnail: 'https://source.unsplash.com/1920x1080/?night-scene',
32 | // },
33 | {
34 | name: 'LoremPicsum Random Image',
35 | url: 'https://picsum.photos/2560/1440/?nature',
36 | thumbnail: 'https://picsum.photos/2560/1440/?nature',
37 | },
38 | {
39 | name: 'Nicolas Lafargue - Rocky Mountain Cloudscape',
40 | url: 'https://cdn.jsdelivr.net/gh/BewlyBewly/Imgs/wallpapers/rocky-mountain-cloudscape.jpg',
41 | thumbnail: 'https://cdn.jsdelivr.net/gh/BewlyBewly/Imgs/wallpapers/rocky-mountain-cloudscape-thumbnail.jpg',
42 | },
43 | {
44 | name: 'Zongnan Bao- Green white mountains',
45 | url: 'https://cdn.jsdelivr.net/gh/BewlyBewly/Imgs/wallpapers/green-white-mountains.jpg',
46 | thumbnail: 'https://cdn.jsdelivr.net/gh/BewlyBewly/Imgs/wallpapers/green-white-mountains-thumbnail.jpg',
47 | },
48 | {
49 | name: 'Colin Watts - Night Sky Stars',
50 | url: 'https://cdn.jsdelivr.net/gh/BewlyBewly/Imgs/wallpapers/night-sky-stars.jpg',
51 | thumbnail: 'https://cdn.jsdelivr.net/gh/BewlyBewly/Imgs/wallpapers/night-sky-stars-thumbnail.jpg',
52 | },
53 | {
54 | name: 'Ryan Geller - Sailboats moored at Land and Sea Park in The Exumas',
55 | url: 'https://cdn.jsdelivr.net/gh/BewlyBewly/Imgs/wallpapers/sailboats-moored-at-the-exumas.jpg',
56 | thumbnail: 'https://cdn.jsdelivr.net/gh/BewlyBewly/Imgs/wallpapers/sailboats-moored-at-the-exumas-thumbnail.jpg',
57 | },
58 | {
59 | name: 'NASA - Outer Space Photo',
60 | url: 'https://cdn.jsdelivr.net/gh/BewlyBewly/Imgs/wallpapers/outer-space-photo.jpg',
61 | thumbnail: 'https://cdn.jsdelivr.net/gh/BewlyBewly/Imgs/wallpapers/outer-space-photo-thumbnail.jpg',
62 | },
63 | {
64 | name: 'BML2019 VR (pid: 74271400)',
65 | url: 'https://cdn.jsdelivr.net/gh/BewlyBewly/Imgs/wallpapers/bml2019-vr.jpg',
66 | thumbnail: 'https://cdn.jsdelivr.net/gh/BewlyBewly/Imgs/wallpapers/bml2019-vr-thumbnail.jpg',
67 | },
68 | {
69 | name: '2020 拜年祭活动',
70 | url: 'https://cdn.jsdelivr.net/gh/BewlyBewly/Imgs/wallpapers/2020-拜年祭活动.jpg',
71 | thumbnail: 'https://cdn.jsdelivr.net/gh/BewlyBewly/Imgs/wallpapers/2020-拜年祭活动-thumbnail.jpg',
72 | },
73 | {
74 | name: '2020 BDF',
75 | url: 'https://cdn.jsdelivr.net/gh/BewlyBewly/Imgs/wallpapers/2020-bdf.jpg',
76 | thumbnail: 'https://cdn.jsdelivr.net/gh/BewlyBewly/Imgs/wallpapers/2020-bdf-thumbnail.jpg',
77 | },
78 | ]
79 |
--------------------------------------------------------------------------------
/src/contentScripts/views/Home/types.ts:
--------------------------------------------------------------------------------
1 | import type { GridLayoutType } from '~/logic'
2 |
3 | export enum HomeSubPage {
4 | ForYou = 'ForYou',
5 | Following = 'Following',
6 | SubscribedSeries = 'SubscribedSeries',
7 | Trending = 'Trending',
8 | Ranking = 'Ranking',
9 | Live = 'Live',
10 | }
11 |
12 | export interface RankingType {
13 | id: number
14 | name: string
15 | rid?: number
16 | seasonType?: number
17 | type?: string
18 | }
19 |
20 | export interface GridLayoutIcon {
21 | icon: string
22 | iconActivated: string
23 | value: GridLayoutType
24 | }
25 |
--------------------------------------------------------------------------------
/src/contentScripts/views/Moments/Moments.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Moments (WIP)
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/contentScripts/views/Search/Search.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
13 |
18 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/enums/appEnums.ts:
--------------------------------------------------------------------------------
1 | export enum LanguageType {
2 | English = 'en',
3 | Mandarin_CN = 'cmn-CN',
4 | Mandarin_TW = 'cmn-TW',
5 | Cantonese = 'jyut', // Hakadao: jyut 爲「粵」之粵拼,我覺得保留這一點作爲彩蛋或冷知識也不錯
6 | }
7 |
8 | export enum AppPage {
9 | Home = 'Home',
10 | Search = 'Search',
11 | Anime = 'Anime',
12 | History = 'History',
13 | Favorites = 'Favorites',
14 | WatchLater = 'WatchLater',
15 | Moments = 'Moments',
16 | }
17 |
18 | export enum TopBarPopup {
19 | FavoritesPop = 'FavoritesPop',
20 | HistoryPop = 'HistoryPop',
21 | MomentsPop = 'MomentsPop',
22 | NotificationsPop = 'NotificationsPop',
23 | UploadPop = 'UploadPop',
24 | WatchLaterPop = 'WatchLaterPop',
25 | }
26 |
27 | export enum VideoPageTopBarConfig {
28 | AlwaysShow = 'alwaysShow', // 总是显示
29 | AlwaysHide = 'alwaysHide', // 总是隐藏
30 | ShowOnMouse = 'showOnMouse', // 鼠标显示
31 | ShowOnScroll = 'showOnScroll', // 滚动显示
32 | }
33 |
--------------------------------------------------------------------------------
/src/global.d.ts:
--------------------------------------------------------------------------------
1 | declare const __DEV__: boolean
2 |
3 | declare module '*.vue' {
4 | const component: any
5 | export default component
6 | }
7 |
--------------------------------------------------------------------------------
/src/inject/README.md:
--------------------------------------------------------------------------------
1 | # inject/
2 |
3 | All injected scripts will be placed in this folder.
4 |
--------------------------------------------------------------------------------
/src/logic/common-setup.ts:
--------------------------------------------------------------------------------
1 | import 'vue-toastification/dist/index.css'
2 |
3 | import { createPinia } from 'pinia'
4 | import type { App } from 'vue'
5 | import Toast, { POSITION } from 'vue-toastification'
6 | import { getCurrentContext } from 'webext-bridge'
7 |
8 | import components from '~/components'
9 | import { i18n } from '~/utils/i18n'
10 |
11 | const pinia = createPinia()
12 |
13 | export async function setupApp(app: App) {
14 | const context = getCurrentContext()
15 |
16 | // Inject a globally available `$app` object in template
17 | app.config.globalProperties.$app = { context }
18 |
19 | // Provide access to `app` in script setup with `const app = inject('app')`
20 | app.provide('app', app.config.globalProperties.$app)
21 |
22 | // Here you can install additional plugins for all contexts: popup, options page and content-script.
23 | // example: app.use(i18n)
24 | // example excluding content-script context: if (context !== 'content-script') app.use(i18n)
25 | app.use(i18n)
26 | app
27 | .use(Toast, {
28 | transition: 'Vue-Toastification__fade',
29 | maxToasts: 20,
30 | newestOnTop: true,
31 | position: POSITION.TOP_RIGHT,
32 | })
33 | app.use(components)
34 | app.use(pinia)
35 | }
36 |
--------------------------------------------------------------------------------
/src/logic/index.ts:
--------------------------------------------------------------------------------
1 | export * from './storage'
2 |
--------------------------------------------------------------------------------
/src/models/anime/popular.ts:
--------------------------------------------------------------------------------
1 | export interface PopularAnimeResult {
2 | code: number
3 | message: string
4 | result: Result
5 | }
6 |
7 | export interface Result {
8 | list: List[]
9 | note: string
10 | }
11 |
12 | export interface List {
13 | badge: string
14 | badge_info: BadgeInfo
15 | badge_type: number
16 | copyright: string
17 | cover: string
18 | enable_vt: boolean
19 | icon_font: IconFont
20 | new_ep: NewEp
21 | rank: number
22 | rating: string
23 | season_id: number
24 | ss_horizontal_cover: string
25 | stat: Stat
26 | title: string
27 | url: string
28 | }
29 |
30 | export interface BadgeInfo {
31 | bg_color: string
32 | bg_color_night: string
33 | text: string
34 | }
35 |
36 | export interface IconFont {
37 | name: string
38 | text: string
39 | }
40 |
41 | export interface NewEp {
42 | cover: string
43 | index_show: string
44 | }
45 |
46 | export interface Stat {
47 | danmaku: number
48 | follow: number
49 | series_follow: number
50 | view: number
51 | }
52 |
--------------------------------------------------------------------------------
/src/models/anime/recommendation.ts:
--------------------------------------------------------------------------------
1 | export interface RecommendationResult {
2 | code: number
3 | data: Data
4 | message: string
5 | }
6 |
7 | export interface Data {
8 | coursor: number
9 | has_next: boolean
10 | items: Item[]
11 | }
12 |
13 | export interface Item {
14 | rank_id: number
15 | sub_items: ItemSubItem[]
16 | text: any[]
17 | }
18 |
19 | export interface ItemSubItem {
20 | card_style: string
21 | cover: string
22 | episode_id?: number
23 | evaluate?: string
24 | hover?: Hover
25 | inline?: Inline
26 | link?: string
27 | rank_id: number
28 | rating?: string
29 | rating_count?: number
30 | report: Report
31 | season_id?: number
32 | season_type?: number
33 | stat?: Stat
34 | sub_title: string
35 | text: any[]
36 | title: string
37 | user_status?: UserStatus
38 | sub_items?: SubItemSubItem[]
39 | }
40 |
41 | export interface Hover {
42 | img: string
43 | text: string[]
44 | }
45 |
46 | export interface Inline {
47 | end_time: number
48 | ep_id: number
49 | first_ep: number
50 | material_no: string
51 | scene: number
52 | start_time: number
53 | }
54 |
55 | export interface Report {
56 | first_ep?: number
57 | scene?: number
58 | }
59 |
60 | export interface Stat {
61 | danmaku: number
62 | duration: number
63 | view: number
64 | }
65 |
66 | export interface SubItemSubItem {
67 | card_style: string
68 | cover: string
69 | evaluate: string
70 | hover: Hover
71 | inline: Inline
72 | link: string
73 | rank_id: number
74 | rating?: string
75 | rating_count?: number
76 | report: Report
77 | season_id: number
78 | season_type: number
79 | stat: Stat
80 | sub_title: string
81 | text: any[]
82 | title: string
83 | user_status: UserStatus
84 | }
85 |
86 | export interface UserStatus {
87 | follow: number
88 | }
89 |
--------------------------------------------------------------------------------
/src/models/anime/timeTable.ts:
--------------------------------------------------------------------------------
1 | export interface TimetableResult {
2 | code: number
3 | message: string
4 | result: Result[]
5 | }
6 |
7 | export interface Result {
8 | date: string
9 | date_ts: number
10 | day_of_week: number
11 | episodes: Episode[]
12 | is_today: number
13 | }
14 |
15 | export interface Episode {
16 | cover: string
17 | delay: number
18 | delay_id: number
19 | delay_index: string
20 | delay_reason: string
21 | enable_vt: boolean
22 | ep_cover: string
23 | episode_id: number
24 | follow: number
25 | follows: string
26 | icon_font: IconFont
27 | plays: string
28 | pub_index: string
29 | pub_time: string
30 | pub_ts: number
31 | published: number
32 | season_id: number
33 | square_cover: string
34 | title: string
35 | }
36 |
37 | export interface IconFont {
38 | name: string
39 | text: string
40 | }
41 |
--------------------------------------------------------------------------------
/src/models/history/history.ts:
--------------------------------------------------------------------------------
1 | export interface HistoryResult {
2 | code: number
3 | message: string
4 | ttl: number
5 | data: Data
6 | }
7 |
8 | export interface Data {
9 | cursor: Cursor
10 | tab: Tab[]
11 | list: List[]
12 | }
13 |
14 | export interface Cursor {
15 | max: number
16 | view_at: number
17 | business: Business | string
18 | ps: number
19 | }
20 |
21 | export enum Business {
22 | ARCHIVE = 'archive',
23 | PGC = 'pgc',
24 | LIVE = 'live',
25 | ARTICLE = 'article',
26 | ARTICLE_LIST = 'article-list',
27 | }
28 |
29 | export interface List {
30 | title: string
31 | long_title: string
32 | cover: string
33 | covers: null
34 | uri: string
35 | history: History
36 | videos: number
37 | author_name: string
38 | author_face: string
39 | author_mid: number
40 | view_at: number
41 | progress: number
42 | badge: string
43 | show_title: string
44 | duration: number
45 | current: string
46 | total: number
47 | new_desc: string
48 | is_finish: number
49 | is_fav: number
50 | kid: number
51 | tag_name: string
52 | live_status: number
53 | }
54 |
55 | export interface History {
56 | oid: number
57 | epid: number
58 | bvid: string
59 | page: number
60 | cid: number
61 | part: string
62 | business: Business
63 | dt: number
64 | }
65 |
66 | export interface Tab {
67 | type: string
68 | name: string
69 | }
70 |
--------------------------------------------------------------------------------
/src/models/live/getFollowingLiveList.ts:
--------------------------------------------------------------------------------
1 | // https://app.quicktype.io/?l=ts
2 |
3 | export interface FollowingLiveResult {
4 | code: number
5 | message: string
6 | ttl: number
7 | data: Data
8 | }
9 |
10 | export interface Data {
11 | title: string
12 | pageSize: number
13 | totalPage: number
14 | list: List[]
15 | count: number
16 | never_lived_count: number
17 | live_count: number
18 | never_lived_faces: any[]
19 | }
20 |
21 | export interface List {
22 | roomid: number
23 | uid: number
24 | uname: string
25 | title: string
26 | face: string
27 | live_status: number
28 | record_num: number
29 | recent_record_id: string
30 | is_attention: number
31 | clipnum: number
32 | fans_num: number
33 | area_name: string
34 | area_value: string
35 | tags: string
36 | recent_record_id_v2: string
37 | record_num_v2: number
38 | record_live_time: number
39 | area_name_v2: string
40 | room_news: string
41 | switch: boolean
42 | watch_icon: string
43 | text_small: string
44 | room_cover: string
45 | parent_area_id: number
46 | area_id: number
47 | }
48 |
--------------------------------------------------------------------------------
/src/models/video/favorite.ts:
--------------------------------------------------------------------------------
1 | export interface FavoritesResult {
2 | code: number
3 | message: string
4 | ttl: number
5 | data: Data
6 | }
7 |
8 | export interface Data {
9 | info: Info
10 | medias: Media[]
11 | has_more: boolean
12 | ttl: number
13 | }
14 |
15 | export interface Info {
16 | id: number
17 | fid: number
18 | mid: number
19 | attr: number
20 | title: string
21 | cover: string
22 | upper: InfoUpper
23 | cover_type: number
24 | cnt_info: InfoCntInfo
25 | type: number
26 | intro: string
27 | ctime: number
28 | mtime: number
29 | state: number
30 | fav_state: number
31 | like_state: number
32 | media_count: number
33 | }
34 |
35 | export interface InfoCntInfo {
36 | collect: number
37 | play: number
38 | thumb_up: number
39 | share: number
40 | }
41 |
42 | export interface InfoUpper {
43 | mid: number
44 | name: string
45 | face: string
46 | followed: boolean
47 | vip_type: number
48 | vip_statue: number
49 | }
50 |
51 | export interface Media {
52 | id: number
53 | type: number
54 | title: string
55 | cover: string
56 | intro: string
57 | page: number
58 | duration: number
59 | upper: MediaUpper
60 | attr: number
61 | cnt_info: MediaCntInfo
62 | link: string
63 | ctime: number
64 | pubtime: number
65 | fav_time: number
66 | bv_id: string
67 | bvid: string
68 | season: null
69 | ogv: null
70 | ugc: Ugc
71 | }
72 |
73 | export interface MediaCntInfo {
74 | collect: number
75 | play: number
76 | danmaku: number
77 | vt: number
78 | play_switch: number
79 | reply: number
80 | view_text_1: string
81 | }
82 |
83 | export interface Ugc {
84 | first_cid: number
85 | }
86 |
87 | export interface MediaUpper {
88 | mid: number
89 | name: string
90 | face: string
91 | }
92 |
--------------------------------------------------------------------------------
/src/models/video/favoriteCategory.ts:
--------------------------------------------------------------------------------
1 | export interface FavoritesCategoryResult {
2 | code: number
3 | message: string
4 | ttl: number
5 | data: Data
6 | }
7 |
8 | export interface Data {
9 | count: number
10 | list: List[]
11 | season: null
12 | }
13 |
14 | export interface List {
15 | id: number
16 | fid: number
17 | mid: number
18 | attr: number
19 | title: string
20 | fav_state: number
21 | media_count: number
22 | }
23 |
--------------------------------------------------------------------------------
/src/models/video/forYou.ts:
--------------------------------------------------------------------------------
1 | // https://app.quicktype.io/?l=ts
2 |
3 | export interface forYouResult {
4 | code: number
5 | message: string
6 | ttl: number
7 | data: Data
8 | }
9 |
10 | export interface Data {
11 | item: Item[]
12 | side_bar_column: any[]
13 | business_card: null
14 | floor_info: null
15 | user_feature: null
16 | preload_expose_pct: number
17 | preload_floor_expose_pct: number
18 | mid: number
19 | }
20 |
21 | export interface Item {
22 | id: number
23 | bvid: string
24 | cid: number
25 | goto: Goto
26 | uri: string
27 | pic: string
28 | pic_4_3: string
29 | title: string
30 | duration: number
31 | pubdate: number
32 | owner: Owner
33 | stat: Stat
34 | av_feature: null
35 | is_followed: number
36 | rcmd_reason: RcmdReason
37 | show_info: number
38 | track_id: string
39 | pos: number
40 | room_info: null
41 | ogv_info: null
42 | business_info: null
43 | is_stock: number
44 | enable_vt: number
45 | vt_display: string
46 | }
47 |
48 | export enum Goto {
49 | AV = 'av',
50 | }
51 |
52 | export interface Owner {
53 | mid: number
54 | name: string
55 | face: string
56 | }
57 |
58 | export interface RcmdReason {
59 | reason_type: number
60 | content?: string
61 | }
62 |
63 | export interface Stat {
64 | view: number
65 | like: number
66 | danmaku: number
67 | vt: number
68 | }
69 |
--------------------------------------------------------------------------------
/src/models/video/historySearch.ts:
--------------------------------------------------------------------------------
1 | export interface HistorySearchResult {
2 | code: number
3 | message: string
4 | ttl: number
5 | data: Data
6 | }
7 |
8 | export interface Data {
9 | has_more: boolean
10 | page: Page
11 | list: List[]
12 | }
13 |
14 | export interface List {
15 | title: string
16 | long_title: string
17 | cover: string
18 | covers: null
19 | uri: string
20 | history: History
21 | videos: number
22 | author_name: string
23 | author_face: string
24 | author_mid: number
25 | view_at: number
26 | progress: number
27 | badge: string
28 | show_title: ShowTitle
29 | duration: number
30 | total: number
31 | new_desc: NewDesc
32 | is_finish: number
33 | is_fav: number
34 | kid: number
35 | tag_name: string
36 | live_status: number
37 | }
38 |
39 | export interface History {
40 | oid: number
41 | epid: number
42 | bvid: string
43 | page: number
44 | cid: number
45 | part: string
46 | business: Business
47 | dt: number
48 | }
49 |
50 | export enum Business {
51 | Archive = 'archive',
52 | }
53 |
54 | export enum NewDesc {
55 | Empty = '',
56 | 共2P = '共2P',
57 | 共4P = '共4P',
58 | }
59 |
60 | export enum ShowTitle {
61 | Empty = '',
62 | JohnLennonOnceSaid = 'john lennon once said',
63 | The4K = '4K',
64 | }
65 |
66 | export interface Page {
67 | pn: number
68 | total: number
69 | }
70 |
--------------------------------------------------------------------------------
/src/models/video/ranking.ts:
--------------------------------------------------------------------------------
1 | // https://app.quicktype.io/?l=ts
2 |
3 | export interface RankingResult {
4 | code: number
5 | message: string
6 | ttl: number
7 | data: Data
8 | }
9 |
10 | export interface Data {
11 | note: string
12 | list: List[]
13 | }
14 |
15 | export interface List {
16 | aid: number
17 | videos: number
18 | tid: number
19 | tname: string
20 | copyright: number
21 | pic: string
22 | title: string
23 | pubdate: number
24 | ctime: number
25 | desc: string
26 | state: number
27 | duration: number
28 | mission_id?: number
29 | rights: { [key: string]: number }
30 | owner: Owner
31 | stat: { [key: string]: number }
32 | dynamic: string
33 | cid: number
34 | dimension: Dimension
35 | short_link_v2: string
36 | first_frame: string
37 | pub_location?: string
38 | bvid: string
39 | score: number
40 | enable_vt: number
41 | up_from_v2?: number
42 | season_id?: number
43 | others?: List[]
44 | attribute?: number
45 | attribute_v2?: number
46 | charging_pay?: ChargingPay
47 | }
48 |
49 | export interface ChargingPay {
50 | level: number
51 | }
52 |
53 | export interface Dimension {
54 | width: number
55 | height: number
56 | rotate: number
57 | }
58 |
59 | export interface Owner {
60 | mid: number
61 | name: string
62 | face: string
63 | }
64 |
--------------------------------------------------------------------------------
/src/models/video/rankingPgc.ts:
--------------------------------------------------------------------------------
1 | // https://app.quicktype.io/?l=ts
2 |
3 | export interface RankingPgcResult {
4 | code: number
5 | data: Data
6 | message: string
7 | }
8 |
9 | export interface Data {
10 | list: List[]
11 | note: string
12 | season_type: number
13 | }
14 |
15 | export interface List {
16 | badge: Badge
17 | badge_info: BadgeInfo
18 | badge_type: number
19 | cover: string
20 | desc: string
21 | enable_vt: boolean
22 | icon_font: IconFont
23 | new_ep: NewEp
24 | rank: number
25 | rating: string
26 | season_id: number
27 | ss_horizontal_cover: string
28 | stat: Stat
29 | title: string
30 | url: string
31 | }
32 |
33 | export enum Badge {
34 | Empty = '',
35 | 会员专享 = '会员专享',
36 | 会员抢先 = '会员抢先',
37 | 独家 = '独家',
38 | 限时免费 = '限时免费',
39 | }
40 |
41 | export interface BadgeInfo {
42 | bg_color: BgColor
43 | bg_color_night: BgColorNight
44 | text: Badge
45 | }
46 |
47 | export enum BgColor {
48 | Fb7299 = '#FB7299',
49 | The00C0Ff = '#00C0FF',
50 | }
51 |
52 | export enum BgColorNight {
53 | Bb5B76 = '#BB5B76',
54 | The0B91Be = '#0B91BE',
55 | }
56 |
57 | export interface IconFont {
58 | name: Name
59 | text: string
60 | }
61 |
62 | export enum Name {
63 | PlaydataSquareLine500 = 'playdata-square-line@500',
64 | }
65 |
66 | export interface NewEp {
67 | cover: string
68 | index_show: string
69 | }
70 |
71 | export interface Stat {
72 | danmaku: number
73 | follow: number
74 | series_follow: number
75 | view: number
76 | }
77 |
--------------------------------------------------------------------------------
/src/models/video/trending.ts:
--------------------------------------------------------------------------------
1 | // https://app.quicktype.io/?l=ts
2 |
3 | export interface TrendingResult {
4 | code: number
5 | message: string
6 | ttl: number
7 | data: Data
8 | }
9 |
10 | export interface Data {
11 | list: List[]
12 | no_more: boolean
13 | }
14 |
15 | export interface List {
16 | aid: number
17 | videos: number
18 | tid: number
19 | tname: string
20 | copyright: number
21 | pic: string
22 | title: string
23 | pubdate: number
24 | ctime: number
25 | desc: string
26 | state: number
27 | duration: number
28 | mission_id?: number
29 | rights: { [key: string]: number }
30 | owner: Owner
31 | stat: { [key: string]: number }
32 | dynamic: string
33 | cid: number
34 | dimension: Dimension
35 | season_id?: number
36 | short_link_v2: string
37 | first_frame: string
38 | pub_location: string
39 | bvid: string
40 | season_type: number
41 | is_ogv: boolean
42 | ogv_info: null
43 | enable_vt: number
44 | ai_rcmd: null
45 | rcmd_reason: RcmdReason
46 | up_from_v2?: number
47 | }
48 |
49 | export interface Dimension {
50 | width: number
51 | height: number
52 | rotate: number
53 | }
54 |
55 | export interface Owner {
56 | mid: number
57 | name: string
58 | face: string
59 | }
60 |
61 | export interface RcmdReason {
62 | content: string
63 | corner_mark: number
64 | }
65 |
--------------------------------------------------------------------------------
/src/models/video/videoPreview.ts:
--------------------------------------------------------------------------------
1 | export interface VideoPreviewResult {
2 | code: number
3 | message: string
4 | ttl: number
5 | data: Data
6 | }
7 |
8 | export interface Data {
9 | from: string
10 | result: string
11 | message: string
12 | quality: number
13 | format: string
14 | timelength: number
15 | accept_format: string
16 | accept_description: string[]
17 | accept_quality: number[]
18 | video_codecid: number
19 | seek_param: string
20 | seek_type: string
21 | durl: Durl[]
22 | support_formats: SupportFormat[]
23 | high_format: null
24 | volume: Volume
25 | last_play_time: number
26 | last_play_cid: number
27 | }
28 |
29 | export interface Durl {
30 | order: number
31 | length: number
32 | size: number
33 | ahead: string
34 | vhead: string
35 | url: string
36 | backup_url: null
37 | }
38 |
39 | export interface SupportFormat {
40 | quality: number
41 | format: string
42 | new_description: string
43 | display_desc: string
44 | superscript: string
45 | codecs: null
46 | }
47 |
48 | export interface Volume {
49 | measured_i: number
50 | measured_lra: number
51 | measured_tp: number
52 | measured_threshold: number
53 | target_offset: number
54 | target_i: number
55 | target_tp: number
56 | }
57 |
--------------------------------------------------------------------------------
/src/models/video/watchLater.ts:
--------------------------------------------------------------------------------
1 | export interface WatchLaterResult {
2 | code: number
3 | message: string
4 | ttl: number
5 | data: Data
6 | }
7 |
8 | export interface Data {
9 | count: number
10 | list: List[]
11 | }
12 |
13 | export interface List {
14 | aid: number
15 | videos: number
16 | tid: number
17 | tname: string
18 | copyright: number
19 | pic: string
20 | title: string
21 | pubdate: number
22 | ctime: number
23 | desc: string
24 | state: number
25 | duration: number
26 | rights: { [key: string]: number }
27 | owner: Owner
28 | stat: { [key: string]: number }
29 | dynamic: Dynamic
30 | dimension: Dimension
31 | short_link_v2: string
32 | up_from_v2?: number
33 | first_frame: string
34 | pub_location: string
35 | page: Page
36 | count: number
37 | cid: number
38 | progress: number
39 | add_at: number
40 | bvid: string
41 | uri: string
42 | enable_vt: number
43 | view_text_1: string
44 | card_type: number
45 | left_icon_type: number
46 | left_text: string
47 | right_icon_type: number
48 | right_text: string
49 | arc_state: number
50 | pgc_label: string
51 | show_up: boolean
52 | forbid_fav: boolean
53 | forbid_sort: boolean
54 | season_id?: number
55 | mission_id?: number
56 | }
57 |
58 | export interface Dimension {
59 | width: number
60 | height: number
61 | rotate: number
62 | }
63 |
64 | export enum Dynamic {
65 | Empty = '',
66 | 后期鸽看了看自己暗淡无光的羽毛又看了看你们手里闪闪发光的硬币 = '后期鸽看了看自己暗淡无光的羽毛,又看了看你们手里闪闪发光的硬币',
67 | }
68 |
69 | export interface Owner {
70 | mid: number
71 | name: string
72 | face: string
73 | }
74 |
75 | export interface Page {
76 | cid: number
77 | page: number
78 | from: From
79 | part: string
80 | duration: number
81 | vid: string
82 | weblink: string
83 | dimension: Dimension
84 | first_frame: string
85 | }
86 |
87 | export enum From {
88 | Vupload = 'vupload',
89 | }
90 |
--------------------------------------------------------------------------------
/src/options/Options.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 | Options
9 |
10 | This is the options page
11 |
12 |
13 |
14 |
15 |
16 | Powered by Vite
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/options/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Options
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/options/main.ts:
--------------------------------------------------------------------------------
1 | import '../styles'
2 |
3 | import { createApp } from 'vue'
4 |
5 | import App from './Options.vue'
6 |
7 | const app = createApp(App)
8 | app.mount('#app')
9 |
--------------------------------------------------------------------------------
/src/popup/Popup.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 | Popup
13 |
14 | This is the popup page
15 |
16 |
19 |
20 | Storage
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/popup/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Popup
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/popup/main.ts:
--------------------------------------------------------------------------------
1 | import '../styles'
2 |
3 | import { createApp } from 'vue'
4 |
5 | import App from './Popup.vue'
6 |
7 | const app = createApp(App)
8 | app.mount('#app')
9 |
--------------------------------------------------------------------------------
/src/stores/settingsStore.ts:
--------------------------------------------------------------------------------
1 | import { defineStore } from 'pinia'
2 |
3 | import type { AppPage } from '~/enums/appEnums'
4 | import { settings } from '~/logic'
5 |
6 | export interface DockItemConfig {
7 | page: AppPage
8 | visible: boolean
9 | openInNewTab: boolean
10 | useOriginalBiliPage: boolean
11 | }
12 |
13 | export const useSettingsStore = defineStore('settings', () => {
14 | function getDockItemConfigByPage(page: AppPage): DockItemConfig | undefined {
15 | return settings.value.dockItemsConfig.find(e => e.page === page)
16 | }
17 |
18 | function getDockItemIsUseOriginalBiliPage(page: AppPage): boolean {
19 | return settings.value.dockItemsConfig.find(e => e.page === page)?.useOriginalBiliPage || false
20 | }
21 |
22 | return { getDockItemConfigByPage, getDockItemIsUseOriginalBiliPage }
23 | })
24 |
--------------------------------------------------------------------------------
/src/styles/adaptedStyles/adaptedStyles-cmn_CN.md:
--------------------------------------------------------------------------------
1 | # Adapted Styles
2 |
3 | 这里放置暗色模式的 CSS 或更改主题颜色。
4 |
5 | 在 `index.ts` 中,我们将编写一些正则表达式,以匹配特定页面上使用的样式。
6 |
7 | ## 样式文件编写风格
8 |
9 | ``` scss
10 | .bewly-design.pageName {
11 | // 实现对页面的特定修改,例如微调布局,将这些样式放在这里
12 | .right-side-bar .catalog {
13 | line-height: 3em;
14 | }
15 |
16 | // ...
17 |
18 | // #region theme color adaption part
19 | // 通过在 `:not()` 中编写一个不存在的选择器来增加内部样式的优先级。
20 | :not(foobar) {
21 | a,
22 | b,
23 | c {
24 | color: var(--bew-theme-color);
25 | }
26 |
27 | d,
28 | e,
29 | f {
30 | // 请注意,使用 `!important` 应该是最后万不得已的手段
31 | color: var(--bew-theme-color) !important;
32 | }
33 |
34 | g,
35 | h,
36 | i {
37 | background-color: var(--bew-theme-color);
38 | }
39 |
40 | j,
41 | k,
42 | l {
43 | background-color: var(--bew-theme-color) !important;
44 | }
45 |
46 | // ...
47 | }
48 | // #endregion
49 |
50 | // #region dark mode adaption part
51 | &.dark {
52 | aa,
53 | bb,
54 | cc {
55 | color: var(--bew-text-1);
56 | }
57 |
58 | dd,
59 | ee,
60 | ff {
61 | color: var(--bew-text-1) !important;
62 | }
63 |
64 | // ...
65 | }
66 | // #endregion
67 | }
68 | ```
69 |
70 | ## 为什么要使用上述的编写风格?
71 |
72 | 也许你会对为什么应该遵循建议的编写风格感到困惑,所以在这里我们将解释一下。
73 | 由于这并不是按照页面特定的初始样式编写的,并且页面已经有了原始样式,你不能简单地写入像 `xxx {border: 1px solid white; color: black}` 这样的 CSS。
74 | 遵循前面的这种编写风格会使得维护暗模式样式变得困难。这是因为暗色模式主要需要改变文本颜色、背景颜色或边框颜色。
75 |
76 | 根据字体颜色、背景颜色和边框颜色高效地进行分组,并通过将适当的选择器放在一起统一方法,以便进行轻松维护。在必要时,只需调整相应样式中的相应选择器。
77 |
--------------------------------------------------------------------------------
/src/styles/adaptedStyles/adaptedStyles-cmn_TW.md:
--------------------------------------------------------------------------------
1 | # Adapted Styles
2 |
3 | 在這裏放置深色模式的 CSS 或更改主題顏色。
4 |
5 | 在 `index.ts` 中,我們將編寫一些正規表達式來匹配特定頁面上使用的樣式。
6 |
7 | ## 樣式表檔案撰寫風格
8 |
9 | ``` scss
10 | .bewly-design.pageName {
11 | // 在此處實施對頁面的特定修改,例如調整佈局,並將那些樣式放在這裏。
12 | .right-side-bar .catalog {
13 | line-height: 3em;
14 | }
15 |
16 | // ...
17 |
18 | // #region theme color adaption part
19 | // 透過在 `:not()` 中寫入一個不存在的選取器來提高內部樣式的優先級。
20 | :not(foobar) {
21 | a,
22 | b,
23 | c {
24 | color: var(--bew-theme-color);
25 | }
26 |
27 | d,
28 | e,
29 | f {
30 | // 請注意,使用 `!important` 應該是最後萬不得已的手段
31 | color: var(--bew-theme-color) !important;
32 | }
33 |
34 | g,
35 | h,
36 | i {
37 | background-color: var(--bew-theme-color);
38 | }
39 |
40 | j,
41 | k,
42 | l {
43 | background-color: var(--bew-theme-color) !important;
44 | }
45 |
46 | // ...
47 | }
48 | // #endregion
49 |
50 | // #region dark mode adaption part
51 | &.dark {
52 | aa,
53 | bb,
54 | cc {
55 | color: var(--bew-text-1);
56 | }
57 |
58 | dd,
59 | ee,
60 | ff {
61 | color: var(--bew-text-1) !important;
62 | }
63 |
64 | // ...
65 | }
66 | // #endregion
67 | }
68 | ```
69 |
70 | ## 何解使用上述之撰寫風格?
71 |
72 | 您可能會對爲什麼應該遵循建議的撰寫風格感到困惑,因此我們在這裏稍作解釋。
73 | 由於這並非以該頁面特有的起始样式所撰寫,而且該頁面已經有了原始樣式,您不能僅僅像這樣寫 CSS `xxx {border: 1px solid white; color: black}`。
74 | 遵循前面的這種撰寫風格使得維持暗模式風格變得困難。這是因爲深色模式主要需要更改字型色彩、背景色彩或框線色彩。
75 |
76 | 根據字型色彩、背景色彩和框線色彩將顏色進行分組是非常高效的,且透過將相應的選取器放在一起以統一方法便於維護。必要時,只需調整相應樣式中的相應選取器即可。
77 |
--------------------------------------------------------------------------------
/src/styles/adaptedStyles/adaptedStyles-jyut.md:
--------------------------------------------------------------------------------
1 | # Adapted Styles
2 |
3 | 喺呢度會擺啲深色模式同埋變更佈景色嘅 CSS
4 |
5 | 喺 `index.ts`,我哋會寫一啲正則表達式令到寫嘅樣式夾返特定嘅頁面。
6 |
7 | ## 樣式檔書寫風格
8 |
9 | ``` scss
10 | .bewly-design.pageName {
11 | // 喺當前嘅頁面進行特別嘅修改,就好似你係噉以執吓個佈局,將嗰啲嘢擺晒落呢度
12 | .right-side-bar .catalog {
13 | line-height: 3em;
14 | }
15 |
16 | // ...
17 |
18 | // #region theme color adaption part
19 | // 用 `:not()` 選取選取唔存在嘅元素愛嚟提高喺呢度入邊嘅優先權
20 | :not(foobar) {
21 | a,
22 | b,
23 | c {
24 | color: var(--bew-theme-color);
25 | }
26 |
27 | d,
28 | e,
29 | f {
30 | // 請注意用 `!important` 係你最後嘅選擇
31 | color: var(--bew-theme-color) !important;
32 | }
33 |
34 | g,
35 | h,
36 | i {
37 | background-color: var(--bew-theme-color);
38 | }
39 |
40 | j,
41 | k,
42 | l {
43 | background-color: var(--bew-theme-color) !important;
44 | }
45 |
46 | // ...
47 | }
48 | // #endregion
49 |
50 | // #region dark mode adaption part
51 | &.dark {
52 | aa,
53 | bb,
54 | cc {
55 | color: var(--bew-text-1);
56 | }
57 |
58 | dd,
59 | ee,
60 | ff {
61 | color: var(--bew-text-1) !important;
62 | }
63 |
64 | // ...
65 | }
66 | // #endregion
67 | }
68 | ```
69 |
70 | ## 點解要用上高嘅書寫風格?
71 |
72 | 你可能唔係幾明點解要跟住建議嘅書寫風格,所以而家我哋慢慢解釋。
73 | 事關你唔係用呢個網頁最初嘅樣式嚟寫,而呢個網頁不溜就已經有咗自己嘅樣式,所以你唔可以直接噉寫 CSS 就好似 `xxx {border: 1px solid white; color: black}` 噉。
74 | 學似啱啱嘅書寫風格後續會勁難維護深色模式。因爲深色模式主要係改文本顏色、背景顏色或者邊框顏色呢啲嚟達到效果。
75 |
76 | 將啲顏色按照字體顏色、背景顏色同埋邊框顏色噉樣分類,再將適合嘅選取器擺埋一齊,用統一嘅處理手法嚟管理,噉樣做會提高效率同埋易啲維護。之後需要執吓佢嗰陣,淨係需要就住相對應嘅樣式風格嚟調整返相對應嘅選取器就得嘞。
77 |
--------------------------------------------------------------------------------
/src/styles/adaptedStyles/adaptedStyles.md:
--------------------------------------------------------------------------------
1 | # Adapted Styles
2 |
3 | Here, place the CSS for dark mode or change the theme color.
4 |
5 | In `index.ts`, we will write some regex rules to match the style used on a specific page.
6 |
7 | ## Style File Writing Style
8 |
9 | ``` scss
10 | .bewly-design.pageName {
11 | // Implement specific modifications to the page, like tweaking the layout, and place those styles here
12 | .right-side-bar .catalog {
13 | line-height: 3em;
14 | }
15 |
16 | // ...
17 |
18 | // #region theme color adaption part
19 | // Increase the priority of the style inside by writing a non-existent selector in `:not()`
20 | :not(foobar) {
21 | a,
22 | b,
23 | c {
24 | color: var(--bew-theme-color);
25 | }
26 |
27 | d,
28 | e,
29 | f {
30 | // PLEASE NOTE THAT USING `!important` SHOULD BE A LAST RESORT
31 | color: var(--bew-theme-color) !important;
32 | }
33 |
34 | g,
35 | h,
36 | i {
37 | background-color: var(--bew-theme-color);
38 | }
39 |
40 | j,
41 | k,
42 | l {
43 | background-color: var(--bew-theme-color) !important;
44 | }
45 |
46 | // ...
47 | }
48 | // #endregion
49 |
50 | // #region dark mode adaption part
51 | &.dark {
52 | aa,
53 | bb,
54 | cc {
55 | color: var(--bew-text-1);
56 | }
57 |
58 | dd,
59 | ee,
60 | ff {
61 | color: var(--bew-text-1) !important;
62 | }
63 |
64 | // ...
65 | }
66 | // #endregion
67 | }
68 | ```
69 |
70 | ## Why use the above writing style?
71 |
72 | You might be confused about why you should follow the suggested writing style, so here we will explain a bit.
73 | Since this isn't written in a pure style specific to the page, and the page already has an original style, you can't simply write CSS like `xxx {border: 1px solid white; color: black}`.
74 | Following this writing style makes it hard to maintain the dark mode style. This is because dark mode primarily requires changes to the text color, background color, or border color.
75 |
76 | It's efficient to group colors according to font color, background color, and border color, and to unify the approach by placing the appropriate selectors together for easy maintenance. When necessary, just adjust the corresponding selectors in the corresponding styles.
77 |
--------------------------------------------------------------------------------
/src/styles/adaptedStyles/common/btn.scss:
--------------------------------------------------------------------------------
1 | .bewly-design {
2 | :not(foobar) {
3 | .btn.primary {
4 | background-color: var(--bew-theme-color) !important;
5 | border-color: var(--bew-theme-color) !important;
6 | }
7 | .btn.primary:focus,
8 | .btn.primary:hover {
9 | background-color: var(--bew-theme-color-80) !important;
10 | border-color: var(--bew-theme-color-80) !important;
11 | }
12 |
13 | .btn.default {
14 | background-color: unset !important;
15 | border-color: var(--bew-border-color) !important;
16 | color: var(--bew-text-1) !important;
17 |
18 | &:focus,
19 | &:hover {
20 | color: var(--bew-theme-color) !important;
21 | background-color: unset !important;
22 | border-color: var(--bew-theme-color) !important;
23 | }
24 | }
25 |
26 | .be-pager-item,
27 | .be-pager-next,
28 | .be-pager-prev {
29 | &:hover {
30 | color: var(--bew-theme-color) !important;
31 | border-color: var(--bew-theme-color);
32 |
33 | a {
34 | color: var(--bew-theme-color) !important;
35 | }
36 | }
37 | }
38 |
39 | .be-pager-item-active {
40 | background-color: var(--bew-theme-color);
41 | border-color: var(--bew-theme-color);
42 | }
43 | }
44 |
45 | &.dark {
46 | // Pagination button
47 | .be-pager-item,
48 | .be-pager-next,
49 | .be-pager-prev,
50 | .space_input {
51 | border-color: var(--bew-border-color);
52 | }
53 |
54 | .be-pager-item,
55 | .be-pager-next,
56 | .be-pager-prev {
57 | background-color: unset;
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/styles/adaptedStyles/common/common.scss:
--------------------------------------------------------------------------------
1 | .bewly-design {
2 | body {
3 | background-color: var(--bew-bg);
4 | }
5 |
6 | .z-top-container {
7 | background-color: transparent;
8 | box-shadow: none;
9 | }
10 |
11 | // #region theme color adaption part
12 | // Increase the priority of the style inside by writing a non-existent selector in `:not()`
13 | :not(foobar) {
14 | .bili-header .bili-header__channel .channel-entry-more__link--current,
15 | .bili-header .bili-header__channel .channel-link--current {
16 | color: var(--bew-theme-color);
17 | }
18 |
19 | .brand_blue {
20 | color: var(--bew-theme-color) !important;
21 | }
22 |
23 | .login-tip {
24 | background: var(--bew-theme-color);
25 | }
26 | }
27 | // #endregion
28 |
29 | // #region dark mode adaption part
30 | &.dark {
31 | .card-loaded,
32 | .van-popover {
33 | background-color: var(--bew-elevated-solid);
34 | }
35 | }
36 | // #endregion
37 | }
38 |
--------------------------------------------------------------------------------
/src/styles/adaptedStyles/common/footer.scss:
--------------------------------------------------------------------------------
1 | .bewly-design {
2 | // #region theme color adaption part
3 | // Increase the priority of the style inside by writing a non-existent selector in :not()
4 | :not(foobar) {
5 | .bili-footer a:hover,
6 | .international-footer a:hover,
7 | .international-footer .partner a:hover,
8 | .international-footer .link-box .link-item.link-c a:hover p {
9 | color: var(--bew-theme-color);
10 | }
11 | }
12 | // #endregion
13 |
14 | // #region dark mode adaption part
15 | &.dark {
16 | .bili-footer a,
17 | .international-footer a,
18 | .international-footer .link-box .link-item.link-c p {
19 | color: var(--bew-text-1);
20 | }
21 |
22 | .bili-footer,
23 | .international-footer .link-box .link-item .bt {
24 | color: var(--bew-text-2);
25 | }
26 |
27 | .international-footer .partner,
28 | .international-footer .partner a {
29 | color: var(--bew-text-3);
30 | }
31 |
32 | .bili-footer .footer-wrp,
33 | .international-footer {
34 | background-color: var(--bew-content-solid);
35 | }
36 |
37 | .bili-footer .boston-postcards li,
38 | .international-footer .link-box .link-item {
39 | border-color: var(--bew-border-color);
40 | }
41 | }
42 | // #endregion
43 | }
44 |
--------------------------------------------------------------------------------
/src/styles/adaptedStyles/common/index.ts:
--------------------------------------------------------------------------------
1 | import './common.scss'
2 | import './comments.scss'
3 | import './topBar.scss'
4 | import './footer.scss'
5 | import './modal.scss'
6 | import './btn.scss'
7 | import './userCard.scss'
8 | import './videoPlayer.scss'
9 | import './loginDialog.scss'
10 |
--------------------------------------------------------------------------------
/src/styles/adaptedStyles/common/modal.scss:
--------------------------------------------------------------------------------
1 | .bewly-design.dark {
2 | .modal-wrapper {
3 | background-color: var(--bew-elevated-solid);
4 | }
5 |
6 | .modal-wrapper .modal-title {
7 | border-color: var(--bew-border-color);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/styles/adaptedStyles/common/topBar.scss:
--------------------------------------------------------------------------------
1 | .large-header {
2 | background-color: transparent !important;
3 | }
4 |
5 | .bewly-design {
6 | // Implement specific modifications to the page, like tweaking the layout, and place those styles here
7 | .bili-header .slide-down,
8 | .bili-header .mini-header {
9 | background-color: var(--bew-elevated) !important;
10 | backdrop-filter: var(--bew-filter-glass-1);
11 | }
12 |
13 | // #region theme color adaption part
14 | // Increase the priority of the style inside by writing a non-existent selector in `:not()`
15 | :not(foobar) {
16 | .bili-header .header-upload-entry {
17 | background-color: var(--bew-theme-color);
18 | }
19 |
20 | .bili-header .header-upload-entry:hover {
21 | background-color: var(--bew-theme-color-80);
22 | }
23 | }
24 | // #endregion
25 |
26 | // #region dark mode adaption part
27 | // &.dark {
28 | // }
29 | // #endregion
30 | }
31 |
--------------------------------------------------------------------------------
/src/styles/adaptedStyles/common/userCard.scss:
--------------------------------------------------------------------------------
1 | .bewly-design {
2 | // #region new user card styles
3 |
4 | // #region theme color adaption part
5 | .bili-user-profile-view__info__button.follow:hover {
6 | background-color: var(--bew-theme-color-80);
7 | }
8 |
9 | .bili-user-profile-view__info__button.follow:hover {
10 | border-color: var(--bew-theme-color-80);
11 | }
12 | // #endregion
13 |
14 | // #endregion
15 |
16 | // #region old user card styles
17 | &.dark {
18 | #id-card {
19 | background-color: var(--bew-elevated-solid);
20 |
21 | .idc-uname[style*="color"],
22 | .idc-uname:hover {
23 | color: var(--bew-theme-color) !important;
24 | }
25 |
26 | .btn.ghost,
27 | .btn.ghost:focus,
28 | .btn.ghost:hover {
29 | background-color: var(--bew-fill-3);
30 | color: var(--bew-text-3);
31 | border-color: var(--bew-fill-3);
32 | }
33 |
34 | .btn-content:hover {
35 | background-color: unset;
36 | }
37 |
38 | .btn.default {
39 | background-color: var(--bew-fill-1);
40 | color: var(--bew-text-1);
41 | border-color: var(--bew-fill-1);
42 |
43 | &:hover {
44 | background-color: unset;
45 | color: var(--bew-theme-color);
46 | border-color: var(--bew-theme-color);
47 | }
48 | }
49 |
50 | .btn.primary {
51 | background-color: var(--bew-theme-color);
52 | border-color: var(--bew-theme-color);
53 |
54 | &:hover {
55 | background-color: var(--bew-theme-color-80);
56 | border-color: var(--bew-theme-color-80);
57 | color: white;
58 | }
59 | }
60 | }
61 | }
62 | // #endregion
63 | }
64 |
--------------------------------------------------------------------------------
/src/styles/adaptedStyles/common/videoPlayer.scss:
--------------------------------------------------------------------------------
1 | .bewly-design {
2 | .bpx-player-toast-row .bpx-player-toast-item {
3 | --bpx-toast-fn-color: var(--bew-theme-color);
4 | --bpx-toast-fn-hover-color: var(--bew-theme-color-80);
5 | }
6 |
7 | // https://github.com/BewlyBewly/BewlyBewly/issues/899
8 | #musicApp {
9 | .container {
10 | width: 336px;
11 | }
12 |
13 | @media (min-width: 1681px) {
14 | .container {
15 | width: 397px;
16 | }
17 | }
18 |
19 | .up a:hover * {
20 | transition: 0.3s;
21 | }
22 | }
23 |
24 | // #region theme color adaption part
25 | // Increase the priority of the style inside by writing a non-existent selector in `:not()`
26 | :not(foobar) {
27 | #musicApp {
28 | .BgmInfo .right .bottom .btn {
29 | color: var(--bew-theme-color);
30 | }
31 |
32 | .card .title:hover,
33 | .h2 .close:hover,
34 | .up a:hover * {
35 | color: var(--bew-theme-color) !important;
36 | }
37 |
38 | .BgmInfo .right .bottom .btn:hover {
39 | color: white;
40 | }
41 |
42 | .BgmInfo .right .bottom .btn:hover {
43 | background-color: var(--bew-theme-color);
44 | }
45 |
46 | .BgmInfo .right .bottom .btn {
47 | background-color: var(--bew-theme-color-10);
48 | }
49 | }
50 | }
51 | // #endregion
52 |
53 | // #region dark mode adaption part
54 | &.dark {
55 | #musicApp {
56 | background-color: var(--bew-elevated-solid) !important;
57 |
58 | .bottom-btn {
59 | background-color: var(--bew-fill-1);
60 | }
61 |
62 | .main {
63 | background-color: var(--bew-fill-1) !important;
64 | }
65 |
66 | .container::after {
67 | background-color: var(--bew-border-color);
68 | }
69 |
70 | .h2 {
71 | background-color: transparent;
72 | }
73 |
74 | .h2 .title svg,
75 | .h2 .title span,
76 | .BgmInfo .right .title,
77 | .main .container .title,
78 | .video-list .title,
79 | .bottom-btn {
80 | color: var(--bew-text-1);
81 | }
82 |
83 | .BgmInfo .right .singer,
84 | .BgmInfo .right .des,
85 | .h2 .close,
86 | .card .up .name .nameText,
87 | .card .up svg {
88 | color: var(--bew-text-2);
89 | }
90 |
91 | .no-more .text {
92 | color: var(--bew-text-3);
93 | }
94 |
95 | .bottom-btn {
96 | border-color: var(--bew-border-color);
97 | }
98 |
99 | .card .up span {
100 | filter: invert(1) hue-rotate(180deg);
101 | mix-blend-mode: screen;
102 | }
103 | }
104 | }
105 | // #endregion
106 | }
107 |
--------------------------------------------------------------------------------
/src/styles/adaptedStyles/forceDark.scss:
--------------------------------------------------------------------------------
1 | html.bewly-design.forceDark.dark {
2 | filter: var(--bew-filter-force-dark) !important;
3 | background-color: white;
4 | }
5 |
6 | .bewly-design.forceDark {
7 | &.dark {
8 | img,
9 | video,
10 | iframe,
11 | [style*="background-image: url"],
12 | [style*="background: url"] {
13 | filter: var(--bew-filter-force-dark);
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/styles/adaptedStyles/pages/accountSettingsPage.scss:
--------------------------------------------------------------------------------
1 | .bewly-design.accountSettingsPage {
2 | // #region theme color adaption part
3 | // Increase the priority of the style inside by writing a non-existent selector in `:not()`
4 | :not(foobar) {
5 | // do nothing..
6 | }
7 | // #endregion
8 |
9 | // #region dark mode adaption part
10 | &.dark {
11 | .top-img,
12 | .top_bg {
13 | filter: invert(1) hue-rotate(180deg);
14 | }
15 |
16 | .security_content {
17 | filter: invert(0.98) hue-rotate(180deg) brightness(1.1);
18 |
19 | img,
20 | video,
21 | iframe,
22 | [style*="background-image: url"],
23 | [style*="background: url"],
24 | .invitation-img,
25 | .no-level-img,
26 | .points-header-warp {
27 | filter: invert(0.98) hue-rotate(180deg) brightness(1.1);
28 | }
29 | }
30 |
31 | .mp-img {
32 | background-color: black;
33 | }
34 |
35 | .xts,
36 | .table-normal *,
37 | .record-login-descript,
38 | .black-btn:not(:hover) {
39 | color: black;
40 | }
41 |
42 | .official-title,
43 | .title-module,
44 | .professional-wrapper .title,
45 | .condition-txt,
46 | .el-form-item__label,
47 | .professional-form-wrapper .official-label,
48 | .el-input__inner,
49 | .professional-form-wrapper .pro-type .el-radio__label,
50 | .el-textarea__inner,
51 | .el-checkbox,
52 | .ava-name {
53 | color: var(--bew-text-1);
54 | }
55 |
56 | .professional-wrapper .desc,
57 | .upload-txt,
58 | .professional-form-wrapper .desc,
59 | .ava-text {
60 | color: var(--bew-text-2);
61 | }
62 |
63 | .professional-wrapper,
64 | .el-input__inner,
65 | .el-textarea__inner,
66 | .professional-form-wrapper .pro-preview-content {
67 | background-color: var(--bew-content-solid);
68 | }
69 |
70 | .per-left-line,
71 | .per-right-line,
72 | .organ-left-line,
73 | .organ-right-line {
74 | background: var(--bew-border-color);
75 | }
76 |
77 | .el-input__inner:not(:focus),
78 | .el-textarea__inner:not(:focus) {
79 | border-color: var(--bew-border-color);
80 | }
81 | }
82 | // #endregion
83 | }
84 |
--------------------------------------------------------------------------------
/src/styles/adaptedStyles/pages/channelPage.scss:
--------------------------------------------------------------------------------
1 | .bewly-design.channelPage {
2 | .banner-wrapper {
3 | background-color: black;
4 | margin-bottom: 0 !important;
5 | padding-bottom: 20px !important;
6 | }
7 |
8 | .index-module {
9 | padding-bottom: 20px;
10 | background: linear-gradient(0deg, #111319 40px, rgba(17, 19, 25, 0.8) 60.12%, rgba(20, 20, 20, 0.0001));
11 | }
12 |
13 | .promotion-c,
14 | .link-c .link,
15 | .section-title .subTitle {
16 | background-color: var(--bew-fill-1);
17 | }
18 |
19 | .hover-c .title,
20 | :not(.index-module) > .inner-c .title,
21 | .link-c .link,
22 | .section-title .subTitle {
23 | color: var(--bew-text-1);
24 | }
25 |
26 | .banner-wrapper .side-list .side-item .title,
27 | .section-title .subTitle.highlight {
28 | color: white;
29 | }
30 |
31 | .link-c .link {
32 | border-color: var(--bew-border-color);
33 | }
34 |
35 | // #region theme color adaption part
36 | // Increase the priority of the style inside by writing a non-existent selector in :not()
37 | :not(fjdslfds) {
38 | .channel-swiper .channel-carousel-tool .channel-nav-click li.active,
39 | .hot-module .left .goto,
40 | .switch-c.checked,
41 | .section-title .subTitle.highlight {
42 | background-color: var(--bew-theme-color);
43 | }
44 |
45 | .inner-c .play-icon.button,
46 | .hover-c .play-icon.button,
47 | .hot-module .left .goto:hover {
48 | background-color: var(--bew-theme-color-80);
49 | }
50 |
51 | .index-module .all-a:hover .all,
52 | .index-module .all-a:hover .arrow-all,
53 | .index-module .sub-m:hover,
54 | .hot-module .hot-item:hover .title,
55 | .promotion-c:hover .title,
56 | .hover-c:hover .title,
57 | .switch-c .round {
58 | color: var(--bew-theme-color);
59 | }
60 | }
61 | // #endregion
62 |
63 | // #region dark mode adaption part
64 | &.dark {
65 | .channel-swiper .channel-carousel-tool .channel-nav-click li {
66 | background-color: var(--bew-fill-1);
67 | }
68 |
69 | .channel-container .el-input__inner {
70 | box-shadow: 0 0 0 1px var(--bew-border-color) inset;
71 | }
72 |
73 | .season-cover {
74 | background-color: transparent;
75 | }
76 | }
77 | // #endregion
78 | }
79 |
--------------------------------------------------------------------------------
/src/styles/adaptedStyles/pages/creativeCenterPage.scss:
--------------------------------------------------------------------------------
1 | .bewly-design.creativeCenterPage {
2 | // #region theme color adaption part
3 | // Increase the priority of the style inside by writing a non-existent selector in `:not()`
4 | // :not(foobar) {
5 | // }
6 | // #endregion
7 |
8 | // #region dark mode adaption part
9 | &.dark {
10 | #appeal-main .err-wrapper.err-no-list,
11 | .loading_wrap .loading .animate,
12 | micro-app[name="allowance-excitation"] .loading > .loading-img,
13 | micro-app[name="template-incentive"] .header-bg,
14 | micro-app[name="upower-manage"] .rights .level-right:after,
15 | micro-app[name="video-up"] .video-complete .bg,
16 | .article-card.v2 .cover-wrp .duration,
17 | .bmc-video-card .video-preview__duration,
18 | .setting .watermark-setting .watermark-wrp .watermark-position-wrp,
19 | .chief-recommend-module .carousel-box .carousel-module .panel .title,
20 | micro-app[name="video-up"] .cover-editor-panel-image,
21 | micro-app[name="video-up"] .cover-upload-mask-btn,
22 | micro-app[name="video-up"] .color-picker-color .list,
23 | micro-app[name="video-up"] .tool-bar-text .btn-color,
24 | // Bilibili sidebar
25 | .be-settings > .sidebar {
26 | filter: var(--bew-filter-force-dark);
27 | }
28 |
29 | .nav-item.message iframe,
30 | .setting .watermark-setting .watermark-wrp .watermark-position-wrp .preview-img,
31 | micro-app[name="video-up"] .cover-editor-sidebar-item-icon {
32 | filter: none !important;
33 | }
34 |
35 | .setting-card-switch,
36 | .note-setting-wrp,
37 | .bcc-select-input-inner,
38 | .ep-table .ep-table-head-tr .ep-table-tr-item,
39 | .ep-table .ep-table-tr-item,
40 | .ep-section-edit-video-list-item-tip,
41 | .video-title-edit > input {
42 | color: black;
43 | }
44 |
45 | .bcc-search-input,
46 | .ep-text-input label input,
47 | .ep-text-area label textarea {
48 | background-color: unset;
49 | }
50 |
51 | .ep-text-input {
52 | background: unset;
53 | }
54 | }
55 | // #endregion
56 | }
57 |
--------------------------------------------------------------------------------
/src/styles/adaptedStyles/pages/error404Page.scss:
--------------------------------------------------------------------------------
1 | .bewly-design.error404Page {
2 | .error-manga img {
3 | background-color: white;
4 | border-top: 20px solid white;
5 | border-bottom: 20px solid white;
6 | border-radius: 4px;
7 | }
8 |
9 | body {
10 | background-color: var(--bew-homepage-bg);
11 | }
12 |
13 | // #region theme color adaption part
14 | // Increase the priority of the style inside by writing a non-existent selector in :not()
15 | :not(foobar) {
16 | .error-panel .rollback-btn,
17 | .error-manga .change-img-btn {
18 | background-color: var(--bew-theme-color);
19 | }
20 |
21 | .error-panel .rollback-btn:hover,
22 | .error-manga .change-img-btn:hover {
23 | background-color: var(--bew-theme-color-80);
24 | }
25 | }
26 | // #endregion
27 |
28 | // #region dark mode adaption part
29 | &.dark {
30 | .error-container {
31 | background-color: var(--bew-content-solid);
32 | }
33 | }
34 | // #endregion
35 | }
36 |
--------------------------------------------------------------------------------
/src/styles/adaptedStyles/pages/homePage.scss:
--------------------------------------------------------------------------------
1 | .bewly-design.homePage {
2 | .header-channel {
3 | // margin-top: 10px;
4 | // display: none;
5 | }
6 |
7 | // fix when the container does not fill the recommended container in large screen situations
8 | .recommended-container_floor-aside .container {
9 | max-width: 100% !important;
10 | }
11 |
12 | // adjust the fixed header in original bilibili homepage
13 | .header-channel {
14 | background-color: transparent;
15 | backdrop-filter: var(--bew-filter-glass-1);
16 | background: var(--bew-elevated);
17 | // border-radius: var(--bew-radius);
18 | // margin: 10px 20px;
19 | // width: calc(100% - 40px);
20 | box-shadow: var(--bew-shadow-2), var(--bew-shadow-edge-glow-1);
21 | }
22 |
23 | .header-channel-fixed-right-item {
24 | background-color: var(--bew-fill-1);
25 | border-color: var(--bew-border-color);
26 |
27 | &:hover {
28 | background-color: var(--bew-fill-2);
29 | }
30 | }
31 |
32 | .bili-live-card .bili-live-card__info--living img,
33 | .single-card.floor-card .living > img {
34 | filter: var(--bew-filter-icon-glow);
35 | }
36 |
37 | &.dark {
38 | .floor-card .floor-title {
39 | color: var(--bew-text-1);
40 | }
41 |
42 | .single-card.floor-card .badge {
43 | background-color: var(--bew-content-solid);
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/styles/adaptedStyles/pages/loginPage.scss:
--------------------------------------------------------------------------------
1 | .bewly-design.loginPage {
2 | .login-pwd,
3 | .tab-sms {
4 | .tab__form,
5 | .btn_wp {
6 | width: 420px;
7 | }
8 | }
9 |
10 | .tab-sms {
11 | .btn_wp {
12 | .btn_primary {
13 | width: 420px !important;
14 | }
15 | }
16 | }
17 |
18 | // #region theme color adaption part
19 | // Increase the priority of the style inside by writing a non-existent selector in :not()
20 | :not(foobar) {
21 | .clickable,
22 | .area-code-select .checked {
23 | color: var(--bew-theme-color);
24 | }
25 |
26 | .tabs_wp .tab_active {
27 | color: var(--bew-theme-color) !important;
28 | }
29 |
30 | .btn_primary {
31 | background-color: var(--bew-theme-color);
32 | }
33 |
34 | .btn_primary.disabled {
35 | background-color: var(--bew-theme-color-60) !important;
36 | }
37 |
38 | .eye-btn:hover svg path {
39 | fill: var(--bew-theme-color) !important;
40 | }
41 | }
42 | // #endregion
43 |
44 | // #region dark mode adaption part
45 | &.dark {
46 | #app {
47 | background-color: var(--bew-bg);
48 | }
49 |
50 | .main__middle-line,
51 | .tabs_wp div:nth-child(2n) {
52 | background-color: var(--bew-border-color);
53 | }
54 |
55 | .login-scan__qrcode {
56 | background-color: white;
57 | }
58 |
59 | .btn_other,
60 | .main__right .tab__form {
61 | background-color: var(--bew-fill-1);
62 | }
63 |
64 | .area-code-select .option:hover {
65 | background-color: var(--bew-fill-2);
66 | }
67 |
68 | .area-code-select {
69 | background-color: var(--bew-elevated-solid);
70 | }
71 |
72 | .main__right .tab__form,
73 | .main__right .tab__form .form__separator-line,
74 | .btn_other,
75 | .tab-sms__vertical-line,
76 | .area-code-select {
77 | border-color: var(--bew-border-color);
78 | }
79 |
80 | .btn_other {
81 | color: var(--bew-text-1);
82 | }
83 |
84 | .login-scan__txt p,
85 | .third-party-login-wrapper .title,
86 | .tabs_wp div {
87 | color: var(--bew-text-2);
88 | }
89 |
90 | .login-protocol {
91 | color: var(--bew-text-3);
92 | }
93 |
94 | .top-header {
95 | filter: invert(1) hue-rotate(180deg);
96 | }
97 | }
98 | // #endregion
99 | }
100 |
--------------------------------------------------------------------------------
/src/styles/adaptedStyles/pages/premiumPage.scss:
--------------------------------------------------------------------------------
1 | .premiumPage {
2 | #biliMainHeader {
3 | height: unset;
4 | }
5 |
6 | &.bewly-design {
7 | --brand_pink: var(--Pi5);
8 |
9 | .home-banner-wrapper .banner-hover-group {
10 | background: linear-gradient(
11 | 180deg,
12 | rgba(var(--bg1_rgb), 0),
13 | rgba(var(--bg1_rgb), 0.07) 11.79%,
14 | rgba(var(--bg1_rgb), 0.08) 21.38%,
15 | rgba(var(--bg1_rgb), 0.0704) 29.12%,
16 | rgba(var(--bg1_rgb), 0.120652) 35.34%,
17 | rgba(var(--bg1_rgb), 0.181481) 40.37%,
18 | rgba(var(--bg1_rgb), 0.2512) 44.56%,
19 | rgba(var(--bg1_rgb), 0.328119) 48.24%,
20 | rgba(var(--bg1_rgb), 0.410548) 51.76%,
21 | rgba(var(--bg1_rgb), 0.4968) 55.44%,
22 | rgba(var(--bg1_rgb), 0.585185) 59.63%,
23 | rgba(var(--bg1_rgb), 0.674015) 64.66%,
24 | rgba(var(--bg1_rgb), 0.7616) 70.88%,
25 | rgba(var(--bg1_rgb), 0.846252) 78.62%,
26 | rgba(var(--bg1_rgb), 0.926281) 88.21%,
27 | var(--bg1)
28 | );
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/styles/adaptedStyles/pages/searchPage.scss:
--------------------------------------------------------------------------------
1 | .bewly-design.searchPage {
2 | body {
3 | background-color: var(--bew-bg) !important;
4 | }
5 |
6 | .search-input-container .search-fixed-header {
7 | background-color: var(--bew-elevated);
8 | backdrop-filter: var(--bew-filter-glass-1);
9 | }
10 |
11 | .search-layout {
12 | .bili-video-card .bili-video-card__wrap {
13 | transition:
14 | box-shadow 0.3s ease-in-out,
15 | background 0.3s ease-in-out;
16 | border-radius: var(--bew-radius-half);
17 | }
18 |
19 | .bili-video-card:hover .bili-video-card__wrap {
20 | background-color: var(--bew-fill-2);
21 | box-shadow: 0 0 0 8px var(--bew-fill-2);
22 | }
23 |
24 | .bili-video-card:hover .bili-video-card__info--tit {
25 | transition: color 0.3s ease-in-out;
26 | }
27 | }
28 |
29 | // #region theme color adaption part
30 | // Increase the priority of the style inside by writing a non-existent selector in `:not()`
31 | :not(foobar) {
32 | .search-layout {
33 | .keyword,
34 | .search-input-wrap .suggest_high_light {
35 | color: var(--bew-theme-color);
36 | }
37 |
38 | .media-footer-badge {
39 | background-color: var(--bew-theme-color) !important;
40 | }
41 |
42 | .search-loading-container .loading-text .loading-gif,
43 | .bili-live-card .bili-live-card__info--living img,
44 | .bili-video-card .bili-video-card__info--living img {
45 | filter: var(--bew-filter-icon-glow);
46 | }
47 | }
48 | }
49 | // #endregion
50 | }
51 |
--------------------------------------------------------------------------------
/src/styles/adaptedStyles/pages/topicPage.scss:
--------------------------------------------------------------------------------
1 | .bewly-design.topicPage {
2 | // #region theme color adaption part
3 | // Increase the priority of the style inside by writing a non-existent selector in `:not()`
4 | :not(foobar) {
5 | .topic-nav button {
6 | background-color: var(--bew-theme-color);
7 | }
8 |
9 | .bili-topic-selector__bulletin__clear {
10 | background-color: var(--bew-theme-color-20);
11 | }
12 |
13 | .topic-detail__header {
14 | background: linear-gradient(var(--bew-theme-color-10), transparent) !important;
15 | }
16 |
17 | .launch-user__name:hover,
18 | .bili-dyn-interaction__item__desc .bili-rich-text-module:hover {
19 | color: var(--bew-theme-color);
20 | }
21 |
22 | .topic-nav span:before {
23 | filter: var(--bew-filter-icon-glow);
24 | }
25 |
26 | .share-popover .share-popover__qrcode canvas {
27 | box-shadow: 0 0 0 2px white;
28 | }
29 |
30 | .topic-tv-icon svg path {
31 | fill: var(--bew-theme-color);
32 | }
33 | }
34 | // #endregion
35 |
36 | // #region dark mode adaption part
37 | &.dark {
38 | .topic-detail {
39 | --bg3: var(--bew-bg);
40 | --bg1: var(--bew-content-solid);
41 | }
42 |
43 | .active-card {
44 | background-color: var(--bew-content-solid);
45 | }
46 |
47 | .active-card .active-content__desc {
48 | color: var(--bew-text-1);
49 | }
50 |
51 | .action-item__data {
52 | color: var(--bew-text-2);
53 | }
54 |
55 | .action-item__icon {
56 | filter: invert(1) hue-rotate(180deg);
57 | }
58 | }
59 | // #endregion
60 | }
61 |
--------------------------------------------------------------------------------
/src/styles/adaptedStyles/pages/watchLaterPage.scss:
--------------------------------------------------------------------------------
1 | .bewly-design.watchLaterPage {
2 | // #region new watch later page
3 |
4 | .watchlater-list-nav.fixed {
5 | background-color: var(--bew-elevated);
6 | backdrop-filter: var(--bew-filter-glass-1);
7 | }
8 |
9 | .watchlater-list-nav .breadcrums-nav {
10 | background-color: transparent;
11 | }
12 |
13 | // #endregion
14 |
15 | // #region old watch later page
16 |
17 | // #region theme color adaption part
18 | // Increase the priority of the style inside by writing a non-existent selector in :not()
19 | :not(foobar) {
20 | .watch-later-list header .s-btn,
21 | .bili-dialog .con .btn-box .btn-submit,
22 | .bili-dialog .con .btn-box .btn-cancel:hover {
23 | border-color: var(--bew-theme-color);
24 | }
25 |
26 | .bili-dialog .con .btn-box .btn-submit:hover {
27 | border-color: var(--bew-theme-color-80);
28 | }
29 |
30 | .watch-later-list header .s-btn,
31 | .watch-later-list .list-box .av-item .av-about .t:hover,
32 | .watch-later-list .list-box .av-item .av-about .info .user:hover,
33 | .bili-dialog .con .btn-box .btn-cancel:hover {
34 | color: var(--bew-theme-color);
35 | }
36 |
37 | .watch-later-list header .s-btn:hover {
38 | color: white;
39 | }
40 |
41 | .watch-later-list header .s-btn:hover,
42 | .bili-dialog .con .btn-box .btn-submit {
43 | background-color: var(--bew-theme-color);
44 | }
45 |
46 | .bili-dialog .con .btn-box .btn-submit:hover {
47 | background-color: var(--bew-theme-color-80);
48 | }
49 |
50 | .bili-dialog .con .btn-box .btn-cancel {
51 | background-color: transparent;
52 | }
53 | }
54 | // #endregion
55 |
56 | // #region dark mode adaption part
57 | &.dark {
58 | .watch-later-list .list-box .av-item .key,
59 | .watch-later-list .list-box .av-item .av-about .t,
60 | .bili-dialog .con header,
61 | .bili-dialog .con .txt {
62 | color: var(--bew-text-1);
63 | }
64 |
65 | .watch-later-list .list-box .av-item .av-about .info .user,
66 | .bili-dialog .con .btn-box .btn-cancel {
67 | color: var(--bew-text-2);
68 | }
69 |
70 | .watch-later-list header .d-btn {
71 | color: var(--bew-text-3);
72 | }
73 |
74 | .watch-later-list .list-box .av-item .av-about,
75 | .bili-dialog .con header,
76 | .bili-dialog .con .btn-box .btn-cancel,
77 | .watch-later-list header .d-btn {
78 | border-color: var(--bew-border-color);
79 | }
80 |
81 | .watch-later-list header .s-btn,
82 | .watch-later-list header .d-btn {
83 | background-color: transparent;
84 | }
85 |
86 | .bili-dialog .con {
87 | background-color: var(--bew-elevated-solid);
88 | }
89 | }
90 | // #endregion
91 |
92 | // #endregion
93 | }
94 |
--------------------------------------------------------------------------------
/src/styles/adaptedStyles/shadowDom/comments.scss:
--------------------------------------------------------------------------------
1 | // :host(bili-comments-vote-card) {
2 | // // https://github.com/BewlyBewly/BewlyBewly/issues/998
3 | // // 這裏好像怎麼調都是用淺色狀態下文字顏色
4 | // .option-info {
5 | // color: var(--bew-text-1);
6 | // }
7 | // }
8 |
9 | :host(bili-comment-user-info) {
10 | #user-name a[style*="color"] {
11 | color: var(--bew-theme-color) !important;
12 | }
13 | }
14 |
15 | :host(bili-comment-renderer) {
16 | #body.dark .tag[style] {
17 | --bili-comment-tag-color: var(--bew-text-2);
18 | --bili-comment-tag-bg: var(--bew-fill-1);
19 | }
20 | }
21 |
22 | :host(bili-rich-text) {
23 | --bili-rich-text-link-color: var(--bew-theme-color) !important;
24 | --bili-rich-text-link-color-hover: var(--bew-theme-color-80) !important;
25 |
26 | #contents img[src*="https://i0.hdslb.com/bfs/activity-plat/static/20201110/4c8b2dbaded282e67c9a31daa4297c3c/AeQJlYP7e.png"],
27 | #contents img[src*="https://i0.hdslb.com/bfs/reply/9f3ad0659e84c96a711b88dd33f4bc2e945045e0.png"]
28 | {
29 | filter: var(--bew-filter-icon-glow);
30 | }
31 | }
32 |
33 | :host(bili-comment-box) {
34 | #pub button:not(:hover, :active, .active) {
35 | background-color: var(--bew-theme-color-60);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/styles/adaptedStyles/shadowDom/index.ts:
--------------------------------------------------------------------------------
1 | import './comments.scss'
2 | import './userProfile.scss'
3 |
--------------------------------------------------------------------------------
/src/styles/adaptedStyles/shadowDom/userProfile.scss:
--------------------------------------------------------------------------------
1 | :host(bili-user-profile) {
2 | #action button#follow:hover {
3 | background-color: var(--bew-theme-color-80);
4 | border-color: var(--bew-theme-color-80);
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/styles/adaptedStyles/thirdParties/bilibiliEnhanceVideoList.scss:
--------------------------------------------------------------------------------
1 | // B站|bilibili 分P视频详情页优化
2 | // https://greasyfork.org/zh-CN/scripts/490676-b站-bilibili-分p视频详情页优化
3 |
4 | .bewly-design.videoPage {
5 | .video-pod__list {
6 | .title-txt {
7 | transition: color 0.2s ease-in-out;
8 | }
9 |
10 | .simple-base-item.normal .title-txt {
11 | color: var(--bew-text-1) !important;
12 | }
13 |
14 | .simple-base-item.active .title-txt,
15 | .simple-base-item.normal:hover .title-txt {
16 | color: var(--bew-theme-color) !important;
17 | }
18 |
19 | .simple-base-item.normal:hover {
20 | background-color: var(--Ga1_s) !important;
21 | }
22 |
23 | .simple-base-item.active:hover {
24 | background-color: var(--Wh0) !important;
25 | }
26 | }
27 |
28 | // #region dark mode adaption part
29 | &.dark {
30 | .video-pod__list {
31 | .simple-base-item.normal .title-txt {
32 | color: var(--bew-text-1) !important;
33 | }
34 |
35 | .simple-base-item.active .title-txt,
36 | .simple-base-item.normal:hover .title-txt {
37 | color: var(--bew-theme-color) !important;
38 | }
39 |
40 | .pod-item.active:hover,
41 | .simple-base-item.active:hover {
42 | background-color: var(--bew-bg) !important;
43 | }
44 | }
45 | }
46 | // #endregion
47 | }
48 |
--------------------------------------------------------------------------------
/src/styles/adaptedStyles/thirdParties/bilibiliEvolved.scss:
--------------------------------------------------------------------------------
1 | // Bilibili Evolved
2 | // https://github.com/the1812/Bilibili-Evolved
3 |
4 | .bewly-design {
5 | // Adapt the colors of the two Bilibili Evolved's buttons on the left side to BewlyBewly theme
6 | .be-settings > .sidebar > * {
7 | background-color: var(--bew-elevated-solid) !important;
8 | opacity: 0.4;
9 |
10 | &:hover {
11 | opacity: 1;
12 | }
13 | }
14 |
15 | .be-settings > .sidebar > * .be-icon {
16 | color: var(--bew-text-1) !important;
17 | fill: var(--bew-text-1) !important;
18 | }
19 |
20 | &.dark {
21 | // Bilibili Evolved's top bar
22 | .custom-navbar.blur:not(.transparent, .fill) {
23 | --navbar-background: var(--bew-elevated);
24 | --navbar-foreground: var(--bew-text-1);
25 | }
26 |
27 | .custom-navbar:not(.transparent, .blur, .fill) {
28 | --navbar-background: var(--bew-elevated-solid);
29 | --navbar-foreground: var(--bew-text-1);
30 | }
31 |
32 | // 動態過濾
33 | .feeds-filter {
34 | background-color: var(--bew-content-solid);
35 | }
36 |
37 | // 評論區 -> 收起評論
38 | .bb-comment .fold-comment,
39 | .bili-comment-container .fold-comment {
40 | background-color: var(--bew-content-solid);
41 | color: var(--bew-text-2);
42 |
43 | &:hover {
44 | background-color: var(--bew-content-solid-hover);
45 | }
46 | }
47 |
48 | // 直播信息擴充
49 | .be-live-list {
50 | background-color: var(--bew-content-solid);
51 | color: var(--bew-text-1);
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/styles/adaptedStyles/thirdParties/index.ts:
--------------------------------------------------------------------------------
1 | import './bilibiliEvolved.scss'
2 | import './bilibiliEnhanceVideoList.scss'
3 |
--------------------------------------------------------------------------------
/src/styles/blockAds.scss:
--------------------------------------------------------------------------------
1 | // Do not use the "ads" keyword. AdGuard, AdBlock, and some ad-blocking extensions will
2 | // detect and remove it when the class name contains "ads"
3 | .block-useless-contents {
4 | // 原版首頁最右則推介內容
5 | .floor-single-card,
6 | // 首頁不能使用不感興趣的影片都當廣告殺了
7 | .feed-card:has(.bili-video-card.is-rcmd:not(.enable-no-interest)),
8 | .bili-video-card.is-rcmd:not(.enable-no-interest),
9 | .ad-report,
10 | .brand-ad-list,
11 | // 视频页游戏卡片
12 | .video-page-game-card-small,
13 | // 大家围观的直播
14 | .pop-live-small-mode,
15 | // 视频页面的侧栏
16 | .slide-ad-exp,
17 | .video-card-ad-small,
18 | // 动态页的广告
19 | .bili-dyn-ads {
20 | display: none !important;
21 | }
22 | // 主頁右下廣告卡片
23 | .adcard,
24 | // 主頁下載桌面版提示
25 | .desktop-download-tip {
26 | display: none !important;
27 | }
28 |
29 | // 主頁推介頂部卡片間距調整
30 | .recommended-container_floor-aside .container > *:nth-of-type(n + 8) {
31 | margin-top: 0px !important;
32 | margin-bottom: 24px;
33 | }
34 | }
35 |
36 | // 检测到您的页面展示可能受到浏览器插件影响,建议您将当前页面加入插件白名单,以保障您的浏览体验~
37 | .adblock-tips {
38 | display: none !important;
39 | }
40 |
--------------------------------------------------------------------------------
/src/styles/fonts.scss:
--------------------------------------------------------------------------------
1 | /** font-family 選用字型設定感謝 胡雨晴 hafterain (https://github.com/hafterain) 提供幫助 ❤️❤️❤️ */
2 |
3 | :root,
4 | :host {
5 | --bew-fonts-basic: CJKEmDash, Numbers, Onest, ShangguSansSCVF, -apple-system, BlinkMacSystemFont, InterVariable, Inter,
6 | "Segoe UI", Cantarell, "Noto Sans", "Roboto Flex", Roboto;
7 | --bew-fonts-fallback: sans-serif, ui-sans-serif, system-ui, "Apple Color Emoji", "Twemoji Mozilla", "Noto Color Emoji",
8 | "Segoe UI Emoji", "Segoe UI Symbol", emoji;
9 |
10 | --bew-fonts: bilifont, var(--bew-custom-fonts, var(--bew-fonts-basic), var(--bew-fonts-fallback));
11 | }
12 |
13 | .bewly-wrapper,
14 | .bewly-design {
15 | code,
16 | kbd,
17 | samp,
18 | pre {
19 | font-family: "JetBrains Mono", "Fira Code", "Fira Mono", "Cascadia Code", "Cascadia Mono", ui-monospace,
20 | SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
21 | }
22 | }
23 |
24 | .modify-fonts .bewly-wrapper,
25 | .bewly-design.modify-fonts,
26 | // Do not change the danmaku font because the Bilibili video player can change the font of danmaku
27 | .bewly-design.modify-fonts *:not(.bili-danmaku-x-dm),
28 | // 热搜点评
29 | .bewly-design.modify-fonts .base-video-sections-v1 .video-sections-head_first-line .first-line-left .first-line-title,
30 | // 接下来播放
31 | .bewly-design.modify-fonts .recommend-list-v1 .rec-title,
32 | // Font for the recommended video list on the right side of the video https://github.com/BewlyBewly/BewlyBewly/issues/1061
33 | .bewly-design.modify-fonts .card-box .info .title,
34 | // Search page video card title https://search.bilibili.com/all?keyword=test
35 | .bewly-design.modify-fonts .bili-video-card .bili-video-card__info--tit {
36 | font-family: var(--bew-fonts);
37 | }
38 |
--------------------------------------------------------------------------------
/src/styles/index.ts:
--------------------------------------------------------------------------------
1 | import './variables.scss'
2 | import './main.scss'
3 | import './adaptedStyles'
4 | import './transitionAndTransitionGroup.scss'
5 | import './blockAds.scss'
6 | import './removeTopBar.scss'
7 | import './fonts.scss'
8 |
--------------------------------------------------------------------------------
/src/styles/removeTopBar.scss:
--------------------------------------------------------------------------------
1 | .remove-top-bar {
2 | // remove the original top bar and adjust the height of the top bar to match the bewly top bar
3 | .bili-header .bili-header__bar,
4 | #internationalHeader,
5 | .link-navbar,
6 | #home_nav,
7 | // only hide the top bar on the home page
8 | .homePage #biliMainHeader,
9 | #bili-header-container {
10 | visibility: hidden;
11 | height: var(--bew-top-bar-height) !important;
12 | }
13 |
14 | // Remove the Bilibili Evolved's top bar
15 | .custom-navbar {
16 | display: none;
17 | }
18 |
19 | // some pages have a white bar at the top; changing the top margin fixes this problem
20 | .banner-wrapper,
21 | .home-banner-wrapper {
22 | margin-top: calc(-1 * var(--bew-top-bar-height)) !important;
23 | }
24 | }
25 |
26 | .remove-top-bar-without-placeholder {
27 | #biliMainHeader {
28 | display: none;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/styles/transitionAndTransitionGroup.scss:
--------------------------------------------------------------------------------
1 | .page-fade-enter-active,
2 | .page-fade-leave-active {
3 | --uno: "transition-opacity duration-300 transform-gpu";
4 | }
5 | .page-fade-enter-from,
6 | .page-fade-leave-to {
7 | --uno: "opacity-0";
8 | }
9 | .page-fade-leave-to {
10 | --uno: "!hidden";
11 | }
12 |
13 | .fade-enter-active,
14 | .fade-leave-active {
15 | --uno: "transition-opacity duration-300";
16 | }
17 | .fade-enter-from,
18 | .fade-leave-to {
19 | --uno: "opacity-0";
20 | }
21 |
22 | .slide-fade-enter-active,
23 | .slide-fade-leave-active {
24 | --uno: "opacity-100 transition-all duration-500";
25 | }
26 | .slide-fade-enter,
27 | .slide-fade-leave-to {
28 | --uno: "opacity-0";
29 | }
30 |
31 | .list-enter-active,
32 | .list-leave-active {
33 | --uno: "transition-all duration-500";
34 | }
35 | .list-enter-from,
36 | .list-leave-to {
37 | --uno: "opacity-0 translate-y-6 transform-gpu";
38 | }
39 | .list-leave-to {
40 | --uno: "hidden";
41 | }
42 |
43 | .modal-enter-active,
44 | .modal-leave-active {
45 | --uno: "transition-all duration-500 transform-gpu";
46 | }
47 | .modal-enter-from,
48 | .modal-leave-to {
49 | --uno: "opacity-0 scale-105";
50 | }
51 |
52 | .dropdown-enter-active,
53 | .dropdown-leave-active {
54 | --uno: "transition-all duration-300 transform-gpu";
55 | }
56 | .dropdown-enter-from,
57 | .dropdown-leave-to {
58 | --uno: "opacity-0 transform-gpu scale-95 -translate-y-4 filter blur-sm";
59 | }
60 |
--------------------------------------------------------------------------------
/src/tests/demo.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest'
2 |
3 | describe('demo', () => {
4 | it('should work', () => {
5 | expect(1 + 1).toBe(2)
6 | })
7 | })
8 |
--------------------------------------------------------------------------------
/src/utils/api.ts:
--------------------------------------------------------------------------------
1 | import type { API_COLLECTION } from '~/background/messageListeners/api'
2 |
3 | type CamelCase = S extends `${infer P1}_${infer P2}${infer P3}`
4 | ? `${Lowercase}${Uppercase}${CamelCase}`
5 | : Lowercase
6 |
7 | type APIFunction = {
8 | [K in keyof T as CamelCase]: {
9 | // @ts-expect-error allow params
10 | [P in keyof T[K]]: T[K][P] extends Function ? T[K][P] : Lowercase extends 'get' ? (options?: Partial) => Promise : (options?: Partial) => Promise
11 | }
12 | }
13 |
14 | // eslint-disable-next-line ts/no-unsafe-declaration-merging
15 | export interface APIClient extends APIFunction {
16 |
17 | }
18 |
19 | // eslint-disable-next-line ts/no-unsafe-declaration-merging
20 | export class APIClient {
21 | private readonly cache = new Map()
22 |
23 | constructor() {
24 | // @ts-expect-error ignore
25 | return new Proxy({}, {
26 | get: (_, namespace) => { // namespace
27 | if (this.cache.has(namespace)) {
28 | return this.cache.get(namespace)
29 | }
30 | else {
31 | const api = new Proxy({}, {
32 | get(_, p) {
33 | return (options?: object) => {
34 | return browser.runtime.sendMessage({
35 | contentScriptQuery: p,
36 | ...options,
37 | })
38 | }
39 | },
40 | })
41 | this.cache.set(namespace, api)
42 | return api
43 | }
44 | },
45 | })
46 | }
47 | }
48 |
49 | const api = new APIClient()
50 |
51 | export default api
52 |
--------------------------------------------------------------------------------
/src/utils/appSign.ts:
--------------------------------------------------------------------------------
1 | import md5 from 'md5'
2 |
3 | /**
4 | * 为请求参数进行 APP 签名
5 | * https://socialsisteryi.github.io/bilibili-API-collect/docs/misc/sign/APP.html#typescript-javascript
6 | */
7 | type Params = Record
8 |
9 | export function appSign(params: Params, appkey: string, appsec: string): string {
10 | params.appkey = appkey
11 | const searchParams = new URLSearchParams(params)
12 | searchParams.sort()
13 | return md5(searchParams.toString() + appsec)
14 | }
15 |
--------------------------------------------------------------------------------
/src/utils/authProvider.ts:
--------------------------------------------------------------------------------
1 | // import browser from 'webextension-polyfill'
2 | import { accessKey } from '~/logic/storage'
3 |
4 | import { appSign } from './appSign'
5 |
6 | export function revokeAccessKey() {
7 | accessKey.value = null
8 | }
9 |
10 | // https://socialsisteryi.github.io/bilibili-API-collect/docs/misc/sign/APPKey.html#appkey
11 | export const TVAppKey = {
12 | appkey: '4409e2ce8ffd12b8',
13 | appsec: '59b43e04ad6965f34319062b478f83dd',
14 | }
15 |
16 | // https://github.com/magicdawn/bilibili-app-recommend/blob/e91722cc076fe65b98116fb0248236851ae6e1dc/src/utility/access-key/tv-qrcode/api.ts#L8
17 | export function tvSignSearchParams(params: Record) {
18 | const sign = appSign(params, TVAppKey.appkey, TVAppKey.appsec)
19 | return new URLSearchParams({
20 | ...params,
21 | sign,
22 | })
23 | }
24 |
25 | export function getTvSign(params: Record) {
26 | return appSign(params, TVAppKey.appkey, TVAppKey.appsec)
27 | }
28 |
29 | export function pollTVLoginQRCode(authCode: string): Promise {
30 | const url = 'https://passport.bilibili.com/x/passport-tv-login/qrcode/poll'
31 |
32 | return new Promise((resolve, reject) => {
33 | fetch(url, {
34 | method: 'POST',
35 | headers: {
36 | 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
37 | },
38 | body: tvSignSearchParams({
39 | appkey: TVAppKey.appkey,
40 | auth_code: authCode,
41 | local_id: '0',
42 | ts: '0',
43 | }),
44 | })
45 | .then(response => response.json())
46 | .then(data => resolve(data))
47 | .catch(error => reject(error))
48 | })
49 | }
50 |
51 | export function getTVLoginQRCode(): Promise {
52 | const url = 'https://passport.bilibili.com/x/passport-tv-login/qrcode/auth_code'
53 |
54 | return new Promise((resolve, reject) => {
55 | fetch(url, {
56 | method: 'POST',
57 | headers: {
58 | 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
59 | },
60 | body: tvSignSearchParams({
61 | appkey: TVAppKey.appkey,
62 | local_id: '0',
63 | ts: '0',
64 | }),
65 | })
66 | .then(response => response.json())
67 | .then(data => resolve(data))
68 | .catch(error => reject(error))
69 | })
70 | }
71 |
--------------------------------------------------------------------------------
/src/utils/dataFormatter.ts:
--------------------------------------------------------------------------------
1 | import { settings } from '~/logic'
2 | import { i18n } from '~/utils/i18n'
3 |
4 | import { LanguageType } from './../enums/appEnums'
5 |
6 | export const { t } = i18n.global
7 |
8 | export function numFormatter(num: number | string): string {
9 | const digits = 1 // specify number of digits after decimal
10 | let lookup
11 |
12 | if (settings.value.language === LanguageType.Mandarin_CN) {
13 | lookup = [
14 | { value: 1, symbol: ' ' },
15 | { value: 1e4, symbol: ' 万' },
16 | { value: 1e7, symbol: ' 千万' },
17 | { value: 1e8, symbol: ' 亿' },
18 | ]
19 | }
20 | else if (settings.value.language === LanguageType.Cantonese || settings.value.language === LanguageType.Mandarin_TW) {
21 | lookup = [
22 | { value: 1, symbol: ' ' },
23 | { value: 1e4, symbol: ' 萬' },
24 | { value: 1e7, symbol: ' 千萬' },
25 | { value: 1e8, symbol: ' 億' },
26 | ]
27 | }
28 | else {
29 | lookup = [
30 | { value: 1, symbol: '' },
31 | { value: 1e3, symbol: 'K' },
32 | { value: 1e6, symbol: 'M' },
33 | { value: 1e9, symbol: 'B' },
34 | ]
35 | }
36 | const rx = /\.0+$|(\.\d*[1-9])0+$/
37 | if (typeof num === 'string') {
38 | if (num.includes('萬') || num.includes('万')) {
39 | num = (Number(num.replaceAll('萬', '').replaceAll('万', '')) || 0) * 10000
40 | }
41 | else {
42 | num = Number(num)
43 | }
44 | }
45 | const item = lookup.slice().reverse().find((item) => {
46 | return num >= item.value
47 | })
48 | return item ? (num / item.value).toFixed(digits).replace(rx, '$1') + item.symbol : '0'
49 | }
50 |
51 | export function calcTimeSince(date: number | string | Date) {
52 | const seconds = Math.floor(((Number(new Date())) - Number(date)) / 1000)
53 | let interval = seconds / 31536000
54 | if (interval > 1)
55 | return `${Math.floor(interval)} ${t('common.year', Math.floor(interval))}`
56 | interval = seconds / 2592000
57 | if (interval > 1)
58 | return `${Math.floor(interval)} ${t('common.month', Math.floor(interval))}`
59 | interval = seconds / 604800
60 | if (interval > 1)
61 | return `${Math.floor(interval)} ${t('common.week', Math.floor(interval))}`
62 | interval = seconds / 86400
63 | if (interval > 1)
64 | return `${Math.floor(interval)} ${t('common.day', Math.floor(interval))}`
65 | interval = seconds / 3600
66 | if (interval > 1)
67 | return `${Math.floor(interval)} ${t('common.hour', Math.floor(interval))}`
68 | interval = seconds / 60
69 | if (interval > 1)
70 | return `${Math.floor(interval)} ${t('common.minute', Math.floor(interval))}`
71 | return `${Math.floor(interval)} ${t('common.second', Math.floor(interval))}`
72 | }
73 |
74 | export function calcCurrentTime(totalSeconds: number) {
75 | const hours = Math.floor(totalSeconds / 3600)
76 | totalSeconds %= 3600
77 | const minutes = Math.floor(totalSeconds / 60)
78 | const seconds = totalSeconds % 60
79 |
80 | if (hours <= 0)
81 | return `${minutes < 10 ? `0${minutes}` : minutes}:${seconds < 10 ? `0${seconds}` : seconds}`
82 |
83 | return `${hours < 10 ? `0${hours}` : hours}:${minutes < 10 ? `0${minutes}` : minutes}:${seconds < 10 ? `0${seconds}` : seconds}`
84 | }
85 |
--------------------------------------------------------------------------------
/src/utils/element.ts:
--------------------------------------------------------------------------------
1 | export function getShadowRoot(v: Element) {
2 | if (v.shadowRoot)
3 | return v.shadowRoot
4 | }
5 |
6 | export function findLeafActiveElement(doc: DocumentOrShadowRoot): Element | undefined {
7 | const active = doc?.activeElement
8 | if (!active)
9 | return
10 |
11 | const shadowRoot = getShadowRoot(active)
12 | if (shadowRoot && shadowRoot.activeElement)
13 | return findLeafActiveElement(shadowRoot)
14 |
15 | return active
16 | }
17 |
--------------------------------------------------------------------------------
/src/utils/i18n.ts:
--------------------------------------------------------------------------------
1 | import messages from '@intlify/unplugin-vue-i18n/messages'
2 | import { createI18n } from 'vue-i18n'
3 |
4 | export const i18n = createI18n({
5 | legacy: false,
6 | locale: 'en',
7 | fallbackLocale: 'en',
8 | globalInjection: true,
9 | messages,
10 | })
11 |
--------------------------------------------------------------------------------
/src/utils/lazyLoad.ts:
--------------------------------------------------------------------------------
1 | // copy with vscode
2 |
3 | interface IdleDeadline {
4 | readonly didTimeout: boolean
5 | timeRemaining: () => number
6 | }
7 |
8 | interface IDisposable {
9 | dispose: () => void
10 | }
11 |
12 | /**
13 | * Execute the callback the next time the browser is idle, returning an
14 | * {@link IDisposable} that will cancel the callback when disposed. This wraps
15 | * [requestIdleCallback] so it will fallback to [setTimeout] if the environment
16 | * doesn't support it.
17 | *
18 | * @param callback The callback to run when idle, this includes an
19 | * [IdleDeadline] that provides the time alloted for the idle callback by the
20 | * browser. Not respecting this deadline will result in a degraded user
21 | * experience.
22 | * @param timeout A timeout at which point to queue no longer wait for an idle
23 | * callback but queue it on the regular event loop (like setTimeout). Typically
24 | * this should not be used.
25 | *
26 | * [IdleDeadline]: https://developer.mozilla.org/en-US/docs/Web/API/IdleDeadline
27 | * [requestIdleCallback]: https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback
28 | * [setTimeout]: https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout
29 | */
30 |
31 | // eslint-disable-next-line import/no-mutable-exports
32 | export let runWhenIdle: (callback: (idle: IdleDeadline) => void, timeout?: number) => IDisposable
33 |
34 | declare function requestIdleCallback(callback: (args: IdleDeadline) => void, options?: { timeout: number }): number
35 | declare function cancelIdleCallback(handle: number): void;
36 |
37 | (function () {
38 | if (typeof requestIdleCallback !== 'function' || typeof cancelIdleCallback !== 'function') {
39 | runWhenIdle = (runner) => {
40 | let disposed = false
41 | setTimeout(() => {
42 | if (disposed)
43 | return
44 | const end = Date.now() + 15 // one frame at 64fps
45 | runner(Object.freeze({
46 | didTimeout: true,
47 | timeRemaining() {
48 | return Math.max(0, end - Date.now())
49 | },
50 | }))
51 | })
52 | return {
53 | dispose() {
54 | if (disposed)
55 | return
56 | disposed = true
57 | },
58 | }
59 | }
60 | }
61 | else {
62 | runWhenIdle = (runner, timeout?) => {
63 | const handle: number = requestIdleCallback(runner, typeof timeout === 'number' ? { timeout } : undefined)
64 | let disposed = false
65 | return {
66 | dispose() {
67 | if (disposed)
68 | return
69 |
70 | disposed = true
71 | cancelIdleCallback(handle)
72 | },
73 | }
74 | }
75 | }
76 | })()
77 |
78 | // TODO: handle error
79 | export class LazyValue {
80 | private _value: T | undefined
81 | private _didRun = false
82 |
83 | constructor(
84 | private executor: () => T,
85 | ) {}
86 |
87 | get value(): T {
88 | if (!this._didRun) {
89 | this._value = this.executor()
90 | this._didRun = true
91 | }
92 | return this._value!
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/utils/mitt.ts:
--------------------------------------------------------------------------------
1 | import type { Emitter } from 'mitt'
2 | import mitt from 'mitt'
3 |
4 | const emitter: Emitter = mitt()
5 | export default emitter
6 |
--------------------------------------------------------------------------------
/src/utils/tabs.ts:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill'
2 |
3 | import { TABS_MESSAGE } from '~/background/messageListeners/tabs'
4 |
5 | export async function openLinkInBackground(url: string) {
6 | try {
7 | browser.runtime.sendMessage({
8 | contentScriptQuery: TABS_MESSAGE.OPEN_LINK_IN_BACKGROUND,
9 | url,
10 | })
11 | }
12 | catch (error) {
13 | console.error('Failed to open link in background:', error)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/utils/timer.ts:
--------------------------------------------------------------------------------
1 | export function executeTimes(fn: () => void | Promise, times: number, interval: number = 1000) {
2 | let count = 0
3 | let timer: NodeJS.Timeout
4 | // eslint-disable-next-line prefer-const
5 | timer = setInterval(async () => {
6 | await fn()
7 | count++
8 | if (count >= times) {
9 | clearInterval(timer)
10 | }
11 | }, interval)
12 |
13 | return timer
14 | }
15 |
--------------------------------------------------------------------------------
/src/utils/uriParse.ts:
--------------------------------------------------------------------------------
1 | interface BilibiliUri {
2 | cid: string | null
3 | player_height: number | null
4 | player_preload: string | null
5 | player_rotate: number | null
6 | player_width: number | null
7 | report_flow_data: string | null
8 | trackid: string | null
9 | }
10 |
11 | export function isVerticalVideo(uri: string): boolean {
12 | const bilibiliUri = parseBilibiliUri(uri)
13 | if (bilibiliUri.player_height == null || bilibiliUri.player_width == null)
14 | return false
15 |
16 | return bilibiliUri.player_height > bilibiliUri.player_width
17 | }
18 |
19 | export function parseBilibiliUri(uri: string): BilibiliUri {
20 | const params = uri.split('?')[1]
21 | const searchParams = new URLSearchParams(params)
22 | return {
23 | cid: searchParams.get('cid'),
24 | player_height: searchParams.get('player_height') ? Number.parseInt(searchParams.get('player_height')!) : null,
25 | player_preload: searchParams.get('player_preload'),
26 | player_rotate: searchParams.get('player_rotate') ? Number.parseInt(searchParams.get('player_rotate')!) : null,
27 | player_width: searchParams.get('player_width') ? Number.parseInt(searchParams.get('player_width')!) : null,
28 | report_flow_data: searchParams.get('report_flow_data'),
29 | trackid: searchParams.get('trackid'),
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/utils/volumeBalance.ts:
--------------------------------------------------------------------------------
1 | import { settings } from '~/logic'
2 | import type { UpVolumeConfig } from '~/logic/storage'
3 |
4 | import { getUpInfo, setVolume } from './player'
5 | import { initVolumeSliders } from './volumeSliders'
6 |
7 | // 音量均衡相关的状态管理
8 |
9 | // 获取UP主音量配置
10 | export function getUpVolumeConfig(uid: string): UpVolumeConfig | null {
11 | if (!settings.value.enableVolumeBalance) {
12 | return null
13 | }
14 |
15 | return settings.value.upVolumeConfigs.find(config => config.uid === uid) || null
16 | }
17 |
18 | // 保存UP主音量配置
19 | export function saveUpVolumeConfig(uid: string, name: string, volumeOffset: number): void {
20 | if (!settings.value.enableVolumeBalance) {
21 | return
22 | }
23 |
24 | const existingIndex = settings.value.upVolumeConfigs.findIndex(config => config.uid === uid)
25 | const newConfig: UpVolumeConfig = {
26 | uid,
27 | name,
28 | volumeOffset,
29 | lastUpdated: Date.now(),
30 | }
31 |
32 | if (existingIndex >= 0) {
33 | settings.value.upVolumeConfigs[existingIndex] = newConfig
34 | }
35 | else {
36 | settings.value.upVolumeConfigs.push(newConfig)
37 | }
38 | }
39 |
40 | // 删除UP主音量配置
41 | export function removeUpVolumeConfig(uid: string): void {
42 | const index = settings.value.upVolumeConfigs.findIndex(config => config.uid === uid)
43 | if (index >= 0) {
44 | settings.value.upVolumeConfigs.splice(index, 1)
45 | }
46 | }
47 |
48 | // 清空所有UP主音量配置
49 | export function clearAllUpVolumeConfigs(): void {
50 | settings.value.upVolumeConfigs = []
51 | }
52 |
53 | // 应用音量均衡 (页面加载时调用)
54 | export function applyVolumeBalance(): void {
55 | if (!settings.value.enableVolumeBalance) {
56 | return
57 | }
58 |
59 | const video = document.querySelector('#bilibiliPlayer video,#bilibili-player video,.bilibili-player video,.player-container video,#bilibiliPlayer bwp-video,#bilibili-player bwp-video,.bilibili-player bwp-video,.player-container bwp-video,#bofqi video,[aria-label="哔哩哔哩播放器"] video') as HTMLVideoElement
60 | if (!video) {
61 | // 如果视频元素还没有加载,延迟重试
62 | setTimeout(() => applyVolumeBalance(), 1000)
63 | return
64 | }
65 |
66 | const upInfo = getUpInfo()
67 |
68 | if (!upInfo.uid) {
69 | // 如果没有UP主信息,设置为基准音量
70 | setVolume(settings.value.baseVolume)
71 | return
72 | }
73 |
74 | const upConfig = getUpVolumeConfig(upInfo.uid)
75 |
76 | if (upConfig) {
77 | // 如果有UP主配置,应用相对基准音量的偏移
78 | const targetVolume = Math.max(0, Math.min(100, settings.value.baseVolume + upConfig.volumeOffset))
79 | setVolume(targetVolume)
80 | }
81 | else {
82 | // 如果没有UP主配置,设置为基准音量
83 | setVolume(settings.value.baseVolume)
84 | }
85 | }
86 |
87 | // 初始化音量滑动条
88 | export function startVolumeChangeMonitoring(): void {
89 | if (!settings.value.enableVolumeBalance) {
90 | return
91 | }
92 |
93 | // 初始化音量滑动条
94 | initVolumeSliders()
95 | }
96 |
97 | // 手动应用音量均衡(用于页面跳转等场景)
98 | export function manualApplyVolumeBalance() {
99 | setTimeout(() => {
100 | applyVolumeBalance()
101 | startVolumeChangeMonitoring()
102 | }, 2000)
103 | }
104 |
105 | // 重置音量监听状态(用于页面跳转时)
106 | export function resetVolumeMonitoring() {
107 | // 现在使用滑动条,不需要重置监听状态
108 | }
109 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "incremental": false,
4 | "target": "es2016",
5 | "jsx": "preserve",
6 | "lib": ["DOM", "ESNext"],
7 | "baseUrl": ".",
8 | "module": "ESNext",
9 | "moduleResolution": "node",
10 | "paths": {
11 | "~/*": ["src/*"]
12 | },
13 | "resolveJsonModule": true,
14 | "types": [
15 | "vite/client"
16 | ],
17 | "strict": true,
18 | "noImplicitAny": false,
19 | "noUnusedLocals": true,
20 | "noEmit": true,
21 | "esModuleInterop": true,
22 | "forceConsistentCasingInFileNames": true,
23 | "isolatedModules": true,
24 | "skipLibCheck": true
25 | },
26 | "exclude": ["dist", "node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/tsup.config.ts:
--------------------------------------------------------------------------------
1 | // import path from 'node:path'
2 |
3 | // import fs from 'fs-extra'
4 | import { defineConfig } from 'tsup'
5 |
6 | import { isDev, isFirefox, isSafari } from './scripts/utils'
7 |
8 | const outDir = isFirefox ? 'extension-firefox/dist' : isSafari ? 'extension-safari/dist' : 'extension/dist'
9 |
10 | export default defineConfig(() => ({
11 | entry: {
12 | 'background/index': './src/background/index.ts',
13 | ...(isDev ? { mv3client: './scripts/client.ts' } : {}),
14 | },
15 | async onSuccess() {
16 | // fs.copySync(path.resolve(__dirname, './src/inject/index.js'), path.resolve(__dirname, `./${outDir}/inject/index.js`))
17 | },
18 | outDir,
19 | format: ['esm'],
20 | target: 'esnext',
21 | ignoreWatch: ['**/extension/**', '**/extension-firefox/**', '**/extension-safari/**'],
22 | splitting: false,
23 | sourcemap: false, // https://github.com/vitejs/vite-plugin-vue/issues/35
24 | define: {
25 | '__DEV__': JSON.stringify(isDev),
26 | 'process.env.NODE_ENV': JSON.stringify(isDev ? 'development' : 'production'),
27 | 'process.env.FIREFOX': isFirefox ? 'true' : 'false',
28 | },
29 | platform: 'browser',
30 | minifyWhitespace: !isDev,
31 | minifySyntax: !isDev,
32 | }))
33 |
--------------------------------------------------------------------------------
/unocss.config.ts:
--------------------------------------------------------------------------------
1 | import { presetAttributify, presetIcons, presetTypography, presetUno, transformerDirectives } from 'unocss'
2 | import { defineConfig } from 'unocss/vite'
3 |
4 | const remRE = /(-?[.\d]+)rem/g
5 |
6 | export default defineConfig({
7 | content: {
8 | pipeline: {
9 | include: [
10 | '**/*.{js,ts}',
11 | /\.(vue|svelte|[jt]sx|mdx?|astro|elm|php|phtml|html)($|\?)/,
12 | ],
13 | },
14 | },
15 | blocklist: [
16 | 'ps',
17 | 'container',
18 | ],
19 | presets: [
20 | presetUno(),
21 | presetAttributify(),
22 | presetIcons({
23 | extraProperties: {
24 | 'display': 'inline-block',
25 | 'vertical-align': 'middle',
26 | 'width': '1.2em',
27 | 'height': '1.2em',
28 | },
29 | }),
30 | presetTypography(),
31 |
32 | {
33 | name: 'text-size-transformer',
34 | postprocess: (util) => {
35 | util.entries.forEach((i) => {
36 | const value = i[1]
37 |
38 | if (typeof value === 'string' && remRE.test(value)) {
39 | i[1] = value.replace(remRE, (_, num: number) => {
40 | return `calc(var(--bew-base-font-size) * ${num})`
41 | })
42 | }
43 | })
44 | },
45 | },
46 | ],
47 | transformers: [
48 | transformerDirectives(),
49 | ],
50 | })
51 |
--------------------------------------------------------------------------------
/vite.config.content.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 |
3 | import packageJson from './package.json'
4 | import { isDev, isFirefox, isSafari, r } from './scripts/utils'
5 | import { sharedConfig } from './vite.config'
6 |
7 | // bundling the content script using Vite
8 | export default defineConfig({
9 | ...sharedConfig,
10 | build: {
11 | watch: isDev
12 | ? { include: ['./**/*'] }
13 | : undefined,
14 | outDir: r(isFirefox ? 'extension-firefox/dist/contentScripts' : isSafari ? 'extension-safari/dist/contentScripts' : 'extension/dist/contentScripts'),
15 | cssCodeSplit: false,
16 | emptyOutDir: false,
17 | sourcemap: false, // https://github.com/vitejs/vite-plugin-vue/issues/35
18 | lib: {
19 | entry: r('src/contentScripts/index.ts'),
20 | name: packageJson.name,
21 | formats: ['iife'],
22 | },
23 | rollupOptions: {
24 | output: {
25 | entryFileNames: 'index.global.js',
26 | extend: true,
27 | },
28 | },
29 | },
30 | })
31 |
--------------------------------------------------------------------------------
/vite.config.inject.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 |
3 | import packageJson from './package.json'
4 | import { isDev, isFirefox, isSafari, r } from './scripts/utils'
5 | import { sharedConfig } from './vite.config'
6 |
7 | // bundling the content script using Vite
8 | export default defineConfig({
9 | ...sharedConfig,
10 | build: {
11 | watch: isDev
12 | ? { include: ['./**/*'] }
13 | : undefined,
14 | outDir: r(isFirefox ? 'extension-firefox/dist/contentScripts' : isSafari ? 'extension-safari/dist/contentScripts' : 'extension/dist/contentScripts'),
15 | cssCodeSplit: false,
16 | emptyOutDir: false,
17 | sourcemap: false, // https://github.com/vitejs/vite-plugin-vue/issues/35
18 | lib: {
19 | entry: r('src/inject/index.ts'),
20 | name: packageJson.name,
21 | formats: ['iife'],
22 | },
23 | rollupOptions: {
24 | output: {
25 | entryFileNames: 'inject.global.js',
26 | extend: true,
27 | },
28 | },
29 | },
30 | })
31 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import { dirname, relative } from 'node:path'
4 |
5 | import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'
6 | import replace from '@rollup/plugin-replace'
7 | import Vue from '@vitejs/plugin-vue'
8 | import UnoCSS from 'unocss/vite'
9 | import AutoImport from 'unplugin-auto-import/vite'
10 | import type { UserConfig } from 'vite'
11 | import { defineConfig } from 'vite'
12 |
13 | import { isDev, isFirefox, isSafari, port, r } from './scripts/utils'
14 | // import { MV3Hmr } from './vite-mv3-hmr'
15 |
16 | export const sharedConfig: UserConfig = {
17 | root: r('src'),
18 | resolve: {
19 | alias: {
20 | '~/': `${r('src')}/`,
21 | '~': r('src'),
22 | },
23 | },
24 | plugins: [
25 | Vue(),
26 |
27 | AutoImport({
28 | imports: [
29 | 'vue',
30 | {
31 | 'webextension-polyfill': [
32 | ['*', 'browser'],
33 | ],
34 | },
35 | ],
36 | }),
37 |
38 | // https://github.com/intlify/bundle-tools/tree/main/packages/unplugin-vue-i18n
39 | VueI18nPlugin({
40 | runtimeOnly: true,
41 | compositionOnly: true,
42 | strictMessage: false,
43 | include: [r('./src/_locales/**')],
44 | }),
45 |
46 | // https://github.com/unocss/unocss
47 | UnoCSS(),
48 |
49 | replace({
50 | '__DEV__': JSON.stringify(isDev),
51 | 'process.env.NODE_ENV': JSON.stringify(isDev ? 'development' : 'production'),
52 | '__VUE_OPTIONS_API__': JSON.stringify(true),
53 | '__VUE_PROD_DEVTOOLS__': JSON.stringify(false),
54 | 'preventAssignment': true,
55 | }),
56 |
57 | // rewrite assets to use relative path
58 | {
59 | name: 'assets-rewrite',
60 | enforce: 'post',
61 | apply: 'build',
62 | transformIndexHtml(html, { path }) {
63 | return html.replace(/"\/assets\//g, `"${relative(dirname(path), '/assets')}/`)
64 | },
65 | },
66 | ],
67 | optimizeDeps: {
68 | include: [
69 | 'vue',
70 | '@vueuse/core',
71 | 'webextension-polyfill',
72 | ],
73 | exclude: [
74 | 'vue-demi',
75 | ],
76 | },
77 | }
78 |
79 | export default defineConfig(({ command }) => ({
80 | ...sharedConfig,
81 | base: command === 'serve' ? `http://localhost:${port}/` : '/dist/',
82 | server: {
83 | port,
84 | hmr: {
85 | host: 'localhost',
86 | },
87 | },
88 | build: {
89 | outDir: r(isFirefox ? 'extension-firefox/dist' : isSafari ? 'extension-safari/dist' : 'extension/dist'),
90 | emptyOutDir: true,
91 | sourcemap: false, // https://github.com/vitejs/vite-plugin-vue/issues/35
92 | // https://developer.chrome.com/docs/webstore/program_policies/#:~:text=Code%20Readability%20Requirements
93 | terserOptions: {
94 | mangle: false,
95 | },
96 | rollupOptions: {
97 | input: {
98 | options: r('src/options/index.html'),
99 | popup: r('src/popup/index.html'),
100 | },
101 | },
102 | minify: 'terser',
103 | },
104 | plugins: [
105 | ...sharedConfig.plugins!,
106 |
107 | // MV3Hmr(),
108 | ],
109 | test: {
110 | globals: true,
111 | environment: 'jsdom',
112 | },
113 | }))
114 |
--------------------------------------------------------------------------------