├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── bug-template.yaml ├── pull_request_template.md └── workflows │ ├── ci.yml │ └── release-it.yml ├── .gitignore ├── .markdownlint.yaml ├── .npmrc ├── .release-it.json ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README-cmn_CN.md ├── README-cmn_TW.md ├── README-jyut.md ├── README.md ├── assets ├── anime-timetable-icons.png ├── bewly-vtuber-style-logo.png ├── broken-image.png ├── empty.png ├── fonts │ ├── Geist-LICENSE.txt │ ├── Geist[wght].woff2 │ ├── Onest-LICENSE.txt │ ├── Onest[wght].woff2 │ ├── ShangguSans-LICENSE.txt │ ├── ShangguSansSC-VF.ttf │ ├── ZhudouSans-LICENSE.txt │ └── ZhudouSansVF-subset.woff2 ├── icon-512-flat.png ├── icon-512.png ├── loading.gif ├── rules.json ├── sponsor │ ├── afdian.jpg │ └── bmc.png └── twitterUsers │ ├── YukiHakarigoto.jpg │ ├── exgphe.png │ ├── st7evechou.jpg │ └── vanillaCitron.jpg ├── docs ├── CONTRIBUTING-cmn_CN.md ├── CONTRIBUTING-cmn_TW.md ├── CONTRIBUTING-jyut.md └── CONTRIBUTING.md ├── eslint.config.mjs ├── knip.json ├── package.json ├── pnpm-lock.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 │ │ ├── components │ │ │ ├── ChangeWallpaper.vue │ │ │ ├── SettingsItem.vue │ │ │ └── SettingsItemGroup.vue │ │ └── types.ts │ ├── SideBar │ │ ├── SideBar.vue │ │ └── types.ts │ ├── Slider.vue │ ├── Tooltip.vue │ ├── TopBar │ │ ├── BewlyOrBiliTopBarSwitcher.vue │ │ ├── OldTopBar.vue │ │ ├── TopBar.vue │ │ ├── components │ │ │ ├── BewlyOrBiliPageSwitcher.vue │ │ │ ├── ChannelsPop.vue │ │ │ ├── FavoritesPop.vue │ │ │ ├── HistoryPop.vue │ │ │ ├── MomentsPop.vue │ │ │ ├── MorePop.vue │ │ │ ├── NotificationsDrawer.vue │ │ │ ├── NotificationsPop.vue │ │ │ ├── UploadPop.vue │ │ │ ├── UserPanelPop.vue │ │ │ └── WatchLaterPop.vue │ │ ├── notify.ts │ │ ├── oldTopBarComponents │ │ │ └── OldUserPanelPop.vue │ │ └── 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.js ├── 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 │ │ ├── topBarLiveMoment.ts │ │ └── topBarMoment.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 │ ├── injectBuildInFonts.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 │ ├── lvIcons.ts │ ├── main.ts │ ├── mitt.ts │ ├── svgIcons.ts │ ├── tabs.ts │ ├── timer.ts │ ├── transformer.ts │ └── uriParse.ts ├── tsconfig.json ├── tsup.config.ts ├── unocss.config.ts ├── vite-mv3-hmr.ts ├── vite.config.content.ts └── vite.config.ts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: hakadao 14 | custom: ['https://afdian.com/a/Hakadao'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-template.yaml: -------------------------------------------------------------------------------- 1 | name: 问题报告 2 | description: 遇到错误请在此报告。 3 | labels: [bug] 4 | 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | ## 问题报告 10 | 报告问题之前请先看“[常見問題](https://github.com/BewlyBewly/BewlyBewly/wiki/%E5%B8%B8%E8%A6%8B%E5%95%8F%E9%A1%8C)”页面。 11 | 12 | 标题不填或直接随便写类似“bug”“错误”“有问题”简单带过的 issue 直接 close + lock 不解释,如果是旧版本的问题或是已经有人提问过的问题将会关闭。 13 | 14 | 功能请求不是在问题报告里面写的,请[开启空 issue](https://github.com/BewlyBewly/BewlyBewly/issues/new)。 15 | 16 | 若遇到页面相关问题(比如某页面下出现了不该出现的元素),我们建议一并附上发生问题的页面链接。 17 | 18 | ### Edge 19 | Edge 受遥遥领先的 Edge Addons 审核导致版本更新永远落后于 Google Chrome,且审核问题不是我们控制的,所以后面已经直接取消上架 Edge Addons。 20 | 21 | [Edge 浏览器也可以通过 Chrome Web Store 下载](https://chromewebstore.google.com/detail/bewlybewly/bbbiejemhfihiooipfcjmjmbfdmobobp),**不要因为 Edge 浏览器在 Chrome Web Store 上弹出大提示而又下回 Edge 版本**。 22 | 23 | ### Safari 24 | 不会上架 Safari,DKLM 苹果我注册苹果开发者每年送苹果 99 美元连资格也不给,系统和客服一直说系统说你有一个或多个问题导致无法注册也没解决办法,另外 Apple 傻閪弱智 on9 笨柒豬閪脑回路 Safari 和系统更新绑定,Safari 我屌你老母吔屎啦快撚啲死柒咗佢啦邊撚個會用佢 25 | 26 | - type: textarea 27 | attributes: 28 | label: 环境信息 29 | description: 【请勿修改 issue 模版。】扩展版本、浏览器版本、以及你做出的自定义设置。 30 | placeholder: | 31 | - 浏览器(如 Google Chrome): 32 | - 浏览器版本(如 126.0.6478.126): 33 | - BewlyBewly 版本(如 0.20.1): 34 | 35 | 如果你修改了 BewlyBewly 的设置,请写在下面以方便我們排查問題(可粗略写成类似“设置了××后出现这个问题”〔将“××”替换为你的设置项〕): 36 | 37 | value: | 38 | - 浏览器(如 Google Chrome): 39 | - 浏览器版本(如 126.0.6478.126): 40 | - BewlyBewly 版本(如 0.20.1): 41 | 42 | 如果你修改了 BewlyBewly 的设置,请写在下面以方便我們排查問題(可粗略写成类似“设置了××后出现这个问题”〔将“××”替换为你的设置项〕): 43 | 44 | validations: 45 | required: true 46 | 47 | - type: textarea 48 | attributes: 49 | label: 问题描述 50 | description: 如何重现,最好带有截图或视频以便排查。 51 | placeholder: | 52 | 请预先搜索此问题是否在其他 issue 中出现过,重复的 issue 会被 close + lock。 53 | validations: 54 | required: true 55 | 56 | - type: textarea 57 | attributes: 58 | label: 预期行为 59 | description: 你认为应该是什么行为。 60 | validations: 61 | required: false 62 | 63 | - type: checkboxes 64 | attributes: 65 | label: 最终确认 66 | description: 请确认以下所有内容,否则将被 close。 67 | options: 68 | - label: 我确认在停用 BewlyBewly 并强制刷新(按住 Shift 键的同时按刷新键)后,问题不再出现。 69 | required: false 70 | - label: 我确认此问题未在其他 issue 中出现过。 71 | required: false 72 | - label: 我确认我已阅读“[常見問題](https://github.com/BewlyBewly/BewlyBewly/wiki/%E5%B8%B8%E8%A6%8B%E5%95%8F%E9%A1%8C)”页面,其中没有对应我问题的解决方案。 73 | required: false 74 | - label: 我确认我正在使用最新的 BewlyBewly 版本。 75 | required: false 76 | 77 | - type: checkboxes 78 | attributes: 79 | label: 作出贡献? 80 | description: 【此选项非必选,如果你不晓得这里在说什么,请勿勾选。】我们欢迎任何人贡献代码,见 [CONTRIBUTING.md](https://github.com/BewlyBewly/BewlyBewly/blob/main/docs/CONTRIBUTING.md)。 81 | options: 82 | - label: 我将自行提交一个 PR 来解决此问题。 83 | required: false 84 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | <!-- see: https://github.com/BewlyBewly/BewlyBewly/blob/main/docs/CONTRIBUTING.md --> 2 | <!-- We may not respond to your issue or PR. --> 3 | <!-- We may close an issue or PR without much feedback. --> 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | # The branch where the project source code resides 7 | # 项目源代码所在的分支 8 | - dev 9 | - main 10 | paths-ignore: 11 | # Changes involving the following path files will not trigger the workflow 12 | # 涉及以下路径文件的更改不会触发工作流 13 | - LICENSE 14 | - README-cmn_CN.md 15 | - README-cmn_TW.md 16 | - README-jyut.md 17 | - docs/** 18 | 19 | pull_request: 20 | branches: 21 | - dev 22 | - main 23 | paths-ignore: 24 | - LICENSE 25 | - README-cmn_CN.md 26 | - README-cmn_TW.md 27 | - README-jyut.md 28 | - docs/** 29 | 30 | jobs: 31 | test: 32 | name: Test 33 | strategy: 34 | matrix: 35 | node: [lts/*, lts/-1] 36 | os: [ubuntu-latest, windows-latest] 37 | fail-fast: false 38 | runs-on: ${{ matrix.os }} 39 | timeout-minutes: 10 40 | 41 | steps: 42 | - uses: actions/checkout@v4 43 | 44 | - name: Set node ${{ matrix.node }} 45 | uses: actions/setup-node@v4 46 | with: 47 | node-version: ${{ matrix.node }} 48 | 49 | - name: Install pnpm 50 | uses: pnpm/action-setup@v4 51 | with: 52 | run_install: | 53 | - args: [--frozen-lockfile] 54 | 55 | - name: Lint 56 | if: ${{ matrix.os == 'ubuntu-latest' }} 57 | run: pnpm run lint 58 | 59 | - name: Type check 60 | if: ${{ matrix.os == 'ubuntu-latest' }} 61 | run: pnpm run typecheck 62 | 63 | - name: Test 64 | run: pnpm run test 65 | 66 | - name: Knip 67 | if: ${{ matrix.os == 'ubuntu-latest' }} 68 | run: pnpm run knip 69 | 70 | - name: Build Extension 71 | run: pnpm build 72 | 73 | - name: Upload Zip 74 | if: ${{ matrix.os == 'ubuntu-latest' && matrix.node == 'lts/*' && github.ref_name == 'main' }} 75 | uses: actions/upload-artifact@v4.3.1 76 | with: 77 | name: BewlyBewly Zip 78 | path: extension 79 | 80 | - name: Build Extension-Firefox 81 | run: pnpm build-firefox 82 | 83 | - name: Upload Zip 84 | if: ${{ matrix.os == 'ubuntu-latest' && matrix.node == 'lts/*' && github.ref_name == 'main' }} 85 | uses: actions/upload-artifact@v4.3.1 86 | with: 87 | name: BewlyBewly-Firefox Zip 88 | path: extension-firefox 89 | -------------------------------------------------------------------------------- /.github/workflows/release-it.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | id-token: write 6 | 7 | on: 8 | workflow_dispatch: 9 | inputs: 10 | increment: 11 | required: true 12 | default: patch 13 | type: choice 14 | options: 15 | - major 16 | - minor 17 | - patch 18 | 19 | jobs: 20 | release: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 0 26 | token: ${{ secrets.RELEASE_TOKEN }} 27 | 28 | - name: Git config 29 | run: | 30 | git config user.name "github-actions[bot]" 31 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 32 | 33 | - name: Set node 34 | uses: actions/setup-node@v4 35 | with: 36 | node-version: lts/* 37 | registry-url: 'https://registry.npmjs.org' 38 | 39 | - name: Install pnpm 40 | uses: pnpm/action-setup@v3 41 | with: 42 | run_install: | 43 | - args: [--frozen-lockfile] 44 | 45 | - name: Release 46 | run: npx release-it ${{ inputs.increment }} --verbose 47 | env: 48 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 49 | CHROME_EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }} 50 | CHROME_CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }} 51 | CHROME_CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }} 52 | CHROME_REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }} 53 | FIREFOX_EXTENSION_ID: ${{ secrets.FIREFOX_EXTENSION_ID }} 54 | FIREFOX_JWT_ISSUER: ${{ secrets.FIREFOX_JWT_ISSUER }} 55 | FIREFOX_JWT_SECRET: ${{ secrets.FIREFOX_JWT_SECRET }} 56 | EDGE_PRODUCT_ID: ${{ secrets.EDGE_PRODUCT_ID }} 57 | EDGE_CLIENT_ID: ${{ secrets.EDGE_CLIENT_ID }} 58 | EDGE_CLIENT_SECRET: ${{ secrets.EDGE_CLIENT_SECRET }} 59 | EDGE_ACCESS_TOKEN_URL: ${{ secrets.EDGE_ACCESS_TOKEN_URL }} 60 | -------------------------------------------------------------------------------- /.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/ -------------------------------------------------------------------------------- /.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.experimental.useFlatConfig": true, 4 | 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 | "WATCHLATER", 55 | "bewly", 56 | "bilibili", 57 | "unocss", 58 | "Vitesse", 59 | "webext", 60 | "vitesse-webext" 61 | ], 62 | "typescript.tsdk": "node_modules/typescript/lib", 63 | "vite.autoStart": false, 64 | "files.associations": { 65 | "*.css": "postcss" 66 | }, 67 | "i18n-ally.localesPaths": [ 68 | "src/_locales" 69 | ], 70 | "i18n-ally.keystyle": "nested" 71 | } 72 | -------------------------------------------------------------------------------- /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/BewlyBewly/BewlyBewly/d42143547bf4e9cc6864f227fcbcbd396bbff25b/assets/anime-timetable-icons.png -------------------------------------------------------------------------------- /assets/bewly-vtuber-style-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BewlyBewly/BewlyBewly/d42143547bf4e9cc6864f227fcbcbd396bbff25b/assets/bewly-vtuber-style-logo.png -------------------------------------------------------------------------------- /assets/broken-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BewlyBewly/BewlyBewly/d42143547bf4e9cc6864f227fcbcbd396bbff25b/assets/broken-image.png -------------------------------------------------------------------------------- /assets/empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BewlyBewly/BewlyBewly/d42143547bf4e9cc6864f227fcbcbd396bbff25b/assets/empty.png -------------------------------------------------------------------------------- /assets/fonts/Geist[wght].woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BewlyBewly/BewlyBewly/d42143547bf4e9cc6864f227fcbcbd396bbff25b/assets/fonts/Geist[wght].woff2 -------------------------------------------------------------------------------- /assets/fonts/Onest[wght].woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BewlyBewly/BewlyBewly/d42143547bf4e9cc6864f227fcbcbd396bbff25b/assets/fonts/Onest[wght].woff2 -------------------------------------------------------------------------------- /assets/fonts/ShangguSansSC-VF.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BewlyBewly/BewlyBewly/d42143547bf4e9cc6864f227fcbcbd396bbff25b/assets/fonts/ShangguSansSC-VF.ttf -------------------------------------------------------------------------------- /assets/fonts/ZhudouSansVF-subset.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BewlyBewly/BewlyBewly/d42143547bf4e9cc6864f227fcbcbd396bbff25b/assets/fonts/ZhudouSansVF-subset.woff2 -------------------------------------------------------------------------------- /assets/icon-512-flat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BewlyBewly/BewlyBewly/d42143547bf4e9cc6864f227fcbcbd396bbff25b/assets/icon-512-flat.png -------------------------------------------------------------------------------- /assets/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BewlyBewly/BewlyBewly/d42143547bf4e9cc6864f227fcbcbd396bbff25b/assets/icon-512.png -------------------------------------------------------------------------------- /assets/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BewlyBewly/BewlyBewly/d42143547bf4e9cc6864f227fcbcbd396bbff25b/assets/loading.gif -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /assets/sponsor/afdian.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BewlyBewly/BewlyBewly/d42143547bf4e9cc6864f227fcbcbd396bbff25b/assets/sponsor/afdian.jpg -------------------------------------------------------------------------------- /assets/sponsor/bmc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BewlyBewly/BewlyBewly/d42143547bf4e9cc6864f227fcbcbd396bbff25b/assets/sponsor/bmc.png -------------------------------------------------------------------------------- /assets/twitterUsers/YukiHakarigoto.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BewlyBewly/BewlyBewly/d42143547bf4e9cc6864f227fcbcbd396bbff25b/assets/twitterUsers/YukiHakarigoto.jpg -------------------------------------------------------------------------------- /assets/twitterUsers/exgphe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BewlyBewly/BewlyBewly/d42143547bf4e9cc6864f227fcbcbd396bbff25b/assets/twitterUsers/exgphe.png -------------------------------------------------------------------------------- /assets/twitterUsers/st7evechou.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BewlyBewly/BewlyBewly/d42143547bf4e9cc6864f227fcbcbd396bbff25b/assets/twitterUsers/st7evechou.jpg -------------------------------------------------------------------------------- /assets/twitterUsers/vanillaCitron.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BewlyBewly/BewlyBewly/d42143547bf4e9cc6864f227fcbcbd396bbff25b/assets/twitterUsers/vanillaCitron.jpg -------------------------------------------------------------------------------- /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 | }, 48 | }, 49 | ) 50 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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('<div id="app"></div>', '<div id="app">Vite server did not start</div>') 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/background/index.ts: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill' 2 | 3 | import { setupApiMsgLstnrs } from './messageListeners/api' 4 | import { setupTabMsgLstnrs } from './messageListeners/tabs' 5 | 6 | browser.runtime.onInstalled.addListener(async () => { 7 | // eslint-disable-next-line no-console 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 | // eslint-disable-next-line node/prefer-global/process 16 | if (process.env.FIREFOX) { 17 | browser.webRequest.onBeforeSendHeaders.addListener( 18 | async (details: any) => { 19 | const requestHeaders: browser.WebRequest.HttpHeaders = [] 20 | if (details.documentUrl) { 21 | const url = new URL(details.documentUrl) 22 | const extensionUri = isExtensionUri(details.documentUrl) 23 | details.requestHeaders = details.requestHeaders || [] 24 | for (let i = 0; i < details.requestHeaders.length; i++) { 25 | if (details.requestHeaders[i].name.toLowerCase() === 'origin' || details.requestHeaders[i].name.toLowerCase() === 'referer') 26 | requestHeaders.push({ name: details.requestHeaders[i].name, value: extensionUri ? 'https://www.bilibili.com' : url.origin }) 27 | else 28 | requestHeaders.push(details.requestHeaders[i]) 29 | 30 | if (details.requestHeaders[i].name === 'firefox-multi-account-cookie') { 31 | requestHeaders.push({ name: 'cookie', value: details.requestHeaders[i].value }) 32 | } 33 | } 34 | 35 | return { ...details, requestHeaders } 36 | } 37 | }, 38 | { urls: ['<all_urls>'] }, 39 | ['blocking', 'requestHeaders'], 40 | ) 41 | } 42 | 43 | // Setup all message listeners 44 | setupApiMsgLstnrs() 45 | setupTabMsgLstnrs() 46 | -------------------------------------------------------------------------------- /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 | } satisfies APIMAP 49 | 50 | export default API_MOMENT 51 | -------------------------------------------------------------------------------- /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 | } satisfies APIMAP 35 | 36 | export default API_USER 37 | -------------------------------------------------------------------------------- /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 | return browser.tabs.create({ url: message.url, active: false }) 15 | } 16 | } 17 | 18 | export function setupTabMsgLstnrs() { 19 | browser.runtime.onMessage.removeListener(handleConnect) 20 | browser.runtime.onMessage.addListener(handleConnect) 21 | } 22 | 23 | function handleConnect() { 24 | browser.runtime.onMessage.removeListener(handleMessage) 25 | browser.runtime.onMessage.addListener(handleMessage) 26 | } 27 | -------------------------------------------------------------------------------- /src/components/ALink.vue: -------------------------------------------------------------------------------- 1 | <script lang="ts" setup> 2 | import { useBewlyApp } from '~/composables/useAppProvider' 3 | import { settings } from '~/logic' 4 | import { isHomePage } from '~/utils/main' 5 | 6 | const props = defineProps<{ 7 | href: string 8 | title?: string 9 | rel?: string 10 | type: 'topBar' | 'videoCard' 11 | customClickEvent?: boolean 12 | }>() 13 | 14 | const emit = defineEmits<{ 15 | (e: 'click', value: MouseEvent): void 16 | }>() 17 | 18 | const { openIframeDrawer } = useBewlyApp() 19 | 20 | const openMode = computed(() => { 21 | if (props.type === 'topBar') 22 | return settings.value.topBarLinkOpenMode 23 | else if (props.type === 'videoCard') 24 | return settings.value.videoCardLinkOpenMode 25 | return 'newTab' 26 | }) 27 | 28 | // Since BewlyBewly sometimes uses an iframe to open the original Bilibili page in the current tab 29 | // please set the target to `_top` instead of `_self` 30 | const target = computed(() => { 31 | if (openMode.value === 'newTab') { 32 | return '_blank' 33 | } 34 | if (openMode.value === 'currentTabIfNotHomepage') { 35 | return isHomePage() ? '_blank' : '_top' 36 | } 37 | if (openMode.value === 'currentTab') { 38 | return '_top' 39 | } 40 | return '_top' 41 | }) 42 | 43 | function handleClick(event: MouseEvent) { 44 | if (event.ctrlKey || event.metaKey || event.altKey) 45 | return 46 | 47 | if (props.customClickEvent) { 48 | event.preventDefault() 49 | emit('click', event) 50 | return 51 | } 52 | 53 | if (openMode.value === 'drawer') { 54 | event.preventDefault() 55 | openIframeDrawer(props.href) 56 | } 57 | } 58 | </script> 59 | 60 | <template> 61 | <a :href="href" :target="target" :title="title" :rel="rel" @click="handleClick"> 62 | <slot /> 63 | </a> 64 | </template> 65 | -------------------------------------------------------------------------------- /src/components/BangumiCard/BangumiCardSkeleton.vue: -------------------------------------------------------------------------------- 1 | <script setup lang="ts"> 2 | defineProps<{ 3 | horizontal?: boolean 4 | }>() 5 | </script> 6 | 7 | <template> 8 | <div 9 | :style="{ 10 | display: horizontal ? 'flex' : 'block', 11 | }" 12 | gap-4 13 | mb-6 14 | > 15 | <div 16 | rounded="$bew-radius" aspect="12/16" overflow-hidden mb-4 bg="$bew-skeleton" 17 | shrink-0 18 | :style="{ width: horizontal ? '170px' : '100%' }" 19 | /> 20 | <div w-full> 21 | <p 22 | w-full h-5 mt-2 mb-3 my-4 23 | bg="$bew-skeleton" 24 | rounded-4px 25 | /> 26 | <div text="$bew-skeleton" mb-10 flex items-center rounded-4px> 27 | <div 28 | text="transparent" bg="$bew-skeleton" p="x-3 y-1" mr-2 h-24px 29 | rounded-4 30 | > 31 | 0.0 32 | </div> 33 | <div w="60%" h-22px bg="$bew-skeleton" rounded-4px /> 34 | </div> 35 | </div> 36 | </div> 37 | </template> 38 | -------------------------------------------------------------------------------- /src/components/CodeEditor.vue: -------------------------------------------------------------------------------- 1 | <script setup lang="ts"> 2 | defineProps<{ 3 | modelValue: string | number 4 | }>() 5 | 6 | const model = defineModel<string | number>() 7 | </script> 8 | 9 | <template> 10 | <textarea 11 | v-model="model" 12 | w-full h-500px border="1 solid $bew-border-color" rounded="4px" p-2 13 | outline-none bg="$bew-fill-1" 14 | /> 15 | </template> 16 | 17 | <style lang="scss" scoped> 18 | textarea { 19 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; /* 1 */ 20 | } 21 | </style> 22 | -------------------------------------------------------------------------------- /src/components/Dock/types.ts: -------------------------------------------------------------------------------- 1 | export interface HoveringDockItem { 2 | themeMode: boolean 3 | settings: boolean 4 | } 5 | -------------------------------------------------------------------------------- /src/components/Empty.vue: -------------------------------------------------------------------------------- 1 | <script setup lang="ts"> 2 | import browser from 'webextension-polyfill' 3 | 4 | const props = defineProps<{ 5 | description?: string 6 | }>() 7 | 8 | const emptyImg = browser.runtime.getURL('/assets/empty.png') 9 | </script> 10 | 11 | <template> 12 | <div flex="~ col gap-4" justify="center" items="center"> 13 | <img :src="emptyImg" w="200px" h="auto"> 14 | <span v-if="props.description" text="$bew-text-3">{{ props.description }}</span> 15 | <slot /> 16 | </div> 17 | </template> 18 | -------------------------------------------------------------------------------- /src/components/HorizontalScrollView.vue: -------------------------------------------------------------------------------- 1 | <script setup lang="ts"> 2 | import type { Ref } from 'vue' 3 | 4 | import { settings } from '~/logic' 5 | 6 | const scrollListWrap = ref<HTMLElement>() as Ref<HTMLElement> 7 | // const showLeftMask = ref<boolean>(false) 8 | // const showRightMask = ref<boolean>(false) 9 | const showScrollMask = ref<boolean>(true) 10 | 11 | watch([() => settings.value.enableHorizontalScrolling, scrollListWrap], ([enableHorizontalScrolling, scrollListWrap]) => { 12 | if (!scrollListWrap) 13 | return 14 | 15 | if (enableHorizontalScrolling) 16 | scrollListWrap.addEventListener('wheel', handleMouseScroll) 17 | else 18 | scrollListWrap.removeEventListener('wheel', handleMouseScroll) 19 | }) 20 | 21 | onMounted(() => { 22 | // scrollListWrap.value.addEventListener('scroll', () => { 23 | // if (scrollListWrap.value.scrollLeft > 0) 24 | // showScrollMask.value = true 25 | 26 | // else 27 | // showScrollMask.value = false 28 | 29 | // if ( 30 | // scrollListWrap.value.scrollLeft + scrollListWrap.value.clientWidth 31 | // >= scrollListWrap.value.scrollWidth - 20 32 | // ) 33 | // showScrollMask.value = false 34 | // }) 35 | }) 36 | 37 | function handleMouseScroll(event: WheelEvent) { 38 | event.preventDefault() 39 | scrollListWrap.value.scrollLeft += event.deltaY 40 | } 41 | </script> 42 | 43 | <template> 44 | <div relative> 45 | <!-- <transition name="fade"> 46 | <div 47 | v-show="showLeftMask" 48 | h-full 49 | w-80px 50 | absolute 51 | z-1 52 | style="mask-image: linear-gradient(to left, transparent, black); mask-mode: alpha;" 53 | /> 54 | </transition> 55 | <transition name="fade"> 56 | <div 57 | v-show="showRightMask" 58 | h-full 59 | w-80px 60 | pos="absolute right-0" 61 | z-1 62 | style=" 63 | background: linear-gradient(to right, transparent, var(--bew-bg)); 64 | " 65 | /> 66 | </transition> --> 67 | 68 | <div 69 | ref="scrollListWrap" 70 | w="[calc(100%+80px)]" 71 | h="[calc(100%+40px)]" 72 | m="x--40px y--20px" p="x-40px y-20px" 73 | overflow-x-scroll 74 | overflow-y-hidden 75 | relative 76 | :class="{ 'scroll-mask': showScrollMask }" 77 | > 78 | <slot /> 79 | </div> 80 | </div> 81 | </template> 82 | 83 | <style lang="scss" scoped> 84 | .scroll-mask { 85 | mask-image: linear-gradient(to right, transparent 0%, black 40px calc(100% - 40px), transparent 100%); 86 | -webkit-mask-image: linear-gradient(to right, transparent 0%, black 40px calc(100% - 40px), transparent 100%); 87 | } 88 | </style> 89 | -------------------------------------------------------------------------------- /src/components/IframePage.vue: -------------------------------------------------------------------------------- 1 | <script setup lang="ts"> 2 | import { useDark } from '~/composables/useDark' 3 | 4 | const props = defineProps<{ 5 | url: string 6 | }>() 7 | const { isDark } = useDark() 8 | const headerShow = ref(false) 9 | const iframeRef = ref<HTMLIFrameElement | null>(null) 10 | const currentUrl = ref<string>(props.url) 11 | const showIframe = ref<boolean>(false) 12 | const showLoading = ref<boolean>(false) 13 | 14 | watch(() => isDark.value, (newValue) => { 15 | iframeRef.value?.contentDocument?.documentElement.classList.toggle('dark', newValue) 16 | iframeRef.value?.contentDocument?.body?.classList.toggle('dark', newValue) 17 | }) 18 | 19 | watch(() => props.url, () => { 20 | showIframe.value = false 21 | }) 22 | 23 | // Only show loading animation after 1.5 seconds to prevent annoying flash when content loads quickly 24 | const showLoadingTimeout = ref() 25 | watch(() => showIframe.value, async (newValue) => { 26 | clearTimeout(showLoadingTimeout.value) 27 | if (!newValue) { 28 | showLoadingTimeout.value = setTimeout(() => { 29 | showLoading.value = true 30 | }, 1500) 31 | } 32 | else { 33 | showLoading.value = false 34 | } 35 | }, { immediate: true }) 36 | 37 | onMounted(() => { 38 | nextTick(() => { 39 | iframeRef.value?.focus() 40 | }) 41 | }) 42 | 43 | onBeforeUnmount(() => { 44 | releaseIframeResources() 45 | }) 46 | 47 | async function releaseIframeResources() { 48 | // Clear iframe content 49 | currentUrl.value = 'about:blank' 50 | /** 51 | * eg: When use 'iframeRef.value?.contentWindow?.document' of t.bilibili.com iframe on bilibili.com, there may be cross domain issues 52 | * set the src to 'about:blank' to avoid this issue, it also can release the memory 53 | */ 54 | if (iframeRef.value) { 55 | iframeRef.value.src = 'about:blank' 56 | } 57 | await nextTick() 58 | iframeRef.value?.contentWindow?.close() 59 | 60 | // Remove iframe from the DOM 61 | iframeRef.value?.parentNode?.removeChild(iframeRef.value) 62 | await nextTick() 63 | 64 | // Nullify the reference 65 | iframeRef.value = null 66 | } 67 | 68 | function handleBackToTop() { 69 | if (iframeRef.value) { 70 | iframeRef.value.contentWindow?.scrollTo({ top: 0, behavior: 'smooth' }) 71 | } 72 | } 73 | 74 | function handleRefresh() { 75 | if (iframeRef.value) { 76 | iframeRef.value.contentWindow?.location.reload() 77 | } 78 | } 79 | 80 | defineExpose({ 81 | handleBackToTop, 82 | handleRefresh, 83 | }) 84 | </script> 85 | 86 | <template> 87 | <div 88 | pos="relative top-0 left-0" of-hidden w-full h-full 89 | > 90 | <Transition name="fade"> 91 | <Loading v-if="showLoading" w-full h-full pos="absolute top-0 left-0" /> 92 | </Transition> 93 | <Transition name="fade"> 94 | <!-- Iframe --> 95 | <iframe 96 | v-show="showIframe" 97 | ref="iframeRef" 98 | :src="props.url" 99 | :style="{ 100 | bottom: headerShow ? `var(--bew-top-bar-height)` : '0', 101 | }" 102 | frameborder="0" 103 | pointer-events-auto 104 | pos="absolute left-0" 105 | w-inherit h-inherit 106 | @load="showIframe = true" 107 | /> 108 | </Transition> 109 | </div> 110 | </template> 111 | 112 | <style lang="scss" scoped> 113 | 114 | </style> 115 | -------------------------------------------------------------------------------- /src/components/Input.vue: -------------------------------------------------------------------------------- 1 | <script lang="ts" setup> 2 | type Size = 'small' | 'medium' | 'large' 3 | interface Props { 4 | size?: Size 5 | type?: 'text' | 'password' | 'email' | 'number' 6 | min?: number 7 | max?: number 8 | placeholder?: string 9 | } 10 | const props = withDefaults(defineProps<Props>(), { size: 'medium' }) 11 | 12 | defineEmits(['enter']) 13 | 14 | const modelValue = defineModel<string | number>() 15 | 16 | const inputRef = ref<HTMLInputElement | null>(null) 17 | 18 | const height = computed(() => { 19 | if (props.size === 'small') 20 | return '30px' 21 | if (props.size === 'medium') 22 | return '35px' 23 | if (props.size === 'large') 24 | return '40px' 25 | return '35px' 26 | }) 27 | 28 | const padding = computed(() => { 29 | if (props.size === 'small') 30 | return '0 calc(var(--bew-base-font-size) * 0.5)' 31 | return '0 var(--bew-base-font-size)' 32 | }) 33 | 34 | function focus() { 35 | inputRef.value?.focus() 36 | } 37 | 38 | defineExpose({ focus }) 39 | </script> 40 | 41 | <template> 42 | <div 43 | :style="{ height, padding }" 44 | focus-within:ring="2px $bew-theme-color" 45 | p="x-4" 46 | rounded="$bew-radius" transition-all duration-300 47 | bg="$bew-fill-1" flex="~ gap-2" 48 | > 49 | <div v-if="$slots.prefix" class="prefix"> 50 | <div> 51 | <slot name="prefix" /> 52 | </div> 53 | </div> 54 | 55 | <input 56 | ref="inputRef" 57 | v-model="modelValue" 58 | :style="{ lineHeight: height }" 59 | :type="type" 60 | :min="min" 61 | :max="max" 62 | :placeholder="placeholder" 63 | w-inherit h-inherit 64 | outline-none flex-1 bg-transparent 65 | @keydown.enter="$emit('enter')" 66 | > 67 | 68 | <div v-if="$slots.suffix" class="suffix"> 69 | <div> 70 | <slot name="suffix" /> 71 | </div> 72 | </div> 73 | </div> 74 | </template> 75 | 76 | <style lang="scss" scoped> 77 | .prefix, 78 | .suffix { 79 | --uno: "flex items-center"; 80 | } 81 | </style> 82 | -------------------------------------------------------------------------------- /src/components/List/List.vue: -------------------------------------------------------------------------------- 1 | <script setup lang="ts"> 2 | withDefaults(defineProps<{ 3 | highlightFirst?: boolean 4 | pinTop?: boolean 5 | }>(), { 6 | highlightFirst: false, 7 | }) 8 | </script> 9 | 10 | <template> 11 | <div 12 | class="b-list" 13 | :class="{ 'highlight-first': highlightFirst, 'pin-top': pinTop }" 14 | > 15 | <slot /> 16 | </div> 17 | </template> 18 | 19 | <style lang="scss" scoped> 20 | .b-list { 21 | &.highlight-first :deep(.b-list-item:first-child) { 22 | --uno: "!bg-$bew-fill-2 !font-bold"; 23 | } 24 | 25 | &.pin-top :deep(.b-list-item:first-child) { 26 | --uno: "sticky top-0 z-1"; 27 | } 28 | } 29 | </style> 30 | -------------------------------------------------------------------------------- /src/components/List/ListItem.vue: -------------------------------------------------------------------------------- 1 | <script setup lang="ts"> 2 | 3 | </script> 4 | 5 | <template> 6 | <div 7 | class="b-list-item" 8 | p="x-4 y-2" 9 | flex="~ gap-2" 10 | bg="odd:$bew-fill-1 hover:!$bew-fill-2" 11 | rounded="$bew-radius" 12 | duration-300 13 | > 14 | <slot /> 15 | </div> 16 | </template> 17 | 18 | <style lang="scss" scoped> 19 | .b-list-item { 20 | > * { 21 | --uno: "flex items-center flex-1 shrink-0"; 22 | } 23 | 24 | & + & { 25 | --uno: "mt-1 border-$bew-border-color"; 26 | } 27 | } 28 | </style> 29 | -------------------------------------------------------------------------------- /src/components/Loading.vue: -------------------------------------------------------------------------------- 1 | <script setup lang="ts"> 2 | import browser from 'webextension-polyfill' 3 | 4 | const imgURL = browser.runtime.getURL('/assets/loading.gif') 5 | </script> 6 | 7 | <template> 8 | <div 9 | w="full" 10 | min-h="46px" 11 | p="y-8" 12 | flex="~" 13 | justify="center" 14 | items="center" 15 | > 16 | <img 17 | :src="imgURL" 18 | alt="loading" 19 | w="46px" 20 | h="46px" 21 | m="r-2" 22 | > 23 | {{ $t('common.loading') }} 24 | </div> 25 | </template> 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 | <script setup lang="ts"> 2 | defineProps<{ 3 | src: string 4 | loading: 'lazy' | 'eager' 5 | alt?: string 6 | }>() 7 | </script> 8 | 9 | <template> 10 | <picture> 11 | <source :srcset="`${src}.avif`" type="image/avif"> 12 | <source :srcset="`${src}.webp`" type="image/webp"> 13 | <img 14 | :src="src" 15 | :loading="loading" 16 | :alt="alt" 17 | block w-full h-full object="[inherit]" aspect-inherit 18 | rounded-inherit 19 | > 20 | </picture> 21 | </template> 22 | -------------------------------------------------------------------------------- /src/components/Progress.vue: -------------------------------------------------------------------------------- 1 | <script setup lang="ts"> 2 | interface Props { 3 | percentage: number 4 | color?: string 5 | height?: number | string 6 | } 7 | 8 | const props = withDefaults(defineProps<Props>(), { 9 | color: 'var(--bew-theme-color)', 10 | }) 11 | </script> 12 | 13 | <template> 14 | <div 15 | h="6px" 16 | rounded="$bew-radius" 17 | :style="{ 18 | width: `${percentage}%`, 19 | backgroundColor: props.color, 20 | height: 21 | typeof props.height === 'number' ? `${props.height}px` : props.height, 22 | }" 23 | /> 24 | </template> 25 | 26 | <style lang="scss" scoped></style> 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 | <script lang="ts" setup> 2 | defineProps<{ 3 | modelValue: boolean 4 | label?: string 5 | }>() 6 | 7 | const model = defineModel() 8 | </script> 9 | 10 | <template> 11 | <label cursor="pointer" pointer="auto" flex items-center gap-3> 12 | <span>{{ label }}</span> 13 | <input v-model="model" type="checkbox" hidden> 14 | <span 15 | inline-block w="$b-button-width" h="$b-button-height" bg="$bew-fill-1" rounded="[calc(var(--b-button-height)/2)]" 16 | relative border="size-$b-border-width color-$bew-border-color" 17 | after:content-empty after:inline-block after:bg-white after:rounded="[calc(var(--b-button-height)/2)]" 18 | after:w="[calc(var(--b-button-height)-var(--b-border-width))]" after:h="[calc(var(--b-button-height)-var(--b-border-width))]" 19 | after:border="size-$b-border-width color-$bew-border-color" 20 | after:pos="absolute top-[calc(0px-var(--b-border-width)/2)]" 21 | /> 22 | </label> 23 | </template> 24 | 25 | <style lang="scss" scoped> 26 | label { 27 | --b-button-width: 50px; 28 | --b-button-height: 25px; 29 | --b-border-width: 2px; 30 | } 31 | 32 | input[type="checkbox"] + span::after { 33 | box-sizing: border-box; 34 | } 35 | 36 | input[type="checkbox"] { 37 | &:hover + span { 38 | --uno: "bg-$bew-fill-2"; 39 | } 40 | 41 | &:active + span::after { 42 | --uno: "scale-90"; 43 | } 44 | 45 | &:checked + span { 46 | --uno: "bg-$bew-theme-color-60 border-$bew-theme-color"; 47 | } 48 | 49 | &:checked:hover + span { 50 | --uno: "bg-$bew-theme-color-80 border-$bew-theme-color"; 51 | box-shadow: 52 | 0 0 6px 2px var(--bew-theme-color-40), 53 | inset 0 0 6px var(--bew-theme-color-30); 54 | } 55 | 56 | & + span, 57 | & + span::after { 58 | transition: 0.3s cubic-bezier(0.25, 0.15, 0.29, 1.51); 59 | } 60 | 61 | &:checked + span::after { 62 | --uno: "border-$bew-theme-color translate-x-full"; 63 | } 64 | } 65 | </style> 66 | -------------------------------------------------------------------------------- /src/components/Select.vue: -------------------------------------------------------------------------------- 1 | <script setup lang="ts"> 2 | const props = defineProps<{ 3 | options: OptionType[] 4 | modelValue: any 5 | }>() 6 | 7 | const emit = defineEmits(['update:modelValue', 'change']) 8 | 9 | interface OptionType { 10 | value: any 11 | label: string 12 | } 13 | 14 | const label = ref<string>('') 15 | const showOptions = ref<boolean>(false) 16 | 17 | onUpdated(() => { 18 | // fix the issue when the dropdown menu text doesn't update in real-time based on the updated page language 19 | if (props.options) 20 | label.value = `${props.options.find((item: OptionType) => item.value === props.modelValue)?.label}` 21 | }) 22 | 23 | onMounted(() => { 24 | if (props.options) 25 | label.value = `${props.options.find((item: OptionType) => item.value === props.modelValue)?.label}` 26 | }) 27 | 28 | function onClickOption(val: OptionType) { 29 | window.removeEventListener('click', () => {}) 30 | label.value = val.label 31 | emit('update:modelValue', val.value) 32 | emit('change', val.value) 33 | showOptions.value = false 34 | } 35 | 36 | function closeOptions() { 37 | showOptions.value = false 38 | } 39 | 40 | /** when you click on it outside, the selection option will be turned off */ 41 | function onMouseLeave() { 42 | window.addEventListener('click', closeOptions) 43 | } 44 | 45 | function onMouseEnter() { 46 | window.removeEventListener('click', closeOptions) 47 | } 48 | </script> 49 | 50 | <template> 51 | <div 52 | pos="relative" 53 | @mouseleave="onMouseLeave" 54 | @mouseenter="onMouseEnter" 55 | > 56 | <div 57 | p="x-4 y-2" 58 | bg="$bew-fill-1" 59 | rounded="$bew-radius" 60 | text="center $bew-text-1" 61 | cursor="pointer" 62 | flex="~" 63 | justify="between" 64 | items="center" w="full" 65 | :ring="showOptions ? '2px $bew-theme-color' : ''" duration-300 66 | @click="showOptions = !showOptions" 67 | > 68 | <div 69 | truncate 70 | overflow="hidden" 71 | m="r-2" 72 | v-text="label === 'undefined' ? '' : label" 73 | /> 74 | 75 | <!-- arrow --> 76 | <div 77 | border="~ solid t-0 l-0 r-2 b-2" 78 | :border-color="showOptions ? '$bew-theme-color' : '$bew-fill-4'" 79 | p="3px" 80 | m="l-2" 81 | display="inline-block" 82 | :transform="`~ ${!showOptions ? 'rotate-45 -translate-y-1/4' : 'rotate-225 translate-y-1/4'} `" 83 | transition="all duration-300" 84 | /> 85 | </div> 86 | <Transition name="dropdown"> 87 | <div 88 | v-if="showOptions" 89 | style="backdrop-filter: var(--bew-filter-glass-1)" 90 | pos="absolute" bg="$bew-elevated" shadow="$bew-shadow-2" p="2" 91 | m="t-2" 92 | rounded="$bew-radius" z="1" flex="~ col gap-1" 93 | w="full" max-h-300px overflow-y-overlay will-change-transform transform-gpu 94 | > 95 | <div 96 | v-for="option in options" 97 | :key="option.value" 98 | p="x-2 y-2" 99 | rounded="$bew-radius" 100 | w="full" 101 | bg="hover:$bew-fill-2" 102 | transition="all duration-300" 103 | cursor="pointer" 104 | @click="onClickOption(option)" 105 | > 106 | <span v-text="option.label" /> 107 | </div> 108 | </div> 109 | </Transition> 110 | </div> 111 | </template> 112 | 113 | <style lang="scss" scoped> 114 | </style> 115 | -------------------------------------------------------------------------------- /src/components/Settings/BIlibiliSettings/BilibiliSettings.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div flex="~ justify-between gap-4"> 3 | <aside> 4 | <ul flex="~ col gap-1" ml--4> 5 | <li p="x-4 y-2" bg="hover:$bew-fill-2" rounded="$bew-radius"> 6 | home page 7 | </li> 8 | <li p="x-4 y-2" bg="hover:$bew-fill-2" rounded="$bew-radius"> 9 | video page 10 | </li> 11 | <li p="x-4 y-2" bg="hover:$bew-fill-2" rounded="$bew-radius"> 12 | moments page 13 | </li> 14 | </ul> 15 | </aside> 16 | <main flex-1> 17 | <span text="8xl">WIP...</span> 18 | </main> 19 | </div> 20 | </template> 21 | -------------------------------------------------------------------------------- /src/components/Settings/BewlyPages/BewlyPages.vue: -------------------------------------------------------------------------------- 1 | <script setup lang="ts"> 2 | import { ref } from 'vue' 3 | import { useI18n } from 'vue-i18n' 4 | 5 | import { BewlyPage } from '../types' 6 | 7 | const { t } = useI18n() 8 | 9 | const activePage = ref<BewlyPage>(BewlyPage.Home) 10 | 11 | const pages = [ 12 | { 13 | value: BewlyPage.Home, 14 | title: t('settings.menu_home'), 15 | icon: 'i-mingcute:home-5-line', 16 | iconActivated: 'i-mingcute:home-5-fill', 17 | component: defineAsyncComponent(() => import('./Home/Home.vue')), 18 | }, 19 | { 20 | value: BewlyPage.Search, 21 | title: t('settings.menu_search_page'), 22 | icon: 'i-mingcute:search-2-line', 23 | iconActivated: 'i-mingcute:search-2-fill', 24 | component: defineAsyncComponent(() => import('./SearchPage/SearchPage.vue')), 25 | }, 26 | ] 27 | </script> 28 | 29 | <template> 30 | <div flex="~ gap-2"> 31 | <!-- Sidebar --> 32 | <div w-140px> 33 | <div w-inherit pos="fixed"> 34 | <ul flex="~ col gap-1"> 35 | <li 36 | v-for="page in pages" 37 | :key="page.value" 38 | :style="{ backgroundColor: activePage === page.value ? 'var(--bew-fill-3)' : '' }" 39 | cursor-pointer p="y-2 x-4" ml--4 rounded="$bew-radius" bg="hover:$bew-fill-2" 40 | duration-300 41 | @click="activePage = page.value" 42 | > 43 | <div class="flex items-center"> 44 | <div :class="activePage === page.value ? page.iconActivated : page.icon" class="mr-2 text-lg" /> 45 | <span>{{ page.title }}</span> 46 | </div> 47 | </li> 48 | </ul> 49 | </div> 50 | </div> 51 | 52 | <!-- Content --> 53 | <div class="flex-1 p-4"> 54 | <Transition name="page-fade"> 55 | <Component :is="pages.find(page => page.value === activePage)?.component" /> 56 | </Transition> 57 | </div> 58 | </div> 59 | </template> 60 | -------------------------------------------------------------------------------- /src/components/Settings/Compatibility/Compatibility.vue: -------------------------------------------------------------------------------- 1 | <script lang="ts" setup> 2 | import { settings } from '~/logic' 3 | import { isHomePage } from '~/utils/main' 4 | 5 | import SettingsItem from '../components/SettingsItem.vue' 6 | import SettingsItemGroup from '../components/SettingsItemGroup.vue' 7 | 8 | watch(() => settings.value.useOriginalBilibiliHomepage, () => { 9 | if (isHomePage()) 10 | location.reload() 11 | }) 12 | 13 | const bilibiliEvolvedThemeColor = computed(() => { 14 | return getComputedStyle(document.querySelector('html') as HTMLElement).getPropertyValue('--theme-color').trim() ?? '#00a1d6' 15 | }) 16 | 17 | function changeThemeColor(color: string) { 18 | settings.value.themeColor = color 19 | } 20 | </script> 21 | 22 | <template> 23 | <div> 24 | <SettingsItemGroup :title="$t('settings.group_common')"> 25 | <SettingsItem :title="$t('settings.topbar_visibility')" :desc="$t('settings.topbar_visibility_desc')"> 26 | <Radio v-model="settings.showTopBar" :label="settings.showTopBar ? $t('settings.chk_box.show') : $t('settings.chk_box.hidden')" /> 27 | </SettingsItem> 28 | <SettingsItem :title="$t('settings.use_original_bilibili_topbar')"> 29 | <Radio v-model="settings.useOriginalBilibiliTopBar" /> 30 | </SettingsItem> 31 | <SettingsItem :title="$t('settings.use_original_bilibili_homepage')"> 32 | <template #desc> 33 | <span color="$bew-error-color" v-text="$t('settings.use_original_bilibili_homepage_desc')" /> 34 | </template> 35 | <Radio v-model="settings.useOriginalBilibiliHomepage" /> 36 | </SettingsItem> 37 | <SettingsItem :title="$t('settings.adapt_to_other_page_styles')" :desc="$t('settings.adapt_to_other_page_styles_desc')"> 38 | <Radio v-model="settings.adaptToOtherPageStyles" /> 39 | </SettingsItem> 40 | </SettingsItemGroup> 41 | 42 | <SettingsItemGroup title="Bilibili Evolved"> 43 | <SettingsItem :title="$t('settings.follow_bilibili_evolved_color')" :desc="$t('settings.follow_bilibili_evolved_color_desc')"> 44 | <div 45 | w-20px h-20px rounded-8 cursor-pointer transition 46 | duration-300 box-border 47 | :style="{ 48 | background: bilibiliEvolvedThemeColor, 49 | transform: bilibiliEvolvedThemeColor === settings.themeColor ? 'scale(1.3)' : 'scale(1)', 50 | border: bilibiliEvolvedThemeColor === settings.themeColor ? '2px solid white' : '2px solid transparent', 51 | boxShadow: bilibiliEvolvedThemeColor === settings.themeColor ? '0 0 0 1px var(--bew-border-color), var(--bew-shadow-1)' : 'none', 52 | }" 53 | @click="changeThemeColor(bilibiliEvolvedThemeColor)" 54 | /> 55 | </SettingsItem> 56 | </SettingsItemGroup> 57 | </div> 58 | </template> 59 | 60 | <style lang="scss" scoped> 61 | 62 | </style> 63 | -------------------------------------------------------------------------------- /src/components/Settings/components/SettingsItem.vue: -------------------------------------------------------------------------------- 1 | <script setup lang="ts"> 2 | defineProps<{ 3 | title?: string 4 | desc?: string 5 | }>() 6 | </script> 7 | 8 | <template> 9 | <div class="b-settings-item" py-4> 10 | <div flex="~ gap-4" justify-between items-center text-base> 11 | <div class="left-content" w="5/7"> 12 | <div> 13 | <slot name="title"> 14 | {{ title }} 15 | </slot> 16 | </div> 17 | 18 | <div 19 | text="sm $bew-text-2" 20 | :style="{ marginTop: $slots.desc || desc ? '0.25rem' : '0' }" 21 | > 22 | <slot name="desc"> 23 | {{ desc }} 24 | </slot> 25 | </div> 26 | </div> 27 | 28 | <div class="right-content" w="2/7"> 29 | <slot /> 30 | </div> 31 | </div> 32 | 33 | <div v-if="$slots.bottom" mt-4> 34 | <slot name="bottom" /> 35 | </div> 36 | </div> 37 | </template> 38 | 39 | <style lang="scss" scoped> 40 | :deep(.right-content > *) { 41 | --uno: "float-right clear-both"; 42 | } 43 | 44 | .b-settings-item + .b-settings-item { 45 | --uno: "border-t-1 border-$bew-border-color"; 46 | } 47 | </style> 48 | -------------------------------------------------------------------------------- /src/components/Settings/components/SettingsItemGroup.vue: -------------------------------------------------------------------------------- 1 | <script setup lang="ts"> 2 | defineProps<{ 3 | title?: string 4 | desc?: string 5 | }>() 6 | </script> 7 | 8 | <template> 9 | <div class="b-settings-item-group"> 10 | <p text="base $bew-text-1" fw-bold> 11 | {{ title }} 12 | </p> 13 | <p v-if="desc" text="sm $bew-text-2"> 14 | {{ desc }} 15 | </p> 16 | 17 | <main 18 | style="box-shadow: var(--bew-shadow-edge-glow-1), var(--bew-shadow-1);" 19 | mt-2 px-4 mx--4 rounded="$bew-radius" 20 | bg="$bew-fill-alt" 21 | shadow="$bew-shadow-edge-glow-1" 22 | > 23 | <slot /> 24 | </main> 25 | </div> 26 | </template> 27 | 28 | <style lang="scss" scoped> 29 | .b-settings-item-group + .b-settings-item-group { 30 | --uno: "mt-6"; 31 | } 32 | </style> 33 | -------------------------------------------------------------------------------- /src/components/Settings/types.ts: -------------------------------------------------------------------------------- 1 | export enum MenuType { 2 | General = 'General', 3 | DesktopAndDock = 'DesktopAndDock', 4 | Appearance = 'Appearance', 5 | BewlyPages = 'BewlyPages', 6 | Compatibility = 'Compatibility', 7 | BilibiliSettings = 'BilibiliSettings', 8 | About = 'About', 9 | } 10 | 11 | export enum BewlyPage { 12 | Home = 'Home', 13 | Search = 'Search', 14 | } 15 | 16 | export interface MenuItem { 17 | value: MenuType 18 | title: string 19 | icon: string 20 | iconActivated: string 21 | } 22 | -------------------------------------------------------------------------------- /src/components/SideBar/types.ts: -------------------------------------------------------------------------------- 1 | export interface HoveringDockItem { 2 | themeMode: boolean 3 | settings: boolean 4 | } 5 | -------------------------------------------------------------------------------- /src/components/Slider.vue: -------------------------------------------------------------------------------- 1 | <script lang="ts" setup> 2 | import type { Ref } from 'vue' 3 | 4 | interface Props { 5 | min?: number 6 | max?: number 7 | modelValue: number 8 | label: string 9 | } 10 | const props = withDefaults(defineProps<Props>(), { 11 | min: 0, 12 | max: 100, 13 | }) 14 | 15 | const emit = defineEmits(['update:modelValue']) 16 | 17 | const modelValue = ref<number>(props.modelValue) 18 | const rangeRef = ref<HTMLInputElement>() as Ref<HTMLInputElement> 19 | 20 | onMounted(() => { 21 | modelValue.value = props.modelValue 22 | const progress = (modelValue.value / Number(rangeRef.value.max)) * 100 23 | 24 | rangeRef.value.style.background = `linear-gradient(to right, var(--bew-theme-color) ${progress}%, var(--bew-fill-1) ${progress}%) no-repeat` 25 | 26 | if (rangeRef.value) { 27 | rangeRef.value.addEventListener('input', (event: Event) => { 28 | const tempSliderValue = Number((event.target as HTMLInputElement).value) 29 | emit('update:modelValue', Number(tempSliderValue)) 30 | 31 | const progress = (tempSliderValue / Number(rangeRef.value.max)) * 100 32 | 33 | rangeRef.value.style.background = `linear-gradient(to right, var(--bew-theme-color) ${progress}%, var(--bew-fill-1) ${progress}%) no-repeat` 34 | }) 35 | } 36 | }) 37 | </script> 38 | 39 | <template> 40 | <label cursor-pointer flex items-center gap-3 w="$b-slider-width"> 41 | <input 42 | ref="rangeRef" 43 | v-model="modelValue" type="range" :min="min" :max="max" class="slider" 44 | appearance-none outline-none bg="$bew-fill-1" rounded="$b-slider-height" 45 | border="size-$b-border-width color-$bew-border-color" w="$b-slider-width" h="$b-slider-height" 46 | > 47 | <span>{{ label }}</span> 48 | </label> 49 | </template> 50 | 51 | <style lang="scss" scoped> 52 | label { 53 | --b-border-width: 2px; 54 | --b-slider-height: 10px; 55 | --b-slider-width: 100%; 56 | --b-thumb-width: calc(20px - var(--b-border-width)); 57 | --b-thumb-height: calc(20px - var(--b-border-width)); 58 | } 59 | 60 | input[type="range"] { 61 | &::-webkit-slider-thumb { 62 | --uno: "appearance-none w-$b-thumb-height h-$b-thumb-height bg-white rounded-$b-thumb-height"; 63 | --uno: "ring-$bew-border-color ring-2 cursor-pointer duration-300"; 64 | } 65 | 66 | &::-webkit-slider-thumb:hover { 67 | --uno: "ring-$bew-theme-color"; 68 | } 69 | 70 | &::-moz-range-thumb { 71 | --uno: "appearance-none w-$b-thumb-height h-$b-thumb-height bg-white rounded-$b-thumb-height"; 72 | --uno: "ring-$bew-border-color ring-2 cursor-pointer duration-300"; 73 | } 74 | 75 | &::-moz-range-thumb:hover { 76 | --uno: "ring-$bew-theme-color"; 77 | } 78 | } 79 | </style> 80 | -------------------------------------------------------------------------------- /src/components/Tooltip.vue: -------------------------------------------------------------------------------- 1 | <script lang="ts" setup> 2 | defineProps<{ 3 | content: string 4 | placement: 'left' | 'right' | 'top' | 'bottom' | 'bottom-left' | 'bottom-right' 5 | type?: 'default' | 'dark' | 'white' 6 | }>() 7 | 8 | const tooltipPos = ref({ left: 0, top: 0 }) 9 | const tooltipRef = ref(null) 10 | </script> 11 | 12 | <template> 13 | <span 14 | class="b-tooltip-wrapper" 15 | :style="{ 16 | top: `${tooltipPos.top}px`, 17 | left: `${tooltipPos.left}px`, 18 | }" 19 | > 20 | <div 21 | ref="tooltipRef" 22 | class="b-tooltip" 23 | :class="[`b-tooltip--placement-${placement ?? 'top'}`, `b-tooltip--type-${type ?? 'default'}`]" 24 | > 25 | {{ content }} 26 | </div> 27 | <slot /> 28 | </span> 29 | </template> 30 | 31 | <style lang="scss" scoped> 32 | .b-tooltip-wrapper { 33 | --uno: "flex items-center relative"; 34 | 35 | .b-tooltip { 36 | --uno: "absolute px-2 lh-2em rounded-8 pointer-events-none text-sm opacity-0 duration-300 shadow-$bew-shadow-2 whitespace-nowrap"; 37 | 38 | &--placement-right { 39 | --uno: "left-[calc(100%+0.5em)]"; 40 | } 41 | 42 | &--placement-left { 43 | --uno: "right-[calc(100%+0.5em)]"; 44 | } 45 | 46 | &--placement-top { 47 | --uno: "top--2.5em left-1/2 translate-x--1/2"; 48 | } 49 | 50 | &--placement-bottom { 51 | --uno: "bottom--2.5em left-1/2 translate-x--1/2"; 52 | } 53 | 54 | &--placement-bottom-left { 55 | --uno: "bottom--2.5em left--2"; 56 | } 57 | 58 | &--placement-bottom-right { 59 | --uno: "bottom--2.5em right--2"; 60 | } 61 | 62 | &--type-default { 63 | --uno: "text-white dark:text-black bg-black dark:bg-white"; 64 | } 65 | 66 | &--type-dark { 67 | --uno: "text-white bg-black"; 68 | } 69 | 70 | &--type-white { 71 | --uno: "text-black bg-white"; 72 | } 73 | } 74 | 75 | &:hover .b-tooltip { 76 | --uno: "opacity-100"; 77 | } 78 | } 79 | </style> 80 | -------------------------------------------------------------------------------- /src/components/TopBar/BewlyOrBiliTopBarSwitcher.vue: -------------------------------------------------------------------------------- 1 | <script setup lang="ts"> 2 | import { settings } from '~/logic' 3 | 4 | function toggleBewlyTopBar() { 5 | settings.value.useOriginalBilibiliTopBar = !settings.value.useOriginalBilibiliTopBar 6 | settings.value.showTopBar = !settings.value.showTopBar 7 | } 8 | </script> 9 | 10 | <template> 11 | <div 12 | class="group" 13 | pos="fixed top-0 right-0" 14 | z-10 15 | w-full 16 | flex="~ items-center justify-center" 17 | m="t-[calc(var(--bew-top-bar-height)-20px)]" 18 | p="t-30px" 19 | > 20 | <button 21 | style="backdrop-filter: var(--bew-filter-glass-1);" 22 | pos="absolute" 23 | class="opacity-0 group-hover:opacity-100" 24 | transform="translate-y--100% group-hover:translate-y-0 hover:translate-y-0" 25 | flex="~ items-center gap-2" 26 | text="$bew-text-2 sm" 27 | bg="$bew-elevated" p="x-2 y-1" mt-2 28 | rounded="full" shadow="$bew-shadow-1" 29 | duration-300 30 | @click="toggleBewlyTopBar" 31 | > 32 | <i i-mingcute:transfer-3-line text-xs /> 33 | <span> 34 | <template v-if="settings.showTopBar"> 35 | {{ $t('topbar.switch_to_bili_top_bar') }} 36 | </template> 37 | <template v-else> 38 | {{ $t('topbar.switch_to_bewly_top_bar') }} 39 | </template> 40 | </span> 41 | </button> 42 | </div> 43 | </template> 44 | -------------------------------------------------------------------------------- /src/components/TopBar/components/BewlyOrBiliPageSwitcher.vue: -------------------------------------------------------------------------------- 1 | <script lang="ts" setup> 2 | import { useBewlyApp } from '~/composables/useAppProvider' 3 | import { IFRAME_PAGE_SWITCH_BEWLY, IFRAME_PAGE_SWITCH_BILI } from '~/constants/globalEvents' 4 | import { settings } from '~/logic' 5 | import { useMainStore } from '~/stores/mainStore' 6 | // import { useSettingsStore } from '~/stores/settingsStore' 7 | import { isHomePage, isInIframe } from '~/utils/main' 8 | 9 | const { activatedPage } = useBewlyApp() 10 | const { getDockItemByPage } = useMainStore() 11 | // const { getDockItemConfigByPage } = useSettingsStore() 12 | const options = readonly([ 13 | { 14 | name: 'BewlyBewly', 15 | shortName: 'Bewly', 16 | useOriginalBiliPage: false, 17 | }, 18 | { 19 | name: 'BiliBili', 20 | shortName: 'Bili', 21 | useOriginalBiliPage: true, 22 | }, 23 | ]) 24 | 25 | const showBewlyOrBiliPageSwitcher = computed(() => { 26 | if (settings.value.useOriginalBilibiliHomepage) 27 | return false 28 | if (!isInIframe() && getDockItemByPage(activatedPage.value)?.hasBewlyPage && isHomePage()) 29 | return true 30 | if (isInIframe() && getDockItemByPage(activatedPage.value)?.hasBewlyPage) 31 | return true 32 | // const dockItemConfig = getDockItemConfigByPage(activatedPage.value) 33 | // if (dockItemConfig?.useOriginalBiliPage && isInIframe()) 34 | // return true 35 | return false 36 | }) 37 | 38 | function switchPage(useOriginalBiliPage: boolean) { 39 | const dockItem = settings.value.dockItemsConfig.find(dockItem => dockItem.page === activatedPage.value) 40 | if (dockItem) { 41 | dockItem.useOriginalBiliPage = useOriginalBiliPage 42 | } 43 | 44 | if (isInIframe()) { 45 | if (useOriginalBiliPage) 46 | parent.postMessage(IFRAME_PAGE_SWITCH_BILI, '*') 47 | else 48 | parent.postMessage(IFRAME_PAGE_SWITCH_BEWLY, '*') 49 | } 50 | } 51 | </script> 52 | 53 | <template> 54 | <div 55 | v-if="showBewlyOrBiliPageSwitcher" 56 | class="bewly-bili-switcher" 57 | :class="{ 'disable-frosted-glass': settings.disableFrostedGlass }" 58 | style="backdrop-filter: var(--bew-filter-glass-1);" 59 | flex="~ gap-1" bg="$bew-elevated" p-1 rounded-full 60 | h-34px 61 | > 62 | <button 63 | v-for="option in options" :key="option.name" 64 | class="bewly-bili-switcher-button" 65 | :class="{ 66 | active: option.useOriginalBiliPage === isInIframe(), 67 | }" 68 | rounded-inherit text="$bew-text-2 hover:$bew-text-1 xs" p="x-2 lg:x-4" bg="hover:$bew-fill-2" 69 | fw-bold duration-300 70 | @click="switchPage(option.useOriginalBiliPage)" 71 | > 72 | <span class="hidden lg:block"> 73 | {{ option.name }} 74 | </span> 75 | <span class="block lg:hidden"> 76 | {{ option.shortName }} 77 | </span> 78 | </button> 79 | </div> 80 | </template> 81 | 82 | <style lang="scss" scoped> 83 | .force-white-icon .bewly-bili-switcher:not(.disable-frosted-glass) { 84 | background-color: color-mix(in oklab, var(--bew-elevated-solid), transparent 80%); 85 | } 86 | 87 | .force-white-icon .bewly-bili-switcher:not(.disable-frosted-glass) .bewly-bili-switcher-button { 88 | --uno: "text-white"; 89 | 90 | &:hover { 91 | --uno: "bg-white bg-opacity-20"; 92 | } 93 | 94 | &.active { 95 | --uno: "bg-white bg-opacity-30"; 96 | } 97 | } 98 | 99 | .active { 100 | --uno: "bg-$bew-fill-3 text-$bew-text-1"; 101 | } 102 | </style> 103 | -------------------------------------------------------------------------------- /src/components/TopBar/components/MorePop.vue: -------------------------------------------------------------------------------- 1 | <script setup lang="ts"> 2 | import { useI18n } from 'vue-i18n' 3 | 4 | import { getUserID } from '~/utils/main' 5 | 6 | const { t } = useI18n() 7 | 8 | const list = computed((): { name: string, url: string, icon: string }[] => [ 9 | { name: t('topbar.notifications'), url: '//message.bilibili.com', icon: 'i-mingcute:notification-line' }, 10 | { name: t('topbar.moments'), url: '//t.bilibili.com/', icon: 'i-tabler:windmill' }, 11 | { name: t('topbar.favorites'), url: `//space.bilibili.com/${getUserID() ?? ''}/favlist`, icon: 'i-mingcute:star-line' }, 12 | { name: t('topbar.history'), url: '//www.bilibili.com/history', icon: 'i-mingcute:time-line' }, 13 | { name: t('topbar.watch_later'), url: '//www.bilibili.com/watchlater/#/list', icon: 'i-mingcute:carplay-line' }, 14 | { name: t('topbar.creative_center'), url: '//member.bilibili.com/platform/home', icon: 'i-mingcute:bulb-line' }, 15 | ]) 16 | </script> 17 | 18 | <template> 19 | <div 20 | style="backdrop-filter: var(--bew-filter-glass-1);" 21 | h="[calc(100vh-100px)]" max-h-264px important-overflow-y-auto 22 | w="180px" 23 | bg="$bew-elevated" 24 | p="4" 25 | rounded="$bew-radius" 26 | flex="~ col" 27 | shadow="[var(--bew-shadow-edge-glow-1),var(--bew-shadow-3)]" 28 | border="1 $bew-border-color" 29 | > 30 | <ALink 31 | v-for="item in list" 32 | :key="item.name" 33 | :href="item.url" 34 | type="topBar" 35 | pos="relative" 36 | p="x-4 y-2" 37 | bg="hover:$bew-fill-2" 38 | rounded="$bew-radius" 39 | transition="all duration-300" 40 | m="b-1 last:b-0" 41 | flex="~" 42 | items="center" 43 | > 44 | <i :class="item.icon" class="mr-4" /> 45 | <span class="flex-1">{{ item.name }}</span> 46 | </ALink> 47 | </div> 48 | </template> 49 | -------------------------------------------------------------------------------- /src/components/TopBar/components/UploadPop.vue: -------------------------------------------------------------------------------- 1 | <script setup> 2 | import { useI18n } from 'vue-i18n' 3 | 4 | const { t } = useI18n() 5 | 6 | const list = computed(() => { 7 | return [ 8 | { 9 | name: t('topbar.upload_dropdown.article'), 10 | url: 'https://member.bilibili.com/platform/upload/text/apply', 11 | icon: 'i-solar:document-add-bold-duotone', 12 | }, 13 | { 14 | name: t('topbar.upload_dropdown.music'), 15 | url: 'https://member.bilibili.com/platform/upload/audio/frame', 16 | icon: 'i-solar:music-notes-bold-duotone', 17 | }, 18 | { 19 | name: t('topbar.upload_dropdown.sticker'), 20 | url: 'https://member.bilibili.com/platform/upload/sticker', 21 | icon: 'i-solar:sticker-smile-square-bold-duotone', 22 | }, 23 | { 24 | name: t('topbar.upload_dropdown.video'), 25 | url: 'https://member.bilibili.com/platform/upload/video/frame', 26 | icon: 'i-solar:video-frame-bold-duotone', 27 | }, 28 | { 29 | name: t('topbar.upload_dropdown.manager'), 30 | url: 'https://member.bilibili.com/platform/upload-manager/article', 31 | icon: 'i-solar:video-library-bold-duotone', 32 | }, 33 | ] 34 | }) 35 | </script> 36 | 37 | <template> 38 | <div 39 | style="backdrop-filter: var(--bew-filter-glass-1);" 40 | bg="$bew-elevated" 41 | rounded="$bew-radius" 42 | p="4" 43 | min-w="120px" 44 | shadow="[var(--bew-shadow-edge-glow-1),var(--bew-shadow-3)]" 45 | border="1 $bew-border-color" 46 | flex="~ col" 47 | > 48 | <a 49 | v-for="(item, index) in list" 50 | :key="index" 51 | class="upload-item" 52 | :href="item.url" 53 | target="_blank" 54 | flex="~ items-center gap-2" 55 | p="x-4 y-2" 56 | bg="hover:$bew-fill-2" 57 | rounded="$bew-radius" 58 | transition="all duration-300" 59 | m="b-1 last:b-0" 60 | > 61 | <i :class="item.icon" text="$bew-text-2" /> 62 | 63 | <div text-nowrap>{{ item.name }}</div> 64 | </a> 65 | </div> 66 | </template> 67 | 68 | <style lang="scss" scoped> 69 | 70 | </style> 71 | -------------------------------------------------------------------------------- /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 | } 16 | wallet: { 17 | mid: number 18 | bcoin_balance: number // b幣 19 | } 20 | is_senior_member: boolean 21 | } 22 | 23 | /** 24 | * Number of follower, following and published posts by user 25 | */ 26 | export interface UserStat { 27 | dynamic_count: number 28 | follower: number 29 | following: number 30 | } 31 | 32 | // 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 33 | export interface UnReadMessage { 34 | at: number 35 | chat: number 36 | like: number 37 | reply: number 38 | sys_msg: number 39 | up: number 40 | } 41 | 42 | export interface UnReadDm { 43 | // https://api.vc.bilibili.com/session_svr/v1/session_svr/single_unread?build=0&mobi_app=web&unread_type=0 44 | unfollow_unread: number 45 | follow_unread: number 46 | unfollow_push_msg: number 47 | dustbin_push_msg: number 48 | dustbin_unread: number 49 | biz_msg_unfollow_unread: number 50 | biz_msg_follow_unread: number 51 | } 52 | 53 | export enum MomentType { 54 | Video = 8, 55 | Article = 64, 56 | Bangumi = 512, 57 | PGC = 4097, 58 | Movie = 4098, 59 | TvShow = 4099, 60 | ChineseAnime = 4100, 61 | Documentary = 4101, 62 | } 63 | 64 | export interface FavoriteCategory { 65 | id: number 66 | fid: number 67 | mid: number 68 | attr: number 69 | title: string 70 | fav_state: number 71 | media_count: number 72 | } 73 | 74 | // 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 75 | export interface FavoriteResource { 76 | id: number 77 | type: number // 2:视频稿件 12:音频 21:视频合集 78 | title: string 79 | cover: string 80 | intro: string 81 | page: number // 视频分P数 82 | duration: number // 音频/视频时长 83 | /** UP主信息 */ 84 | upper: { 85 | mid: number 86 | name: string 87 | face: string 88 | } 89 | /** 状态数 */ 90 | cnt_info: { 91 | collect: number // 收藏数 92 | play: number // 播放数 93 | danmaku: number // 弹幕数 94 | } 95 | link: string 96 | ctime: number // 投稿时间 97 | pubtime: number // 发布时间 98 | fav_time: number // 收藏时间 99 | bv_id: string 100 | bvid: string 101 | } 102 | -------------------------------------------------------------------------------- /src/components/VideoCard/VideoCardAuthor/components/VideoCardAuthorAvatar.vue: -------------------------------------------------------------------------------- 1 | <script lang="ts" setup> 2 | import { removeHttpFromUrl } from '~/utils/main' 3 | 4 | import type { Author } from '../../types' 5 | import { getAuthorJumpUrl } from '../../utils' 6 | 7 | const props = withDefaults(defineProps<{ 8 | author: Author | Author[] 9 | maxCount?: number 10 | isLive?: boolean 11 | }>(), { 12 | maxCount: 3, // 最多显示的头像数量 13 | }) 14 | 15 | // 限制显示的头像数量,最多显示 maxCount 个 16 | const displayedAvatars = computed(() => { 17 | if (Array.isArray(props.author)) 18 | return props.author?.slice(0, props.maxCount) || [] 19 | else 20 | return [props.author] 21 | }) 22 | </script> 23 | 24 | <template> 25 | <div 26 | :style="{ 27 | width: Array.isArray(author) && author.length > 1 ? `${28 + (displayedAvatars?.length) * 6}px` : '34px', 28 | height: Array.isArray(author) && author.length > 1 ? '28px' : '34px', 29 | }" 30 | mr-4 31 | pos="relative" 32 | shrink-0 33 | > 34 | <a 35 | v-for="(item, index) in displayedAvatars" 36 | :key="index" 37 | :href="getAuthorJumpUrl(item)" target="_blank" 38 | rounded="1/2" 39 | object="center cover" bg="$bew-skeleton" cursor="pointer" 40 | position-absolute top-0 inline-block 41 | :style="{ 42 | zIndex: displayedAvatars.length - index, 43 | left: `${index * 6}px`, 44 | width: displayedAvatars.length > 1 ? `28px` : '34px', 45 | height: displayedAvatars.length > 1 ? `28px` : '34px', 46 | }" 47 | :class="{ live: isLive }" 48 | @click.stop="" 49 | > 50 | <!-- Avatar --> 51 | <Picture 52 | :src="`${removeHttpFromUrl(item.authorFace)}@50w_50h_1c`" 53 | loading="lazy" 54 | w-inherit h-inherit 55 | rounded="1/2" 56 | /> 57 | 58 | <!-- Following Flag --> 59 | <div 60 | v-if="item.followed && !Array.isArray(author)" 61 | pos="absolute top-21px left-22px" 62 | w-14px h-14px 63 | bg="$bew-theme-color" 64 | border="2 outset solid white" 65 | rounded="1/2" 66 | grid place-items-center 67 | > 68 | <div color-white text-sm class="i-mingcute:check-fill w-8px h-8px" /> 69 | </div> 70 | <div 71 | v-else-if="isLive" 72 | pos="absolute top-18px left-22px" 73 | w-14px h-14px 74 | bg="$bew-theme-color" 75 | rounded="1/2" grid place-items-center 76 | > 77 | <div color-white text-sm class="i-svg-spinners:pulse-3 w-12px h-12px" /> 78 | </div> 79 | </a> 80 | 81 | <!-- More avatars not shown --> 82 | <span 83 | v-if="Array.isArray(author) && author.length > maxCount" 84 | pos="absolute right--4px" 85 | w="28px" h="28px" 86 | bg="$bew-skeleton" 87 | rounded="1/2" 88 | flex="~ items-center justify-end" 89 | > 90 | <span text="sm $bew-text-2" mr-1px>+</span> 91 | </span> 92 | </div> 93 | </template> 94 | 95 | <style scoped lang="scss"> 96 | .live { 97 | --uno: "p-2px box-border border-2 border-$bew-theme-color-60"; 98 | } 99 | </style> 100 | -------------------------------------------------------------------------------- /src/components/VideoCard/VideoCardAuthor/components/VideoCardAuthorName.vue: -------------------------------------------------------------------------------- 1 | <script lang="ts" setup> 2 | import { getAuthorJumpUrl } from '~/components/VideoCard/utils' 3 | 4 | import type { Author } from '../../types' 5 | 6 | defineProps<{ 7 | author?: Author | Author[] 8 | }>() 9 | </script> 10 | 11 | <template> 12 | <a 13 | class="channel-name" 14 | un-text="hover:$bew-text-1" 15 | cursor-pointer mr-4 16 | :href="getAuthorJumpUrl(Array.isArray(author) ? author[0] : author)" 17 | target="_blank" 18 | @click.stop="" 19 | > 20 | <span> 21 | <span v-if="Array.isArray(author) && author.length > 1"> 22 | {{ $t('video_card.group_contribution', { firstAuthor: author[0].name, num: author.length }) }} 23 | </span> 24 | <span v-else> 25 | {{ Array.isArray(author) ? author[0].name : author?.name }} 26 | </span> 27 | </span> 28 | </a> 29 | </template> 30 | -------------------------------------------------------------------------------- /src/components/VideoCard/VideoCardSkeleton.vue: -------------------------------------------------------------------------------- 1 | <script setup lang="ts"> 2 | defineProps<{ 3 | horizontal?: boolean 4 | hasTag?: boolean 5 | }>() 6 | </script> 7 | 8 | <template> 9 | <div 10 | v-if="!horizontal" 11 | mb-4 pointer-events-none select-none 12 | > 13 | <div aspect-video bg="$bew-skeleton" rounded="$bew-radius" /> 14 | <div flex mt-5> 15 | <div 16 | m="r-4" w="34px" h="34px" rounded="1/2" bg="$bew-skeleton" 17 | shrink-0 18 | /> 19 | <div w="[calc(100%-34px)]"> 20 | <div flex="~ col gap-2" mb-4 w-inherit> 21 | <div w-full h-5 bg="$bew-skeleton" rounded-4px /> 22 | <div w="3/4" h-5 bg="$bew-skeleton" rounded-4px /> 23 | </div> 24 | <div flex="~ col gap-2" mb-3> 25 | <div w="40%" h-3 bg="$bew-skeleton" rounded-4px /> 26 | <div w="60%" h-3 bg="$bew-skeleton" rounded-4px /> 27 | </div> 28 | <div 29 | text="transparent sm" inline-block p="x-2" lh-22px 30 | bg="$bew-skeleton" rounded-4 31 | > 32 | hello world 33 | </div> 34 | </div> 35 | </div> 36 | </div> 37 | 38 | <div 39 | v-else 40 | flex="~ gap-6" 41 | mb-4 pointer-events-none select-none 42 | > 43 | <!-- Cover --> 44 | <div 45 | :class="horizontal ? 'horizontal-card-cover' : 'vertical-card-cover'" 46 | shrink-0 aspect-video h-fit bg="$bew-skeleton" 47 | rounded="$bew-radius" 48 | /> 49 | <!-- Other Information --> 50 | <div 51 | w-full mt-0 52 | flex="~ gap-4" 53 | > 54 | <div w="[calc(100%-30px)]"> 55 | <div grid gap-2> 56 | <div w-full h-5 bg="$bew-skeleton" rounded-4px /> 57 | </div> 58 | 59 | <div mt-4 flex="~ col gap-2"> 60 | <div flex="~ items-center justify-start" w-inherit> 61 | <div 62 | m="r-2" w="30px" h="30px" rounded="1/2" bg="$bew-skeleton" 63 | shrink-0 64 | /> 65 | 66 | <div w="40%" h-5 bg="$bew-skeleton" rounded-4px /> 67 | </div> 68 | <div w="60%" h-4 bg="$bew-skeleton" rounded-4px /> 69 | <div 70 | text="transparent sm" inline-block w-fit 71 | lh-6 p="x-2" mt-1 72 | bg="$bew-skeleton" rounded-4 73 | > 74 | hello world 75 | </div> 76 | </div> 77 | </div> 78 | </div> 79 | </div> 80 | </template> 81 | 82 | <style lang="scss" scoped> 83 | .horizontal-card-cover { 84 | --uno: "xl:w-280px lg:w-250px md:w-200px w-200px"; 85 | } 86 | 87 | .vertical-card-cover { 88 | --uno: "w-full"; 89 | } 90 | </style> 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<HTMLVideoElement | null>) { 11 | if (videoElement.value) { 12 | return videoElement.value.currentTime 13 | } 14 | return null 15 | } 16 | 17 | export function getCurrentVideoUrl(video: Video, videoCurrentTime: Ref<number | null>) { 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<string, { default: Component }> = 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<AppPage> 7 | scrollbarRef: Ref<any> 8 | reachTop: Ref<boolean> 9 | mainAppRef: Ref<HTMLElement> 10 | handleReachBottom: Ref<(() => void) | undefined> 11 | handlePageRefresh: Ref<(() => void) | undefined> 12 | handleBackToTop: (targetScrollTop?: number) => void 13 | haveScrollbar: () => Promise<boolean> 14 | openIframeDrawer: (url: string) => void 15 | } 16 | 17 | export function useBewlyApp(): BewlyAppProvider { 18 | const provider = inject<BewlyAppProvider>('BEWLY_APP') 19 | 20 | if (import.meta.env.DEV && !provider) 21 | throw new Error('AppProvider is not injected') 22 | 23 | return provider! 24 | } 25 | -------------------------------------------------------------------------------- /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<HTMLElement>() 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/composables/useStorageLocal.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | MaybeRef, 3 | RemovableRef, 4 | StorageLikeAsync, 5 | UseStorageAsyncOptions, 6 | } from '@vueuse/core' 7 | import { 8 | useStorageAsync, 9 | } from '@vueuse/core' 10 | import { storage } from 'webextension-polyfill' 11 | 12 | const storageLocal: StorageLikeAsync = { 13 | removeItem(key: string) { 14 | return storage.local.remove(key) 15 | }, 16 | 17 | setItem(key: string, value: string) { 18 | return storage.local.set({ [key]: value }) 19 | }, 20 | 21 | async getItem(key: string) { 22 | return (await storage.local.get(key))[key] 23 | }, 24 | } 25 | 26 | export function useStorageLocal<T>(key: string, initialValue: MaybeRef<T>, options?: UseStorageAsyncOptions<T>): RemovableRef<T> { 27 | return useStorageAsync(key, initialValue, storageLocal, options) 28 | } 29 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 { name: string, url: string, thumbnail?: string } 9 | 10 | export const WALLPAPERS: wallpaperItem[] = [ 11 | // { 12 | // name: 'Unsplash Random Nature Image', 13 | // url: 'https://source.unsplash.com/1920x1080/?nature', 14 | // thumbnail: 'https://source.unsplash.com/1920x1080/?nature', 15 | // }, 16 | // { 17 | // name: 'Unsplash Random Building Image', 18 | // url: 'https://source.unsplash.com/1920x1080/?building', 19 | // thumbnail: 'https://source.unsplash.com/1920x1080/?building', 20 | // }, 21 | // { 22 | // name: 'Unsplash Random Night Scene Image', 23 | // url: 'https://source.unsplash.com/1920x1080/?night-scene', 24 | // thumbnail: 'https://source.unsplash.com/1920x1080/?night-scene', 25 | // }, 26 | { 27 | name: 'LoremPicsum Random Image', 28 | url: 'https://picsum.photos/2560/1440/?nature', 29 | thumbnail: 'https://picsum.photos/2560/1440/?nature', 30 | }, 31 | { 32 | name: 'Nicolas Lafargue - Rocky Mountain Cloudscape', 33 | url: 'https://cdn.jsdelivr.net/gh/BewlyBewly/Imgs/wallpapers/rocky-mountain-cloudscape.jpg', 34 | thumbnail: 'https://cdn.jsdelivr.net/gh/BewlyBewly/Imgs/wallpapers/rocky-mountain-cloudscape-thumbnail.jpg', 35 | }, 36 | { 37 | name: 'Zongnan Bao- Green white mountains', 38 | url: 'https://cdn.jsdelivr.net/gh/BewlyBewly/Imgs/wallpapers/green-white-mountains.jpg', 39 | thumbnail: 'https://cdn.jsdelivr.net/gh/BewlyBewly/Imgs/wallpapers/green-white-mountains-thumbnail.jpg', 40 | }, 41 | { 42 | name: 'Colin Watts - Night Sky Stars', 43 | url: 'https://cdn.jsdelivr.net/gh/BewlyBewly/Imgs/wallpapers/night-sky-stars.jpg', 44 | thumbnail: 'https://cdn.jsdelivr.net/gh/BewlyBewly/Imgs/wallpapers/night-sky-stars-thumbnail.jpg', 45 | }, 46 | { 47 | name: 'Ryan Geller - Sailboats moored at Land and Sea Park in The Exumas', 48 | url: 'https://cdn.jsdelivr.net/gh/BewlyBewly/Imgs/wallpapers/sailboats-moored-at-the-exumas.jpg', 49 | thumbnail: 'https://cdn.jsdelivr.net/gh/BewlyBewly/Imgs/wallpapers/sailboats-moored-at-the-exumas-thumbnail.jpg', 50 | }, 51 | { 52 | name: 'NASA - Outer Space Photo', 53 | url: 'https://cdn.jsdelivr.net/gh/BewlyBewly/Imgs/wallpapers/outer-space-photo.jpg', 54 | thumbnail: 'https://cdn.jsdelivr.net/gh/BewlyBewly/Imgs/wallpapers/outer-space-photo-thumbnail.jpg', 55 | }, 56 | { 57 | name: 'BML2019 VR (pid: 74271400)', 58 | url: 'https://cdn.jsdelivr.net/gh/BewlyBewly/Imgs/wallpapers/bml2019-vr.jpg', 59 | thumbnail: 'https://cdn.jsdelivr.net/gh/BewlyBewly/Imgs/wallpapers/bml2019-vr-thumbnail.jpg', 60 | }, 61 | { 62 | name: '2020 拜年祭活动', 63 | url: 'https://cdn.jsdelivr.net/gh/BewlyBewly/Imgs/wallpapers/2020-拜年祭活动.jpg', 64 | thumbnail: 'https://cdn.jsdelivr.net/gh/BewlyBewly/Imgs/wallpapers/2020-拜年祭活动-thumbnail.jpg', 65 | }, 66 | { 67 | name: '2020 BDF', 68 | url: 'https://cdn.jsdelivr.net/gh/BewlyBewly/Imgs/wallpapers/2020-bdf.jpg', 69 | thumbnail: 'https://cdn.jsdelivr.net/gh/BewlyBewly/Imgs/wallpapers/2020-bdf-thumbnail.jpg', 70 | }, 71 | ] 72 | -------------------------------------------------------------------------------- /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 | <template> 2 | <div> 3 | Moments (WIP) 4 | </div> 5 | </template> 6 | -------------------------------------------------------------------------------- /src/contentScripts/views/Search/Search.vue: -------------------------------------------------------------------------------- 1 | <script lang="ts" setup> 2 | import { settings } from '~/logic' 3 | </script> 4 | 5 | <template> 6 | <div 7 | flex="~ col" 8 | justify-center 9 | items-center 10 | w-full z-10 11 | m="t-20vh" 12 | > 13 | <Logo 14 | v-if="settings.searchPageShowLogo" :size="180" :color="settings.searchPageLogoColor === 'white' ? 'white' : 'var(--bew-theme-color)'" 15 | :glow="settings.searchPageLogoGlow" 16 | mb-12 z-1 17 | /> 18 | <SearchBar 19 | :darken-on-focus="settings.searchPageDarkenOnSearchFocus" 20 | :blurred-on-focus="settings.searchPageBlurredOnSearchFocus" 21 | :focused-character="settings.searchPageSearchBarFocusCharacter" 22 | /> 23 | </div> 24 | </template> 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', 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 | -------------------------------------------------------------------------------- /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/inject/index.js: -------------------------------------------------------------------------------- 1 | const isArray = val => Array.isArray(val) 2 | function injectFunction( 3 | origin, 4 | keys, 5 | cb, 6 | ) { 7 | if (!isArray(keys)) 8 | keys = [keys] 9 | 10 | const originKeysValue = keys.reduce((obj, key) => { 11 | obj[key] = origin[key] 12 | return obj 13 | }, {}) 14 | 15 | keys.map(k => origin[k]) 16 | 17 | keys.forEach((key) => { 18 | const fn = (...args) => { 19 | cb(...args) 20 | return (originKeysValue[key]).apply(origin, args) 21 | } 22 | fn.toString = (origin)[key].toString 23 | ;(origin)[key] = fn 24 | }) 25 | 26 | return { 27 | originKeysValue, 28 | restore: () => { 29 | for (const key in originKeysValue) { 30 | origin[key] = (originKeysValue[key]).bind(origin) 31 | } 32 | }, 33 | } 34 | } 35 | 36 | injectFunction( 37 | window.history, 38 | ['pushState', 'forward', 'replaceState'], 39 | (...args) => { 40 | window.dispatchEvent(new CustomEvent('historyChange', { detail: args })) 41 | }, 42 | ) 43 | 44 | window.___inject = true 45 | 46 | // History.prototype.pushState = history.pushState 47 | // History.prototype.replaceState = history.replaceState 48 | // History.prototype.forward = history.forward 49 | -------------------------------------------------------------------------------- /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/moment/topBarLiveMoment.ts: -------------------------------------------------------------------------------- 1 | // https://app.quicktype.io/?l=ts 2 | 3 | export interface TopBarLiveMomentResult { 4 | code: number 5 | message: string 6 | ttl: number 7 | data: Data 8 | } 9 | 10 | export interface Data { 11 | results: number 12 | page: string 13 | pagesize: string 14 | list: List[] 15 | } 16 | 17 | export interface List { 18 | cover: string 19 | face: string 20 | uname: string 21 | title: string 22 | roomid: number 23 | pic: string 24 | online: number 25 | link: string 26 | uid: number 27 | parent_area_id: number 28 | area_id: number 29 | } 30 | -------------------------------------------------------------------------------- /src/models/moment/topBarMoment.ts: -------------------------------------------------------------------------------- 1 | // https://app.quicktype.io/?l=ts 2 | 3 | export interface TopBarMomentResult { 4 | code: number 5 | message: string 6 | ttl: number 7 | data: Data 8 | } 9 | 10 | export interface Data { 11 | has_more: boolean 12 | items: Item[] 13 | offset: string 14 | update_baseline: string 15 | update_num: number 16 | } 17 | 18 | export interface Item { 19 | author: Author 20 | cover: string 21 | id_str: string 22 | jump_url: string 23 | pub_time: string 24 | rid: number 25 | title: string 26 | type: number 27 | visible: boolean 28 | } 29 | 30 | export interface Author { 31 | face: string 32 | jump_url: string 33 | mid: number 34 | name: string 35 | official: Official 36 | vip: Vip 37 | } 38 | 39 | export interface Official { 40 | desc: string 41 | role: number 42 | title: string 43 | type: number 44 | } 45 | 46 | export interface Vip { 47 | avatar_icon: AvatarIcon 48 | avatar_subscript: number 49 | avatar_subscript_url: string 50 | due_date: number 51 | label: Label 52 | nickname_color: Color 53 | role: number 54 | status: number 55 | theme_type: number 56 | tv_due_date: number 57 | tv_vip_pay_type: number 58 | tv_vip_status: number 59 | type: number 60 | vip_pay_type: number 61 | } 62 | 63 | export interface AvatarIcon { 64 | icon_resource: IconResource 65 | icon_type?: number 66 | } 67 | 68 | export interface IconResource { 69 | type?: number 70 | url?: string 71 | } 72 | 73 | export interface Label { 74 | bg_color: Color 75 | bg_style: number 76 | border_color: string 77 | img_label_uri_hans: string 78 | img_label_uri_hans_static: string 79 | img_label_uri_hant: string 80 | img_label_uri_hant_static: string 81 | label_theme: LabelTheme 82 | path: string 83 | text: Text 84 | text_color: TextColor 85 | use_img_label: boolean 86 | } 87 | 88 | export enum Color { 89 | Empty = '', 90 | Fb7299 = '#FB7299', 91 | } 92 | 93 | export enum LabelTheme { 94 | AnnualVip = 'annual_vip', 95 | Empty = '', 96 | Vip = 'vip', 97 | } 98 | 99 | export enum Text { 100 | Empty = '', 101 | 大会员 = '大会员', 102 | 年度大会员 = '年度大会员', 103 | } 104 | 105 | export enum TextColor { 106 | Empty = '', 107 | Ffffff = '#FFFFFF', 108 | } 109 | -------------------------------------------------------------------------------- /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 | <script setup lang="ts"> 2 | import { storageDemo } from '~/logic/storage' 3 | </script> 4 | 5 | <template> 6 | <main class="px-4 py-10 text-center text-gray-700 dark:text-gray-200"> 7 | <!-- <img src="/assets/icon.svg" class="icon-btn mx-2 text-2xl" alt="extension icon"> --> 8 | <div>Options</div> 9 | <p class="mt-2 opacity-50"> 10 | This is the options page 11 | </p> 12 | 13 | <input v-model="storageDemo" class="border border-gray-400 rounded px-2 py-1 mt-2"> 14 | 15 | <div class="mt-4"> 16 | Powered by Vite <pixelarticons-zap class="align-middle inline-block" /> 17 | </div> 18 | </main> 19 | </template> 20 | -------------------------------------------------------------------------------- /src/options/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="UTF-8"> 5 | <base target="_blank"> 6 | <title>Options</title> 7 | </head> 8 | <body> 9 | <div id="app"></div> 10 | <script type="module" src="./main.ts"></script> 11 | </body> 12 | </html> 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 | <script setup lang="ts"> 2 | import Logo from '~/components/Logo.vue' 3 | import { storageDemo } from '~/logic/storage' 4 | 5 | function openOptionsPage() { 6 | browser.runtime.openOptionsPage() 7 | } 8 | </script> 9 | 10 | <template> 11 | <main class="w-[300px] px-4 py-5 text-center text-gray-700"> 12 | <Logo /> 13 | <div>Popup</div> 14 | <p class="mt-2 opacity-50"> 15 | This is the popup page 16 | </p> 17 | <button class="btn mt-2" @click="openOptionsPage"> 18 | Open Options 19 | </button> 20 | <div class="mt-2"> 21 | <span class="opacity-50">Storage:+</span> {{ storageDemo }} 22 | </div> 23 | </main> 24 | </template> 25 | -------------------------------------------------------------------------------- /src/popup/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="UTF-8"> 5 | <base target="_blank"> 6 | <title>Popup</title> 7 | </head> 8 | <body style="min-width: 100px"> 9 | <div id="app"></div> 10 | <script type="module" src="./main.ts"></script> 11 | </body> 12 | </html> 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/stores/topBarStore.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | import type { AppPage } from '~/enums/appEnums' 4 | import { TopBarPopup } from '~/enums/appEnums' 5 | import { getUserID } from '~/utils/main' 6 | 7 | export interface TopBarItem { 8 | i18nKey: string 9 | icon: string 10 | url: string 11 | popup?: TopBarPopup 12 | showPopup?: boolean 13 | page?: AppPage 14 | } 15 | 16 | export interface PopupVisible { 17 | notifications: boolean 18 | moments: boolean 19 | favorites: boolean 20 | history: boolean 21 | } 22 | 23 | export const useTopBarStore = defineStore('topBar', () => { 24 | const popupVisible = reactive({ 25 | notifications: false, 26 | moments: false, 27 | favorites: false, 28 | history: false, 29 | }) 30 | 31 | const topBarItems = computed((): TopBarItem[] => { 32 | return [ 33 | { 34 | i18nKey: 'topbar.notifications', 35 | icon: 'tabler:bell', 36 | url: 'https://message.bilibili.com', 37 | popup: TopBarPopup.NotificationsPop, 38 | showPopup: popupVisible.notifications, 39 | }, 40 | { 41 | i18nKey: 'topbar.moments', 42 | icon: 'tabler:windmill', 43 | url: 'https://t.bilibili.com', 44 | popup: TopBarPopup.MomentsPop, 45 | showPopup: popupVisible.moments, 46 | }, 47 | { 48 | i18nKey: 'topbar.favorites', 49 | icon: 'mingcute:star-line', 50 | url: `https://space.bilibili.com/${getUserID()}/favlist`, 51 | popup: TopBarPopup.FavoritesPop, 52 | showPopup: popupVisible.favorites, 53 | }, 54 | { 55 | i18nKey: 'topbar.history', 56 | icon: 'mingcute:time-line', 57 | url: 'https://www.bilibili.com/account/history', 58 | popup: TopBarPopup.HistoryPop, 59 | showPopup: popupVisible.history, 60 | }, 61 | { 62 | i18nKey: 'topbar.creative_center', 63 | icon: 'mingcute:bulb-line', 64 | url: 'https://member.bilibili.com/platform/home', 65 | }, 66 | ] 67 | }) 68 | 69 | function setPopupVisible(popup: TopBarPopup) { 70 | switch (popup) { 71 | case TopBarPopup.NotificationsPop: 72 | popupVisible.notifications = !popupVisible.notifications 73 | break 74 | case TopBarPopup.MomentsPop: 75 | popupVisible.moments = !popupVisible.moments 76 | break 77 | case TopBarPopup.FavoritesPop: 78 | popupVisible.favorites = !popupVisible.favorites 79 | break 80 | case TopBarPopup.HistoryPop: 81 | popupVisible.history = !popupVisible.history 82 | break 83 | } 84 | } 85 | 86 | return { topBarItems, setPopupVisible } 87 | }) 88 | -------------------------------------------------------------------------------- /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 | import './injectBuildInFonts.ts' 9 | -------------------------------------------------------------------------------- /src/styles/injectBuildInFonts.ts: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill' 2 | 3 | import { injectCSS } from '~/utils/main' 4 | 5 | injectCSS(` 6 | @font-face { 7 | font-family: "Numbers"; 8 | unicode-range: U+0030-0039; 9 | src: url(${browser.runtime.getURL('/assets/fonts/Geist[wght].woff2')}) format("woff2-variations"); 10 | } 11 | 12 | @font-face { 13 | font-family: "Onest"; 14 | src: url(${browser.runtime.getURL('/assets/fonts/Onest[wght].woff2')}) format("woff2-variations"); 15 | } 16 | 17 | @font-face { 18 | font-family: "ShangguSansSCVF"; 19 | src: url(${browser.runtime.getURL('/assets/fonts/ShangguSansSC-VF.ttf')}) format("truetype-variations"); 20 | } 21 | 22 | @font-face { 23 | font-family: "CJKEmDash"; 24 | unicode-range: U+2014, U+2E3A-2E3B; 25 | src: url(${browser.runtime.getURL('/assets/fonts/ZhudouSansVF-subset.woff2')}) format("woff2-variations"); 26 | } 27 | `) 28 | -------------------------------------------------------------------------------- /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 string> = S extends `${infer P1}_${infer P2}${infer P3}` 4 | ? `${Lowercase<P1>}${Uppercase<P2>}${CamelCase<P3>}` 5 | : Lowercase<S> 6 | 7 | type APIFunction<T = typeof API_COLLECTION> = { 8 | [K in keyof T as CamelCase<string & K>]: { 9 | // @ts-expect-error allow params 10 | [P in keyof T[K]]: T[K][P] extends Function ? T[K][P] : Lowercase<T[K][P]['_fetch']['method']> extends 'get' ? (options?: Partial<T[K][P]['params']>) => Promise<any> : (options?: Partial<T[K][P]['params'] & T[K][P]['_fetch']['body']>) => Promise<any> 11 | } 12 | } 13 | 14 | // eslint-disable-next-line ts/no-unsafe-declaration-merging 15 | export interface APIClient extends APIFunction<typeof API_COLLECTION> { 16 | 17 | } 18 | 19 | // eslint-disable-next-line ts/no-unsafe-declaration-merging 20 | export class APIClient { 21 | private readonly cache = new Map<string | symbol, any>() 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<string, any> 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<string, any>) { 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<string, any>) { 26 | return appSign(params, TVAppKey.appkey, TVAppKey.appsec) 27 | } 28 | 29 | export function pollTVLoginQRCode(authCode: string): Promise<any> { 30 | const url = 'https://passport.bilibili.com/x/passport-tv-login/qrcode/poll' 31 | 32 | return new Promise<void>((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<any> { 52 | const url = 'https://passport.bilibili.com/x/passport-tv-login/qrcode/auth_code' 53 | 54 | return new Promise<void>((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<T> { 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<any> = 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 function openLinkInBackground(url: string) { 6 | return browser.runtime.sendMessage({ 7 | contentScriptQuery: TABS_MESSAGE.OPEN_LINK_IN_BACKGROUND, 8 | url, 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/timer.ts: -------------------------------------------------------------------------------- 1 | export function executeTimes(fn: () => void | Promise<void>, 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 | -------------------------------------------------------------------------------- /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 | "noUnusedLocals": true, 19 | "noEmit": true, 20 | "esModuleInterop": true, 21 | "forceConsistentCasingInFileNames": true, 22 | "isolatedModules": true, 23 | "skipLibCheck": true 24 | }, 25 | "exclude": ["dist", "node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /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.ts: -------------------------------------------------------------------------------- 1 | /// <reference types="vitest" /> 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 | }, 22 | }, 23 | plugins: [ 24 | Vue(), 25 | 26 | AutoImport({ 27 | imports: [ 28 | 'vue', 29 | { 30 | 'webextension-polyfill': [ 31 | ['*', 'browser'], 32 | ], 33 | }, 34 | ], 35 | }), 36 | 37 | // https://github.com/intlify/bundle-tools/tree/main/packages/unplugin-vue-i18n 38 | VueI18nPlugin({ 39 | runtimeOnly: true, 40 | compositionOnly: true, 41 | strictMessage: false, 42 | include: [r('./src/_locales/**')], 43 | }), 44 | 45 | // https://github.com/unocss/unocss 46 | UnoCSS(), 47 | 48 | replace({ 49 | '__DEV__': JSON.stringify(isDev), 50 | 'process.env.NODE_ENV': JSON.stringify(isDev ? 'development' : 'production'), 51 | '__VUE_OPTIONS_API__': JSON.stringify(true), 52 | '__VUE_PROD_DEVTOOLS__': JSON.stringify(false), 53 | 'preventAssignment': true, 54 | }), 55 | 56 | // rewrite assets to use relative path 57 | { 58 | name: 'assets-rewrite', 59 | enforce: 'post', 60 | apply: 'build', 61 | transformIndexHtml(html, { path }) { 62 | return html.replace(/"\/assets\//g, `"${relative(dirname(path), '/assets')}/`) 63 | }, 64 | }, 65 | ], 66 | optimizeDeps: { 67 | include: [ 68 | 'vue', 69 | '@vueuse/core', 70 | 'webextension-polyfill', 71 | ], 72 | exclude: [ 73 | 'vue-demi', 74 | ], 75 | }, 76 | } 77 | 78 | export default defineConfig(({ command }) => ({ 79 | ...sharedConfig, 80 | base: command === 'serve' ? `http://localhost:${port}/` : '/dist/', 81 | server: { 82 | port, 83 | hmr: { 84 | host: 'localhost', 85 | }, 86 | }, 87 | build: { 88 | outDir: r(isFirefox ? 'extension-firefox/dist' : isSafari ? 'extension-safari/dist' : 'extension/dist'), 89 | emptyOutDir: false, 90 | sourcemap: false, // https://github.com/vitejs/vite-plugin-vue/issues/35 91 | // https://developer.chrome.com/docs/webstore/program_policies/#:~:text=Code%20Readability%20Requirements 92 | terserOptions: { 93 | mangle: false, 94 | }, 95 | rollupOptions: { 96 | input: { 97 | options: r('src/options/index.html'), 98 | popup: r('src/popup/index.html'), 99 | }, 100 | }, 101 | minify: 'terser', 102 | }, 103 | plugins: [ 104 | ...sharedConfig.plugins!, 105 | 106 | MV3Hmr(), 107 | ], 108 | test: { 109 | globals: true, 110 | environment: 'jsdom', 111 | }, 112 | })) 113 | --------------------------------------------------------------------------------