The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .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 | 


--------------------------------------------------------------------------------