mirror of
https://github.com/BewlyBewly/BewlyBewly.git
synced 2025-04-14 13:15:29 +00:00
first commit
This commit is contained in:
133
README.md
Normal file
133
README.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# WebExtension Vite Starter
|
||||
|
||||
A [Vite](https://vitejs.dev/) powered WebExtension ([Chrome](https://developer.chrome.com/docs/extensions/reference/), [FireFox](https://addons.mozilla.org/en-US/developers/), etc.) starter template.
|
||||
|
||||
<p align="center">
|
||||
<sub>Popup</sub><br/>
|
||||
<img width="655" src="https://user-images.githubusercontent.com/11247099/126741643-813b3773-17ff-4281-9737-f319e00feddc.png"><br/>
|
||||
<sub>Options Page</sub><br/>
|
||||
<img width="655" src="https://user-images.githubusercontent.com/11247099/126741653-43125b62-6578-4452-83a7-bee19be2eaa2.png"><br/>
|
||||
<sub>Inject Vue App into the Content Script</sub><br/>
|
||||
<img src="https://user-images.githubusercontent.com/11247099/130695439-52418cf0-e186-4085-8e19-23fe808a274e.png">
|
||||
</p>
|
||||
|
||||
## Features
|
||||
|
||||
- ⚡️ **Instant HMR** - use **Vite** on dev (no more refresh!)
|
||||
- 🥝 Vue 3 - Composition API, [`<script setup>` syntax](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0040-script-setup.md) and more!
|
||||
- 💬 Effortless communications - powered by [`webext-bridge`](https://github.com/antfu/webext-bridge) and [VueUse](https://github.com/antfu/vueuse) storage
|
||||
- 🍃 [Windi CSS](https://windicss.org/) - on-demand CSS utilities
|
||||
- 🦾 [TypeScript](https://www.typescriptlang.org/) - type safe
|
||||
- 📦 [Components auto importing](./src/components)
|
||||
- 🌟 [Icons](./src/components) - Access to icons from any iconset directly
|
||||
- 🖥 Content Script - Use Vue even in content script
|
||||
- 🌍 WebExtension - isomorphic extension for Chrome, Firefox, and others
|
||||
- 📃 Dynamic `manifest.json` with full type support
|
||||
|
||||
## Pre-packed
|
||||
|
||||
### WebExtension Libraries
|
||||
|
||||
- [`webextension-polyfill`](https://github.com/mozilla/webextension-polyfill) - WebExtension browser API Polyfill with types
|
||||
- [`webext-bridge`](https://github.com/antfu/webext-bridge) - effortlessly communication between contexts
|
||||
|
||||
### Vite Plugins
|
||||
|
||||
- [`unplugin-auto-import`](https://github.com/antfu/unplugin-auto-import) - Directly use `browser` and Vue Composition API without importing
|
||||
- [`unplugin-vue-components`](https://github.com/antfu/vite-plugin-components) - components auto import
|
||||
- [`unplugin-icons`](https://github.com/antfu/unplugin-icons) - icons as components
|
||||
- [Iconify](https://iconify.design) - use icons from any icon sets [🔍Icônes](https://icones.netlify.app/)
|
||||
- [`vite-plugin-windicss`](https://github.com/antfu/vite-plugin-windicss) - WindiCSS support
|
||||
|
||||
### Vue Plugins
|
||||
|
||||
- [VueUse](https://github.com/antfu/vueuse) - collection of useful composition APIs
|
||||
|
||||
### UI Frameworks
|
||||
|
||||
- [Windi CSS](https://github.com/windicss/windicss) - next generation utility-first CSS framework
|
||||
|
||||
### Coding Style
|
||||
|
||||
- Use Composition API with [`<script setup>` SFC syntax](https://github.com/vuejs/rfcs/pull/227)
|
||||
- [ESLint](https://eslint.org/) with [@antfu/eslint-config](https://github.com/antfu/eslint-config), single quotes, no semi
|
||||
|
||||
### Dev tools
|
||||
|
||||
- [TypeScript](https://www.typescriptlang.org/)
|
||||
- [pnpm](https://pnpm.js.org/) - fast, disk space efficient package manager
|
||||
- [esno](https://github.com/antfu/esno) - TypeScript / ESNext node runtime powered by esbuild
|
||||
- [npm-run-all](https://github.com/mysticatea/npm-run-all) - Run multiple npm-scripts in parallel or sequential
|
||||
- [web-ext](https://github.com/mozilla/web-ext) - Streamlined experience for developing web extensions
|
||||
|
||||
## Use the Template
|
||||
|
||||
### GitHub Template
|
||||
|
||||
[Create a repo from this template on GitHub](https://github.com/antfu/vitesse-webext/generate).
|
||||
|
||||
### Clone to local
|
||||
|
||||
If you prefer to do it manually with the cleaner git history
|
||||
|
||||
> If you don't have pnpm installed, run: npm install -g pnpm
|
||||
|
||||
```bash
|
||||
npx degit antfu/vitesse-webext my-webext
|
||||
cd my-webext
|
||||
pnpm i
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Folders
|
||||
|
||||
- `src` - main source.
|
||||
- `contentScript` - scripts and components to be injected as `content_script`
|
||||
- `background` - scripts for background.
|
||||
- `components` - auto-imported Vue components that shared in popup and options page.
|
||||
- `styles` - styles shared in popup and options page
|
||||
- `manifest.ts` - manifest for the extension.
|
||||
- `extension` - extension package root.
|
||||
- `assets` - static assets.
|
||||
- `dist` - built files, also serve stub entry for Vite on development.
|
||||
- `scripts` - development and bundling helper scripts.
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Then **load extension in browser with the `extension/` folder**.
|
||||
|
||||
For Firefox developers, you can run the following command instead:
|
||||
|
||||
```bash
|
||||
pnpm start:firefox
|
||||
```
|
||||
|
||||
`web-ext` auto reload the extension when `extension/` files changed.
|
||||
|
||||
> While Vite handles HMR automatically in the most of the case, [Extensions Reloader](https://chrome.google.com/webstore/detail/fimgfedafeadlieiabdeeaodndnlbhid) is still recommanded for cleaner hard reloading.
|
||||
|
||||
### Build
|
||||
|
||||
To build the extension, run
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
And then pack files under `extension`, you can upload `extension.crx` or `extension.xpi` to appropriate extension store.
|
||||
|
||||
## Credits
|
||||
|
||||

|
||||
|
||||
This template is originally made for the [volta.net](https://volta.net) browser extension.
|
||||
|
||||
## Variations
|
||||
|
||||
This is a variant of [Vitesse](https://github.com/antfu/vitesse), check out the [full variations list](https://github.com/antfu/vitesse#variations).
|
||||
# BewlyBewly
|
||||
BIN
extension/assets/fonts/Bilibili_Icon.ttf
Normal file
BIN
extension/assets/fonts/Bilibili_Icon.ttf
Normal file
Binary file not shown.
BIN
extension/assets/icon-512.png
Normal file
BIN
extension/assets/icon-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
3
extension/assets/icon.svg
Normal file
3
extension/assets/icon.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M26.6667 1.66667H24V7H8V9.66667H5.33333V20.3333H8V23H10.6667V28.3333H21.3333V25.6667H26.6667V23H21.3333V20.3333H26.6667V17.6667H21.3333V15H10.6667V20.3333H8V9.66667H24V7H26.6667V1.66667ZM18.6667 25.6667H13.3333V17.6667H18.6667V25.6667Z" fill="#888888"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 366 B |
62
package.json
Normal file
62
package.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"name": "bewly-bewly",
|
||||
"displayName": "BewlyBewly",
|
||||
"version": "0.0.1",
|
||||
"description": "[description]",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "npm run clear && cross-env NODE_ENV=development run-p dev:*",
|
||||
"dev:prepare": "esno scripts/prepare.ts",
|
||||
"dev:web": "vite",
|
||||
"dev:js": "npm run build:js -- --mode development",
|
||||
"build": "cross-env NODE_ENV=production run-s clear build:web build:prepare build:js",
|
||||
"build:prepare": "esno scripts/prepare.ts",
|
||||
"build:web": "vite build",
|
||||
"build:js": "vite build --config vite.config.content.ts",
|
||||
"pack": "cross-env NODE_ENV=production run-p pack:*",
|
||||
"pack:zip": "rimraf extension.zip && jszip-cli add extension -o ./extension.zip",
|
||||
"pack:crx": "crx pack extension -o ./extension.crx",
|
||||
"pack:xpi": "cross-env WEB_EXT_ARTIFACTS_DIR=./ web-ext build --source-dir ./extension --filename extension.xpi --overwrite-dest",
|
||||
"start:chromium": "web-ext run --source-dir ./extension --target=chromium",
|
||||
"start:firefox": "web-ext run --source-dir ./extension --target=firefox-desktop",
|
||||
"clear": "rimraf extension/dist extension/manifest.json extension.*",
|
||||
"lint": "eslint 'src/**/*.{json,ts,js,vue}'"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^0.9.0",
|
||||
"@ffflorian/jszip-cli": "^3.1.5",
|
||||
"@iconify/json": "^1.1.408",
|
||||
"@types/chrome": "^0.0.179",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"@types/node": "^16.10.2",
|
||||
"@types/webextension-polyfill": "^0.8.2",
|
||||
"@typescript-eslint/eslint-plugin": "^4.32.0",
|
||||
"@vitejs/plugin-vue": "^1.9.2",
|
||||
"@vue/compiler-sfc": "^3.2.19",
|
||||
"@vueuse/core": "^7.5.1",
|
||||
"chokidar": "^3.5.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"crx": "^5.0.1",
|
||||
"eslint": "^7.32.0",
|
||||
"esno": "^0.10.0",
|
||||
"fs-extra": "^10.0.0",
|
||||
"kolorist": "^1.5.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"rimraf": "^3.0.2",
|
||||
"typescript": "^4.4.3",
|
||||
"unplugin-auto-import": "^0.5.11",
|
||||
"unplugin-icons": "^0.11.4",
|
||||
"unplugin-vue-components": "^0.15.4",
|
||||
"vite": "^2.6.2",
|
||||
"vite-plugin-windicss": "^1.4.8",
|
||||
"vue": "^3.2.19",
|
||||
"vue-demi": "^0.11.4",
|
||||
"web-ext": "^6.4.0",
|
||||
"webext-bridge": "^5.0.0",
|
||||
"webextension-polyfill": "^0.8.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"esbuild-darwin-64": "0.13.3",
|
||||
"vue-router": "4"
|
||||
}
|
||||
}
|
||||
5861
pnpm-lock.yaml
generated
Normal file
5861
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
10
scripts/manifest.ts
Normal file
10
scripts/manifest.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import fs from 'fs-extra'
|
||||
import { getManifest } from '../src/manifest'
|
||||
import { r, log } from './utils'
|
||||
|
||||
export async function writeManifest() {
|
||||
await fs.writeJSON(r('extension/manifest.json'), await getManifest(), { spaces: 2 })
|
||||
log('PRE', 'write manifest.json')
|
||||
}
|
||||
|
||||
writeManifest()
|
||||
44
scripts/prepare.ts
Normal file
44
scripts/prepare.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
// generate stub index.html files for dev entry
|
||||
import { execSync } from 'child_process'
|
||||
import fs from 'fs-extra'
|
||||
import chokidar from 'chokidar'
|
||||
import { r, port, isDev, log } from './utils'
|
||||
|
||||
/**
|
||||
* Stub index.html to use Vite in development
|
||||
*/
|
||||
async function stubIndexHtml() {
|
||||
const views = [
|
||||
'options',
|
||||
'popup',
|
||||
'background',
|
||||
]
|
||||
|
||||
for (const view of views) {
|
||||
await fs.ensureDir(r(`extension/dist/${view}`))
|
||||
let data = await fs.readFile(r(`src/${view}/index.html`), 'utf-8')
|
||||
data = data
|
||||
.replace('"./main.ts"', `"http://localhost:${port}/${view}/main.ts"`)
|
||||
.replace('<div id="app"></div>', '<div id="app">Vite server did not start</div>')
|
||||
await fs.writeFile(r(`extension/dist/${view}/index.html`), data, 'utf-8')
|
||||
log('PRE', `stub ${view}`)
|
||||
}
|
||||
}
|
||||
|
||||
function writeManifest() {
|
||||
execSync('npx esno ./scripts/manifest.ts', { stdio: 'inherit' })
|
||||
}
|
||||
|
||||
writeManifest()
|
||||
|
||||
if (isDev) {
|
||||
stubIndexHtml()
|
||||
chokidar.watch(r('src/**/*.html'))
|
||||
.on('change', () => {
|
||||
stubIndexHtml()
|
||||
})
|
||||
chokidar.watch([r('src/manifest.ts'), r('package.json')])
|
||||
.on('change', () => {
|
||||
writeManifest()
|
||||
})
|
||||
}
|
||||
11
scripts/utils.ts
Normal file
11
scripts/utils.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { resolve } from 'path'
|
||||
import { bgCyan, black } from 'kolorist'
|
||||
|
||||
export const port = parseInt(process.env.PORT || '') || 3303
|
||||
export const r = (...args: string[]) => resolve(__dirname, '..', ...args)
|
||||
export const isDev = process.env.NODE_ENV !== 'production'
|
||||
|
||||
export function log(name: string, message: string) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(black(bgCyan(` ${name} `)), message)
|
||||
}
|
||||
10
shim.d.ts
vendored
Normal file
10
shim.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ProtocolWithReturn } from 'webext-bridge'
|
||||
|
||||
declare module 'webext-bridge' {
|
||||
export interface ProtocolMap {
|
||||
// define message protocol types
|
||||
// see https://github.com/antfu/webext-bridge#type-safe-protocols
|
||||
'tab-prev': { title: string | undefined }
|
||||
'get-current-tab': ProtocolWithReturn<{ tabId: number }, { title?: string }>
|
||||
}
|
||||
}
|
||||
18
src/background/contentScriptHMR.ts
Normal file
18
src/background/contentScriptHMR.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { isFirefox, isForbiddenUrl } from '~/env'
|
||||
|
||||
// Firefox fetch files from cache instead of reloading changes from disk,
|
||||
// hmr will not work as Chromium based browser
|
||||
browser.webNavigation.onCommitted.addListener(({ tabId, frameId, url }) => {
|
||||
// Filter out non main window events.
|
||||
if (frameId !== 0)
|
||||
return
|
||||
|
||||
if (isForbiddenUrl(url))
|
||||
return
|
||||
|
||||
// inject the latest scripts
|
||||
browser.tabs.executeScript(tabId, {
|
||||
file: `${isFirefox ? '' : '.'}/dist/contentScripts/index.global.js`,
|
||||
runAt: 'document_end',
|
||||
}).catch(error => console.error(error))
|
||||
})
|
||||
10
src/background/index.html
Normal file
10
src/background/index.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Background</title>
|
||||
</head>
|
||||
<body>
|
||||
<script type="module" src="./main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
172
src/background/main.ts
Normal file
172
src/background/main.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import { createSharedComposable } from '@vueuse/core'
|
||||
import { sendMessage, onMessage } from 'webext-bridge'
|
||||
import { browserSettings, Tabs } from 'webextension-polyfill'
|
||||
|
||||
// only on dev mode
|
||||
if (import.meta.hot) {
|
||||
// @ts-expect-error for background HMR
|
||||
import('/@vite/client')
|
||||
// load latest content script
|
||||
import('./contentScriptHMR')
|
||||
}
|
||||
|
||||
let previousTabId = 0
|
||||
|
||||
// communication example: send previous tab title from background page
|
||||
// see shim.d.ts for type declaration
|
||||
browser.tabs.onActivated.addListener(async({ tabId }) => {
|
||||
if (!previousTabId) {
|
||||
previousTabId = tabId
|
||||
return
|
||||
}
|
||||
|
||||
let tab: Tabs.Tab
|
||||
|
||||
try {
|
||||
tab = await browser.tabs.get(previousTabId)
|
||||
previousTabId = tabId
|
||||
}
|
||||
catch {
|
||||
return
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('previous tab', tab)
|
||||
sendMessage('tab-prev', { title: tab.title }, { context: 'content-script', tabId })
|
||||
})
|
||||
|
||||
onMessage('get-current-tab', async() => {
|
||||
try {
|
||||
const tab = await browser.tabs.get(previousTabId)
|
||||
return {
|
||||
title: tab?.title,
|
||||
}
|
||||
}
|
||||
catch {
|
||||
return {
|
||||
title: undefined,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// preinsert css
|
||||
browser.tabs.onUpdated.addListener((tabId: number, changInfo: Tabs.OnUpdatedChangeInfoType, tab: Tabs.Tab) => {
|
||||
if (/https?:\/\/bilibili.com\/?$/.test(`${tab.url}`)
|
||||
|| /https?:\/\/www.bilibili.com\/?$/.test(`${tab.url}`)
|
||||
|| /https?:\/\/bilibili.com\/\?spm_id_from=.*/.test(`${tab.url}`)
|
||||
|| /https?:\/\/www.bilibili.com\/\?spm_id_from=(.)*/.test(`${tab.url}`)) {
|
||||
const isDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
if (changInfo.status === 'loading') {
|
||||
const css = `
|
||||
body::after {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
content: '';
|
||||
background: ${isDark ? 'hsl(230 12% 6%)' : 'rgb(243 244 246)'}!important;
|
||||
z-index: 99999;
|
||||
}
|
||||
`
|
||||
|
||||
browser.tabs.insertCSS(tabId, {
|
||||
code: css,
|
||||
runAt: 'document_start',
|
||||
matchAboutBlank: true,
|
||||
})
|
||||
}
|
||||
|
||||
else if (changInfo.status === 'complete') {
|
||||
const css = `
|
||||
body::after {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
browser.tabs.insertCSS(tabId, {
|
||||
code: css,
|
||||
runAt: 'document_start',
|
||||
matchAboutBlank: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
chrome.runtime.onMessage.addListener((message: any, sender: chrome.runtime.MessageSender, sendResponse: any) => {
|
||||
const APP_URL = 'https://app.bilibili.com'
|
||||
const API_URL = 'https://api.bilibili.com'
|
||||
|
||||
if (message.contentScriptQuery === 'getRecommendVideo') {
|
||||
const url = `${APP_URL}/x/feed/index?build=1&idx=${message.idx}&appkey=27eb53fc9058f8c3&access_key=${message.accessKey}`
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => sendResponse(data))
|
||||
.catch(error => console.error(error))
|
||||
return true
|
||||
}
|
||||
if (message.contentScriptQuery === 'getUserInfo') {
|
||||
// https://api.bilibili.com/x/web-interface/nav
|
||||
// const url = `${API_URL}/x/web-interface/card?mid=${message.mid}&photo=true`
|
||||
const url = `${API_URL}/x/web-interface/nav`
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => sendResponse(data))
|
||||
.catch(error => console.error(error))
|
||||
return true // Will respond asynchronously.
|
||||
}
|
||||
if (message.contentScriptQuery === 'getUserStat') {
|
||||
const url = `${API_URL}/x/web-interface/nav/stat`
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => sendResponse(data))
|
||||
.catch(error => console.error(error))
|
||||
return true
|
||||
}
|
||||
if (message.contentScriptQuery === 'getSearchSuggestion') {
|
||||
const url = `https://s.search.bilibili.com/main/suggest?term=${message.term}`
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => sendResponse(data))
|
||||
.catch(error => console.error(error))
|
||||
return true
|
||||
}
|
||||
if (message.contentScriptQuery === 'logout') {
|
||||
const url = `https://passport.bilibili.com/login/exit/v2?biliCSRF=${message.biliCSRF}`
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
biliCSRF: message.biliJct,
|
||||
}),
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => sendResponse(data))
|
||||
.catch(error => console.error(error))
|
||||
return true
|
||||
}
|
||||
if (message.contentScriptQuery === 'getUnreadMsg') {
|
||||
const url = `${API_URL}/x/msgfeed/unread?build=0&mobi_app=web`
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => sendResponse(data))
|
||||
.catch(error => console.error(error))
|
||||
return true
|
||||
}
|
||||
if (message.contentScriptQuery === 'getUnreadDm') {
|
||||
const url = 'https://api.vc.bilibili.com/session_svr/v1/session_svr/single_unread?build=0&mobi_app=web&unread_type=0'
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => sendResponse(data))
|
||||
.catch(error => console.error(error))
|
||||
return true
|
||||
}
|
||||
if (message.contentScriptQuery === 'getNewMomentsCount') {
|
||||
// https://api.bilibili.com/x/web-interface/dynamic/entrance
|
||||
const url = `${API_URL}/x/web-interface/dynamic/entrance`
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => sendResponse(data))
|
||||
.catch(error => console.error(error))
|
||||
return true
|
||||
}
|
||||
})
|
||||
18
src/components/Logo.vue
Normal file
18
src/components/Logo.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<a
|
||||
class="icon-btn mx-2 text-2xl"
|
||||
rel="noreferrer"
|
||||
href="https://github.com/antfu/vitesse-webext"
|
||||
target="_blank"
|
||||
title="GitHub"
|
||||
>
|
||||
<pixelarticons-power />
|
||||
<user-line />
|
||||
<IconAcceptDatabase />
|
||||
</a>
|
||||
</template>
|
||||
<script setup>
|
||||
import IconAccessibility from '~icons/carbon/accessibility'
|
||||
import IconAccountBox from '~icons/mdi/account-box'
|
||||
// import IconAcceptDatabase from '~icons/flat-color-icons/accept-database'
|
||||
</script>
|
||||
11
src/components/README.md
Normal file
11
src/components/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
## Components
|
||||
|
||||
Components in this dir will be auto-registered and on-demand, powered by [`vite-plugin-components`](https://github.com/antfu/vite-plugin-components).
|
||||
|
||||
Components can be shared in all views.
|
||||
|
||||
### Icons
|
||||
|
||||
You can use icons from almost any icon sets by the power of [Iconify](https://iconify.design/).
|
||||
|
||||
It will only bundle the icons you use. Check out [vite-plugin-icons](https://github.com/antfu/vite-plugin-icons) for more details.
|
||||
229
src/components/SearchBar/index.vue
Normal file
229
src/components/SearchBar/index.vue
Normal file
@@ -0,0 +1,229 @@
|
||||
<template>
|
||||
<div
|
||||
id="search-wrap"
|
||||
w="full max-500px"
|
||||
m="x-8"
|
||||
pos="relative"
|
||||
>
|
||||
<div
|
||||
v-if="isFocus"
|
||||
class="fixed top-0 left-0"
|
||||
w="full"
|
||||
h="full"
|
||||
content="~"
|
||||
@click="isFocus = false"
|
||||
></div>
|
||||
<div class="search-bar">
|
||||
<input
|
||||
v-model.trim="keyword"
|
||||
type="text"
|
||||
@focus="isFocus = true"
|
||||
@input="onSearchChange()"
|
||||
@keyup.enter="goToSearchPage(keyword)"
|
||||
@keyup.up="onUp"
|
||||
@keyup.down="onDown"
|
||||
/>
|
||||
<button class="search-btn" @click="goToSearchPage(keyword)">
|
||||
<tabler:search />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<transition>
|
||||
<div v-if="isFocus && !keyword && searchHistory.length !== 0" id="search-history">
|
||||
<div
|
||||
v-for="(item) in searchHistory"
|
||||
:key="item.value"
|
||||
class="history-item"
|
||||
@click="goToSearchPage(item.value)"
|
||||
>
|
||||
{{ item.value }}
|
||||
<button class="delete" @click.stop="onDelHistoryItem(item.value)">
|
||||
<ic-baseline-clear />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<transition>
|
||||
<div v-if="isFocus && Object.keys(suggestions).length !== 0" id="search-suggestion">
|
||||
<div
|
||||
v-for="(item, index) in suggestions"
|
||||
:key="index"
|
||||
class="suggestion-item"
|
||||
@click="goToSearchPage(keyword)"
|
||||
>
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { addSearchHistory, getSearchHistory, HistoryItem, removeSearchHistory } from './search-history-provider'
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
isFocus: false,
|
||||
keyword: '',
|
||||
suggestions: {},
|
||||
selectedIndex: -1,
|
||||
searchHistory: [] as HistoryItem[],
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.searchHistory = getSearchHistory()
|
||||
},
|
||||
methods: {
|
||||
onSearchChange() {
|
||||
browser.runtime
|
||||
.sendMessage({
|
||||
contentScriptQuery: 'getSearchSuggestion',
|
||||
term: this.keyword,
|
||||
})
|
||||
.then((res) => {
|
||||
this.suggestions = res
|
||||
})
|
||||
},
|
||||
goToSearchPage(keyword: string) {
|
||||
if (keyword) {
|
||||
window.open(`https://search.bilibili.com/all?keyword=${keyword}`, '_blank')
|
||||
const searchItem = {
|
||||
value: keyword,
|
||||
timestamp: Number(new Date()),
|
||||
}
|
||||
addSearchHistory(searchItem)
|
||||
this.searchHistory = getSearchHistory()
|
||||
}
|
||||
},
|
||||
onDelHistoryItem(value: string) {
|
||||
removeSearchHistory(value)
|
||||
this.searchHistory = getSearchHistory()
|
||||
},
|
||||
onUp() {
|
||||
if (this.selectedIndex <= 0) {
|
||||
this.selectedIndex = 0
|
||||
return
|
||||
}
|
||||
|
||||
this.selectedIndex--
|
||||
this.keyword = this.suggestions[this.selectedIndex].value
|
||||
|
||||
document.querySelectorAll('.suggestion-item').forEach((item, index) => {
|
||||
if (index === this.selectedIndex)
|
||||
item.classList.add('active')
|
||||
else
|
||||
item.classList.remove('active')
|
||||
})
|
||||
|
||||
document.querySelectorAll('.history-item').forEach((item, index) => {
|
||||
if (index === this.selectedIndex)
|
||||
item.classList.add('active')
|
||||
else
|
||||
item.classList.remove('active')
|
||||
})
|
||||
},
|
||||
onDown() {
|
||||
if (this.selectedIndex >= Object.keys(this.suggestions).length - 1) {
|
||||
this.selectedIndex = Object.keys(this.suggestions).length - 1
|
||||
return
|
||||
}
|
||||
|
||||
this.selectedIndex++
|
||||
this.keyword = this.suggestions[this.selectedIndex].value
|
||||
|
||||
document.querySelectorAll('.suggestion-item').forEach((item, index) => {
|
||||
if (index === this.selectedIndex)
|
||||
item.classList.add('active')
|
||||
else
|
||||
item.classList.remove('active')
|
||||
})
|
||||
|
||||
document.querySelectorAll('.history-item').forEach((item, index) => {
|
||||
if (index === this.selectedIndex)
|
||||
item.classList.add('active')
|
||||
else
|
||||
item.classList.remove('active')
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.v-enter-active,
|
||||
.v-leave-active {
|
||||
@apply transition-all duration-300
|
||||
}
|
||||
|
||||
.v-enter-from,
|
||||
.v-leave-to {
|
||||
@apply transform translate-y-4 opacity-0 scale-95
|
||||
}
|
||||
|
||||
#search-wrap {
|
||||
|
||||
@mixin card-content {
|
||||
@apply text-base border-none outline-none w-full
|
||||
bg-$bew-content-1 backdrop-$bew-filter-glass;
|
||||
box-shadow: var(--bew-shadow-2);
|
||||
backdrop-filter: var(--bew-filter-glass);
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
@apply flex items-center relative;
|
||||
|
||||
input {
|
||||
@include card-content;
|
||||
@apply rounded-$bew-radius pl-6 pr-16 py-3 text-$bew-text-1;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
@apply p-2 rounded-full text-lg leading-0 duration-300
|
||||
border-none outline-none absolute right-2
|
||||
text-$bew-text-1
|
||||
hover:bg-$bew-fill-2;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin search-content {
|
||||
@include card-content;
|
||||
@apply mt-2 p-2 absolute rounded-$bew-radius
|
||||
hover:block;
|
||||
}
|
||||
|
||||
@mixin search-content-item {
|
||||
@apply px-4 py-2 w-full rounded-$bew-radius duration-300 cursor-pointer
|
||||
not-first:mt-1
|
||||
hover:bg-$bew-fill-2;
|
||||
|
||||
&.active {
|
||||
@apply bg-$bew-fill-2;
|
||||
}
|
||||
}
|
||||
|
||||
#search-history,
|
||||
#search-suggestion {
|
||||
@include search-content;
|
||||
|
||||
.history-item,
|
||||
.suggestion-item {
|
||||
@include search-content-item;
|
||||
}
|
||||
}
|
||||
|
||||
#search-history {
|
||||
.history-item {
|
||||
@apply flex justify-between items-center;
|
||||
|
||||
.delete {
|
||||
@apply rounded-full duration-300 text-base
|
||||
leading-0 pointer-events-auto cursor-pointer
|
||||
text-$bew-text-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
48
src/components/SearchBar/search-history-provider.ts
Normal file
48
src/components/SearchBar/search-history-provider.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
const SEARCH_HISTORY_KEY = 'bew_search_history'
|
||||
const SEARCH_HISTORY_LIMIT = 10
|
||||
|
||||
export interface HistoryItem {
|
||||
value: string
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
const historySort = (historyItems: HistoryItem[]) => {
|
||||
historyItems.sort((a, b) => b.timestamp - a.timestamp)
|
||||
return historyItems
|
||||
}
|
||||
|
||||
export const getSearchHistory = (): HistoryItem[] => {
|
||||
const history = localStorage.getItem(SEARCH_HISTORY_KEY)
|
||||
if (!history) {
|
||||
localStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify([]))
|
||||
return []
|
||||
}
|
||||
return historySort(JSON.parse(history))
|
||||
}
|
||||
|
||||
export const addSearchHistory = (historyItem: HistoryItem) => {
|
||||
let history = getSearchHistory()
|
||||
|
||||
let hasSameValue = false
|
||||
history.forEach((item) => {
|
||||
if (item.value === historyItem.value) {
|
||||
item.timestamp = historyItem.timestamp
|
||||
hasSameValue = true
|
||||
}
|
||||
})
|
||||
if (!hasSameValue) history.unshift(historyItem)
|
||||
|
||||
// if out of limit, remove overflow items
|
||||
history = history.filter((item, index) => {
|
||||
if (index < SEARCH_HISTORY_LIMIT) return item
|
||||
else return false
|
||||
})
|
||||
|
||||
localStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(history))
|
||||
}
|
||||
|
||||
export const removeSearchHistory = (value: string) => {
|
||||
let history = getSearchHistory()
|
||||
history = history.filter(item => item.value !== value)
|
||||
localStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(history))
|
||||
}
|
||||
101
src/components/Settings.vue
Normal file
101
src/components/Settings.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<div class="fixed w-full h-full top-0 left-0 bg-black bg-opacity-30" z="9998" @click="close"></div>
|
||||
|
||||
<div id="settings-window" z="9999">
|
||||
<div
|
||||
class="absolute right-0 top-0 transform translate-x-1/2 -translate-y-1/2"
|
||||
text="2xl"
|
||||
font="leading-0"
|
||||
bg="$bew-content-solid-1"
|
||||
w="32px"
|
||||
h="32px"
|
||||
p="1"
|
||||
rounded="full"
|
||||
shadow="md"
|
||||
cursor="pointer"
|
||||
@click="close"
|
||||
>
|
||||
<ic-baseline-clear />
|
||||
</div>
|
||||
<div text="3xl" m="b-4">
|
||||
Settings
|
||||
</div>
|
||||
<div class="settings-item">
|
||||
<div>
|
||||
Authorize BewlyBewly to use Access Key
|
||||
<br />
|
||||
<span class="desc">this change will make you able to use recommend videos</span>
|
||||
</div>
|
||||
<button
|
||||
v-if="accessKey + '' === 'undefined' || accessKey + '' === 'null'"
|
||||
ref="authorizeBtn"
|
||||
class="btn"
|
||||
@click="onAuthorize"
|
||||
>
|
||||
Authorize
|
||||
</button>
|
||||
<button v-else class="line-btn" @click="onRevoke">
|
||||
<span>Revoke</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="settings-item">
|
||||
<div>
|
||||
Topbar visiable
|
||||
<br />
|
||||
<span class="desc">Compatible with bilibili evolved</span>
|
||||
</div>
|
||||
<div>
|
||||
<label for="topbarVisiable" class="chk-btn" cursor="pointer" pointer="auto">
|
||||
<template v-if="isShowTopbar">Show</template>
|
||||
<template v-else>Hidden</template>
|
||||
<input id="topbarVisiable" v-model="isShowTopbar" type="checkbox" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { grantAccessKey, revokeAccessKey } from '~/utils/index'
|
||||
import { isShowTopbar, apperance, accessKey } from '~/logic/storage'
|
||||
|
||||
export default defineComponent({
|
||||
emits: ['close'],
|
||||
data() {
|
||||
return {
|
||||
isShowTopbar,
|
||||
apperance,
|
||||
accessKey,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
this.$emit('close')
|
||||
},
|
||||
onAuthorize() {
|
||||
const authorizeBtn = this.$refs.authorizeBtn as HTMLButtonElement
|
||||
grantAccessKey(authorizeBtn)
|
||||
},
|
||||
onRevoke() {
|
||||
revokeAccessKey()
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
#settings-window {
|
||||
@apply fixed top-1/5 left-1/2 w-700px h-1/2
|
||||
transform -translate-x-1/2
|
||||
rounded-$bew-radius p-8
|
||||
bg-$bew-content-solid-1;
|
||||
box-shadow: var(--bew-shadow-3);
|
||||
|
||||
.settings-item {
|
||||
@apply flex justify-between items-center py-2;
|
||||
.desc {
|
||||
@apply text-xs text-$bew-text-3;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
89
src/components/Topbar/LogoMenuDropdown.vue
Normal file
89
src/components/Topbar/LogoMenuDropdown.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div
|
||||
style="backdrop-filter: var(--bew-filter-glass); box-shadow: var(--bew-shadow-3);"
|
||||
flex="~"
|
||||
p="4"
|
||||
bg="$bew-content-1"
|
||||
rounded="$bew-radius"
|
||||
>
|
||||
<ul
|
||||
v-for="(item, index) in [0, 10, 20, 30]"
|
||||
:key="index"
|
||||
m="r-1 last-of-type:r-0"
|
||||
>
|
||||
<li
|
||||
v-for="genre in genres.slice(item, item + 10)"
|
||||
:key="genre.name"
|
||||
m="b-1 last-of-type:b-0"
|
||||
text="sm"
|
||||
>
|
||||
<a
|
||||
:href="genre.href"
|
||||
target="_blank"
|
||||
flex="~"
|
||||
items="center"
|
||||
w="170px"
|
||||
p="x-2 y-2"
|
||||
hover:bg="$bew-fill-2"
|
||||
transition="duration-300"
|
||||
border="rounded-$bew-radius"
|
||||
>
|
||||
<svg aria-hidden="true" class="svg-icon">
|
||||
<use :xlink:href="genre.icon" />
|
||||
</svg>
|
||||
<span>{{ genre.name }}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const genres = [
|
||||
{ name: 'Anime', icon: '#channel-anime', href: 'https://www.bilibili.com/anime' },
|
||||
{ name: 'Movies', icon: '#channel-movie', href: 'https://www.bilibili.com/movie' },
|
||||
{ name: 'Chinese anime', icon: '#channel-guochuang', href: 'https://www.bilibili.com/guochuang' },
|
||||
{ name: 'TV shows', icon: '#channel-teleplay', href: 'https://www.bilibili.com/tv' },
|
||||
{ name: 'Variety shows', icon: '#channel-zongyi', href: 'https://www.bilibili.com/variety' },
|
||||
{ name: 'Documentary films', icon: '#channel-documentary', href: 'https://www.bilibili.com/documentary' },
|
||||
{ name: 'Animations', icon: '#channel-douga', href: 'https://www.bilibili.com/v/douga' },
|
||||
{ name: 'Gaming', icon: '#channel-game', href: 'https://www.bilibili.com/v/game' },
|
||||
{ name: 'Kichiku', icon: '#channel-kichiku', href: 'https://www.bilibili.com/v/kichiku' },
|
||||
{ name: 'Music', icon: '#channel-music', href: 'https://www.bilibili.com/v/music' },
|
||||
{ name: 'Dance', icon: '#channel-dance', href: 'https://www.bilibili.com/v/dance' },
|
||||
{ name: 'Cinephile', icon: '#channel-cinephile', href: 'https://www.bilibili.com/v/cinephile' },
|
||||
{ name: 'Showbiz', icon: '#channel-ent', href: 'https://www.bilibili.com/v/ent' },
|
||||
{ name: 'Knowledge', icon: '#channel-knowledge', href: 'https://www.bilibili.com/v/knowledge' },
|
||||
{ name: 'Technology', icon: '#channel-tech', href: 'https://www.bilibili.com/v/tech' },
|
||||
{ name: 'News', icon: '#channel-information', href: 'https://www.bilibili.com/v/information' },
|
||||
{ name: 'Foods', icon: '#channel-food', href: 'https://www.bilibili.com/v/food' },
|
||||
{ name: 'Life', icon: '#channel-life', href: 'https://www.bilibili.com/v/life' },
|
||||
{ name: 'Cars', icon: '#channel-car', href: 'https://www.bilibili.com/v/car' },
|
||||
{ name: 'Fashion', icon: '#channel-fashion', href: 'https://www.bilibili.com/v/fashion' },
|
||||
{ name: 'Sports', icon: '#channel-sports', href: 'https://www.bilibili.com/v/sports' },
|
||||
{ name: 'Animals', icon: '#channel-animal', href: 'https://www.bilibili.com/v/animal' },
|
||||
{ name: 'VLOG', icon: '#channel-vlog', href: 'https://www.bilibili.com/v/life/daily/#/530003' },
|
||||
{ name: 'Funny', icon: '#channel-gaoxiao', href: 'https://www.bilibili.com/v/life/funny' },
|
||||
{ name: 'Standalone gaming', icon: '#channel-danjiyouxi', href: 'https://www.bilibili.com/v/game/stand_alone' },
|
||||
{ name: 'Vtubers & Vups', icon: '#channel-vtuber', href: 'https://www.bilibili.com/v/virtual' },
|
||||
{ name: 'Charitable events', icon: '#channel-love', href: 'https://love.bilibili.com' },
|
||||
{ name: 'MOOCs', icon: '#channel-gongkaike', href: 'https://www.bilibili.com/mooc' },
|
||||
{ name: 'Articles', icon: '#channel-read', href: 'https://www.bilibili.com/read/home' },
|
||||
{ name: 'Live', icon: '#channel-live', href: 'https://live.bilibili.com' },
|
||||
{ name: 'Activities', icon: '#channel-activity', href: 'https://www.bilibili.com/blackboard/activity-list.html' },
|
||||
{ name: 'Paid courses', icon: '#channel-zhishi', href: 'https://www.bilibili.com/cheese' },
|
||||
{ name: 'Community', icon: '#channel-blackroom', href: 'https://www.bilibili.com/blackboard/activity-5zJxM3spoS.html' },
|
||||
{ name: 'Music plus', icon: '#channel-musicplus', href: 'https://www.bilibili.com/v/musicplus' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.svg-icon {
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
vertical-align: bottom;
|
||||
fill: currentColor;
|
||||
overflow: hidden;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
</style>
|
||||
4
src/components/Topbar/NotiPopup.vue
Normal file
4
src/components/Topbar/NotiPopup.vue
Normal file
@@ -0,0 +1,4 @@
|
||||
<template>
|
||||
<div>
|
||||
</div>
|
||||
</template>
|
||||
175
src/components/Topbar/UserPanelDropdown.vue
Normal file
175
src/components/Topbar/UserPanelDropdown.vue
Normal file
@@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<div id="user-info-panel">
|
||||
<div id="base-info">
|
||||
{{ userInfo.uname ? userInfo.uname : '-' }}
|
||||
<div
|
||||
class="bg-$bew-theme-color px-3 py-1 ml-2 text-white rounded-$bew-radius text-base leading-none"
|
||||
>
|
||||
{{ userInfo.level_info?.current_level ? userInfo.level_info.current_level : '0' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-$bew-text-2" flex="~" items="center" justify="center" m="t-1 b-3">
|
||||
<a
|
||||
class="mr-4"
|
||||
href="https://account.bilibili.com/account/coin"
|
||||
target="_blank"
|
||||
>Money: {{ userInfo.money }}</a>
|
||||
<a
|
||||
href="https://pay.bilibili.com/pay-v2-web/bcoin_index"
|
||||
target="_blank"
|
||||
>B-coin: {{ userInfo.wallet?.bcoin_balance }}</a>
|
||||
</div>
|
||||
<div id="channel-info">
|
||||
<a
|
||||
:href="'https://space.bilibili.com/' + mid + '/fans/follow'"
|
||||
target="_blank"
|
||||
:title="userStat.following"
|
||||
>
|
||||
<div class="num">{{ userStat.following ? numFormatter(userStat.following) : '0' }}</div>
|
||||
<div>following</div>
|
||||
</a>
|
||||
<a
|
||||
:href="'https://space.bilibili.com/' + mid + '/fans/fans'"
|
||||
target="_blank"
|
||||
:title="userStat.follower"
|
||||
>
|
||||
<div class="num">{{ userStat.follower ? numFormatter(userStat.follower) : '0' }}</div>
|
||||
<div>follower</div>
|
||||
</a>
|
||||
<a
|
||||
href="https://t.bilibili.com/"
|
||||
target="_blank"
|
||||
:title="userStat.dynamic_count"
|
||||
>
|
||||
<div class="num">{{ userStat.dynamic_count ? numFormatter(userStat.dynamic_count) : '0' }}</div>
|
||||
<div>posts</div>
|
||||
</a>
|
||||
</div>
|
||||
<div id="other-link">
|
||||
<a href="https://account.bilibili.com/account/home" target="_blank">
|
||||
Account settings
|
||||
<tabler:arrow-right />
|
||||
</a>
|
||||
<a href="https://member.bilibili.com/v2#/upload-manager/article" target="_blank">
|
||||
Uploads manager
|
||||
<tabler:arrow-right />
|
||||
</a>
|
||||
<a href="https://pay.bilibili.com/" target="_blank">
|
||||
B-coin Wallet
|
||||
<tabler:arrow-right />
|
||||
</a>
|
||||
<a href="https://show.bilibili.com/orderlist" target="_blank">
|
||||
Orders
|
||||
<tabler:arrow-right />
|
||||
</a>
|
||||
<a href="https://link.bilibili.com/p/center/index" target="_blank">
|
||||
My stream info
|
||||
<tabler:arrow-right />
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/cheese/mine/list" target="_blank">
|
||||
My course
|
||||
<tabler:arrow-right />
|
||||
</a>
|
||||
<div id="logout" @click="logout()">
|
||||
Log out
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { revokeAccessKey } from '../../utils/index'
|
||||
import { getCSRF, getUserID, numFormatter } from '~/utils'
|
||||
export default defineComponent({
|
||||
props: {
|
||||
userInfo: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
mid: getUserID(),
|
||||
userStat: {} as any,
|
||||
numFormatter,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
browser.runtime
|
||||
.sendMessage({
|
||||
contentScriptQuery: 'getUserStat',
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.code === 0) this.userStat = res.data
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
async logout() {
|
||||
revokeAccessKey()
|
||||
await browser.runtime
|
||||
.sendMessage({
|
||||
contentScriptQuery: 'logout',
|
||||
biliCSRF: getCSRF(),
|
||||
})
|
||||
location.reload()
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#user-info-panel {
|
||||
@apply p-4 rounded-$bew-radius w-300px -z-1
|
||||
bg-$bew-content-solid-1;
|
||||
box-shadow: var(--bew-shadow-3);
|
||||
}
|
||||
|
||||
#base-info {
|
||||
@apply mt-8 text-xl font-medium flex items-center justify-center;
|
||||
}
|
||||
|
||||
#channel-info {
|
||||
@apply grid grid-cols-3 gap-x-2 mb-2;
|
||||
|
||||
a {
|
||||
@apply p-2 m-0 rounded-$bew-radius text-sm
|
||||
flex flex-col items-center transition-all duration-300
|
||||
bg-$bew-fill-1
|
||||
hover:$bg-$bew-theme-color
|
||||
hover:text-white '!hover:children:text-white';
|
||||
|
||||
> * {
|
||||
@apply duration-300;
|
||||
}
|
||||
|
||||
.num {
|
||||
@apply font-bold text-xl;
|
||||
|
||||
+ div {
|
||||
@apply text-$bew-text-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#other-link {
|
||||
@apply flex justify-between flex-col mt-4;
|
||||
|
||||
a {
|
||||
@apply px-4 py-2 mb-1 flex justify-between items-center
|
||||
rounded-$bew-radius transition-all duration-300
|
||||
hover:bg-$bew-fill-2;
|
||||
|
||||
span {
|
||||
@apply text-$bew-text-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#logout {
|
||||
@apply text-red-400 '!block' px-4 py-2 rounded-$bew-radius
|
||||
duration-300 cursor-pointer
|
||||
hover:bg-$bew-fill-2;
|
||||
}
|
||||
</style>
|
||||
354
src/components/Topbar/index.vue
Normal file
354
src/components/Topbar/index.vue
Normal file
@@ -0,0 +1,354 @@
|
||||
<template>
|
||||
<header>
|
||||
<transition name="topbar">
|
||||
<div
|
||||
v-show="showTopbarMask"
|
||||
class="fixed top-0 left-0"
|
||||
w="full"
|
||||
h="160px"
|
||||
opacity="100"
|
||||
pointer="none"
|
||||
style="background: linear-gradient(var(--bew-bg), transparent)"
|
||||
></div>
|
||||
</transition>
|
||||
|
||||
<div
|
||||
class="left-side"
|
||||
@mouseenter.self="showLogoMenuDropdown()"
|
||||
@mouseleave.self="closeLogoMenuDropdown()"
|
||||
>
|
||||
<div
|
||||
ref="logo"
|
||||
class="logo"
|
||||
>
|
||||
<svg
|
||||
t="1645466458357"
|
||||
class="icon"
|
||||
viewBox="0 0 2299 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="2663"
|
||||
width="60"
|
||||
height="60"
|
||||
>
|
||||
<path
|
||||
d="M1775.840814 322.588002c6.0164 1.002733 53.144869-9.525967 55.150336-6.016401 3.0082 4.5123 24.065601 155.92504 18.550567 156.927774s-44.621635 10.027334-44.621635 10.027334c-3.0082-20.556034-28.577901-147.903173-29.079268-160.938707m75.205003-14.539634l20.556034 162.944174c10.5287-0.501367 53.144869-3.509567 57.155803-4.010934-6.0164-61.668103-16.545101-158.933241-16.545101-158.93324-20.054668-4.010934-41.112069-4.010934-61.166736 0m-40.610702 226.116376s92.752838-23.564234 126.344406-12.0328c17.046467 61.668103 48.131202 407.611118 51.139402 421.649386-21.057401 2.506833-90.246004 8.523234-95.761037 10.027333-4.5123-26.071068-81.72277-403.098818-81.722771-419.643919m343.436183-207.565809c5.515034 1.5041 54.648969-5.013667 55.150335-1.5041 1.002733 12.032801 6.0164 157.42914 0.501367 157.930507s-44.621635 4.010934-44.621635 4.010934c-1.002733-20.054668-12.032801-146.90044-11.030067-160.437341m75.70637-4.010933l4.010933 160.938707c10.5287 0 52.643502 2.506833 57.155803 2.005467-1.002733-61.668103 0-158.933241 0-158.933241-20.054668-3.509567-40.610702-5.013667-61.166736-4.010933m-64.676303 216.089043s94.758304-12.534167 126.845772 2.506833c7.019134 72.196803 6.0164 408.613852 7.019134 422.652119-21.558768 0-90.246004 1.002733-95.761038 2.005467-1.002733-26.071068-39.607968-410.619319-38.103868-427.164419m-220.099977-413.627519c54.648969 278.759879 96.262404 755.058234 97.766504 785.641602 0 0 43.117535 1.002733 91.750105 4.010934C2105.740095 614.383415 2070.644427 134.575493 2071.145794 119.033126c-12.032801-13.536901-126.344406 6.0164-126.344406 6.0164m-120.328005 659.297196c-10.5287-78.213204-290.291313-166.955108-447.720454-138.377206 0 0-19.553301-172.470141-27.073801-339.425248-6.517767-143.390873-1.002733-282.770813 0.501366-305.833681-10.5287-7.5205-123.837572 46.627102-185.004308 69.188603 0 0 73.199537 309.844614 126.344406 952.59671 0 0 84.730971 9.0246 230.12731-19.051934s317.365114-115.815705 302.825481-219.097244m-341.932083 140.88404l-24.566967-176.982441c6.0164-3.0082 156.927774 53.144869 172.971507 63.172203-2.506833 11.030067-148.40454 113.810238-148.40454 113.810238M610.664628 322.588002c6.0164 1.002733 53.144869-9.525967 55.150335-6.016401 3.0082 4.5123 24.065601 155.92504 18.550568 156.927774s-44.621635 10.027334-44.621635 10.027334c-3.0082-20.556034-28.577901-147.903173-29.079268-160.938707m75.205003-14.539634l20.556034 162.944174c10.5287-0.501367 53.144869-3.509567 57.155803-4.010934-6.517767-61.668103-16.545101-158.933241-16.545101-158.93324-20.054668-4.010934-41.112069-4.010934-61.166736 0m-40.610702 226.116376s92.752838-23.564234 126.344406-12.0328c17.046467 61.668103 48.131202 407.611118 51.139402 421.649386-21.057401 2.506833-90.246004 8.523234-95.761037 10.027333-4.5123-26.071068-81.72277-403.098818-81.722771-419.643919m343.436182-207.565809c5.515034 1.5041 54.648969-5.013667 55.150336-1.5041 1.002733 12.032801 6.0164 157.42914 0.501367 157.930507s-44.621635 4.010934-44.621635 4.010934c-1.002733-20.054668-11.531434-146.90044-11.030068-160.437341m75.706371-4.010933l4.010933 160.938707c10.5287 0 52.643502 2.506833 57.155803 2.005467-1.002733-61.668103 0-158.933241 0-158.933241-20.054668-3.509567-40.610702-4.5123-61.166736-4.010933m-64.676303 216.089043s94.758304-12.534167 126.845772 2.506833c7.019134 72.196803 6.0164 408.613852 7.019134 422.652119-21.558768 0-90.246004 1.002733-95.761038 2.005467-0.501367-26.071068-39.607968-410.619319-38.103868-427.164419m-220.099977-413.627519c54.648969 278.759879 96.262404 755.058234 97.766504 785.641602 0 0 43.117535 1.002733 91.750105 4.010934-28.577901-300.318647-63.67357-780.126569-63.172203-796.170303-12.032801-13.035534-126.344406 6.517767-126.344406 6.517767m-120.328005 659.297196c-10.5287-78.213204-290.291313-166.955108-447.720454-138.377206 0 0-19.553301-172.470141-27.073801-339.425248-6.517767-143.390873-1.002733-282.770813 0.501366-305.833681C174.475608-6.308547 61.166736 47.337689 0 69.89919c0 0 73.199537 309.844614 126.344406 952.59671 0 0 84.730971 9.0246 230.12731-19.051934s317.365114-115.815705 302.825481-219.097244m-341.932083 140.88404l-24.566967-176.982441c6.0164-3.0082 156.927774 53.144869 172.971507 63.172203-2.506833 11.030067-148.40454 113.810238-148.40454 113.810238"
|
||||
p-id="2664"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<tabler:chevron-down w="!4" h="!4" m="l-4" icon="stroke-4 fill-$bew-text-1" />
|
||||
</div>
|
||||
<transition name="slide">
|
||||
<logo-menu-dropdown
|
||||
v-if="showLogoDropDown"
|
||||
pos="absolute"
|
||||
m="t-2"
|
||||
after:content="t"
|
||||
after:select="none"
|
||||
after:opacity="0"
|
||||
after:w="full"
|
||||
after:h="2"
|
||||
after:pos="absolute -top-2 left-0"
|
||||
after:z="-1"
|
||||
></logo-menu-dropdown>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<search-bar></search-bar>
|
||||
|
||||
<div class="right-side">
|
||||
<div v-if="!isLogin" class="right-side-item">
|
||||
<a href="https://passport.bilibili.com/login" class="login">
|
||||
<ic-outline-account-circle class="text-base mr-2" />LOGIN IN
|
||||
</a>
|
||||
</div>
|
||||
<template v-if="isLogin">
|
||||
<div
|
||||
id="avatar"
|
||||
class="right-side-item"
|
||||
@mouseenter="openUserPanel"
|
||||
@mouseleave="closeUserPanel"
|
||||
>
|
||||
<div
|
||||
id="avatar-img"
|
||||
ref="avatarImg"
|
||||
class="rounded-full z-1 w-40px h-40px bg-$bew-fill-3 bg-cover bg-center"
|
||||
:style="{ backgroundImage: `url(${(userInfo.face + '').replace('http:', '')})` }"
|
||||
></div>
|
||||
<div
|
||||
id="avatar-shadow"
|
||||
ref="avatarShadow"
|
||||
class="absolute top-0 rounded-full z-0 w-40px h-40px filter blur-sm bg-cover bg-center"
|
||||
opacity="80"
|
||||
:style="{ backgroundImage: `url(${(userInfo.face + '').replace('http:', '')})` }"
|
||||
></div>
|
||||
|
||||
<transition name="slide">
|
||||
<user-panel-dropdown
|
||||
v-if="showUserPanel"
|
||||
:user-info="userInfo"
|
||||
pos="absolute top-60px left-1/2"
|
||||
m="-l-150px"
|
||||
></user-panel-dropdown>
|
||||
</transition>
|
||||
</div>
|
||||
<div class="right-side-item">
|
||||
<div
|
||||
v-if="unReadmessageCount !== 0"
|
||||
class="unread-message"
|
||||
>
|
||||
{{ unReadmessageCount > 999 ? '999+' : unReadmessageCount }}
|
||||
</div>
|
||||
<a href="https://message.bilibili.com" target="_blank" title="Notifications">
|
||||
<tabler:bell />
|
||||
</a>
|
||||
</div>
|
||||
<div class="right-side-item">
|
||||
<div
|
||||
v-if="newMomentsCount !== 0"
|
||||
class="unread-message"
|
||||
>
|
||||
{{ newMomentsCount > 999 ? '999+' : newMomentsCount }}
|
||||
</div>
|
||||
<a href="https://t.bilibili.com" target="_blank" title="Moments">
|
||||
<tabler:windmill />
|
||||
</a>
|
||||
</div>
|
||||
<div class="right-side-item">
|
||||
<a
|
||||
:href="'https://space.bilibili.com/' + mid + '/favlist'"
|
||||
target="_blank"
|
||||
title="Faviours"
|
||||
>
|
||||
<tabler:star />
|
||||
</a>
|
||||
</div>
|
||||
<div class="right-side-item">
|
||||
<a href="https://www.bilibili.com/account/history" target="_blank" title="History">
|
||||
<tabler:clock />
|
||||
</a>
|
||||
</div>
|
||||
<div class="right-side-item">
|
||||
<a
|
||||
href="https://member.bilibili.com/platform/home"
|
||||
target="_blank"
|
||||
title="Creative Center"
|
||||
>
|
||||
<tabler:bulb />
|
||||
</a>
|
||||
</div>
|
||||
<div class="right-side-item">
|
||||
<a
|
||||
href="https://member.bilibili.com/platform/upload/video/frame"
|
||||
target="_blank"
|
||||
class="bg-$bew-theme-color rounded-full !text-white !text-base !px-4 mx-1"
|
||||
w="xl:auto <xl:42px"
|
||||
h="xl:auto <xl:42px"
|
||||
p="xl:auto <xl:unset"
|
||||
>
|
||||
<tabler:upload />
|
||||
<span
|
||||
m="l-2"
|
||||
display="xl:block <xl:hidden"
|
||||
>Upload</span>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { getUserID } from '~/utils'
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
mid: getUserID() || '',
|
||||
userInfo: {} as any,
|
||||
showLogoDropDown: false,
|
||||
showUserPanel: false,
|
||||
showTopbarMask: false,
|
||||
isLogin: !!getUserID(),
|
||||
unReadmessage: {},
|
||||
unReadDm: {},
|
||||
unReadmessageCount: 0,
|
||||
newMomentsCount: 0,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initUserPanel()
|
||||
|
||||
document.addEventListener('scroll', () => {
|
||||
if (window.scrollY > 0)
|
||||
this.showTopbarMask = true
|
||||
else
|
||||
this.showTopbarMask = false
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
initUserPanel() {
|
||||
this.getUserInfo()
|
||||
this.getUnreadMessageCount()
|
||||
this.getNewMomentsCount()
|
||||
},
|
||||
showLogoMenuDropdown() {
|
||||
const logo = this.$refs.logo as HTMLElement
|
||||
logo.classList.add('hover')
|
||||
this.showLogoDropDown = true
|
||||
},
|
||||
closeLogoMenuDropdown() {
|
||||
const logo = this.$refs.logo as HTMLElement
|
||||
logo.classList.remove('hover')
|
||||
this.showLogoDropDown = false
|
||||
},
|
||||
openUserPanel() {
|
||||
this.showUserPanel = true
|
||||
const avatarImg = this.$refs.avatarImg as HTMLImageElement
|
||||
const avatarShadow = this.$refs.avatarShadow as HTMLImageElement
|
||||
avatarImg.classList.add('hover')
|
||||
avatarShadow.classList.add('hover')
|
||||
},
|
||||
closeUserPanel() {
|
||||
this.showUserPanel = false
|
||||
const avatarImg = this.$refs.avatarImg as HTMLImageElement
|
||||
const avatarShadow = this.$refs.avatarShadow as HTMLImageElement
|
||||
avatarImg.classList.remove('hover')
|
||||
avatarShadow.classList.remove('hover')
|
||||
},
|
||||
getUserInfo() {
|
||||
browser.runtime
|
||||
.sendMessage({
|
||||
contentScriptQuery: 'getUserInfo',
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.code === 0)
|
||||
this.userInfo = res.data
|
||||
})
|
||||
},
|
||||
async getUnreadMessageCount() {
|
||||
await browser.runtime
|
||||
.sendMessage({
|
||||
contentScriptQuery: 'getUnreadMsg',
|
||||
}).then(res => this.unReadmessage = res.data)
|
||||
|
||||
await browser.runtime
|
||||
.sendMessage({
|
||||
contentScriptQuery: 'getUnreadDm',
|
||||
}).then(res => this.unReadDm = res.data)
|
||||
|
||||
for (const [key, value] of Object.entries(this.unReadmessage))
|
||||
if (key !== 'up') this.unReadmessageCount += parseInt(value)
|
||||
for (const [key, value] of Object.entries(this.unReadDm))
|
||||
this.unReadmessageCount += parseInt(value)
|
||||
|
||||
// this.unReadmessageCount = this.unReadmessage.at + this.unReadmessage.chat + this.unReadmessage.like
|
||||
// + this.unReadmessage.reply + this.unReadmessage.sys_msg
|
||||
},
|
||||
getNewMomentsCount() {
|
||||
browser.runtime
|
||||
.sendMessage({
|
||||
contentScriptQuery: 'getNewMomentsCount',
|
||||
}).then((res) => {
|
||||
this.newMomentsCount = res.data.update_info.item.count
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.slide-enter-active,
|
||||
.slide-leave-active {
|
||||
@apply transition-all duration-300;
|
||||
}
|
||||
|
||||
.slide-leave-to,
|
||||
.slide-enter-from {
|
||||
@apply transform -translate-y-4 opacity-0;
|
||||
}
|
||||
|
||||
.topbar-enter-active,
|
||||
.topbar-leave-active {
|
||||
@apply transition-all duration-600;
|
||||
}
|
||||
|
||||
.topbar-leave-to,
|
||||
.topbar-enter-from {
|
||||
@apply opacity-0;
|
||||
}
|
||||
|
||||
header {
|
||||
@apply flex justify-between px-23 py-2 w-screen items-center;
|
||||
}
|
||||
.left-side {
|
||||
@apply relative;
|
||||
.logo {
|
||||
@apply flex items-center;
|
||||
|
||||
&.hover {
|
||||
svg:nth-child(2) {
|
||||
@apply transform rotate-180;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
@apply w-60px h-auto filter drop-shadow-lg
|
||||
fill-$bew-logo-color;
|
||||
|
||||
&:nth-child(2) {
|
||||
@apply duration-300;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.right-side {
|
||||
@apply flex h-60px items-center rounded-full p-2
|
||||
bg-$bew-content-1;
|
||||
backdrop-filter: var(--bew-filter-glass);
|
||||
box-shadow: var(--bew-shadow-2);
|
||||
|
||||
.unread-message {
|
||||
@apply absolute -top-1 right-0 "!px-1" "!py-2" rounded-full
|
||||
text-xs leading-0 z-1 min-w-16px h-16px
|
||||
flex justify-center items-center
|
||||
bg-$bew-theme-color text-white;
|
||||
box-shadow: 0 2px 4px rgba(var(--tw-shadow-color), 0.4);
|
||||
}
|
||||
|
||||
&-item {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
&-item:not(#avatar) {
|
||||
@apply min-w-40px;
|
||||
a {
|
||||
@apply text-xl flex items-center py-2 px-4;
|
||||
}
|
||||
}
|
||||
|
||||
.login {
|
||||
@apply rounded-full "!text-$bew-theme-color" "!px-4" mx-1 border-1
|
||||
flex items-center "!text-base"
|
||||
border-solid border-$bew-theme-color;
|
||||
}
|
||||
|
||||
#avatar {
|
||||
@apply flex items-center ml-1 pr-2 relative z-1;
|
||||
|
||||
#avatar-img,
|
||||
#avatar-shadow {
|
||||
@apply duration-300;
|
||||
|
||||
&.hover {
|
||||
@apply transform scale-230 translate-y-4;
|
||||
}
|
||||
}
|
||||
|
||||
#avatar-shadow {
|
||||
@apply pointer-events-none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
28
src/composables/useStorageLocal.ts
Normal file
28
src/composables/useStorageLocal.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { storage } from 'webextension-polyfill'
|
||||
import {
|
||||
useStorageAsync,
|
||||
StorageLikeAsync,
|
||||
MaybeRef,
|
||||
StorageAsyncOptions,
|
||||
RemovableRef,
|
||||
} from '@vueuse/core'
|
||||
|
||||
const storageLocal: StorageLikeAsync = {
|
||||
removeItem(key: string) {
|
||||
return storage.local.remove(key)
|
||||
},
|
||||
|
||||
setItem(key: string, value: string) {
|
||||
return storage.local.set({ [key]: value })
|
||||
},
|
||||
|
||||
async getItem(key: string) {
|
||||
return (await storage.local.get(key))[key]
|
||||
},
|
||||
}
|
||||
|
||||
export const useStorageLocal = <T>(
|
||||
key: string,
|
||||
initialValue: MaybeRef<T>,
|
||||
options?: StorageAsyncOptions<T>,
|
||||
): RemovableRef<T> => useStorageAsync(key, initialValue, storageLocal, options)
|
||||
50
src/contentScripts/index.ts
Normal file
50
src/contentScripts/index.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/* eslint-disable no-console */
|
||||
import { onMessage } from 'webext-bridge'
|
||||
import { createApp } from 'vue'
|
||||
import App from './views/App.vue'
|
||||
import { getCookie, setCookie, SVG_ICONS } from '~/utils'
|
||||
|
||||
// Firefox `browser.tabs.executeScript()` requires scripts return a primitive value
|
||||
;(() => {
|
||||
console.info('[vitesse-webext] Hello world from content script')
|
||||
|
||||
// communication example: send previous tab title from background page
|
||||
onMessage('tab-prev', ({ data }) => {
|
||||
console.log(`[vitesse-webext] Navigate from page "${data.title}"`)
|
||||
})
|
||||
|
||||
const currentUrl = document.URL
|
||||
|
||||
if (
|
||||
/https?:\/\/bilibili.com\/?$/.test(currentUrl)
|
||||
|| /https?:\/\/www.bilibili.com\/?$/.test(currentUrl)
|
||||
|| /https?:\/\/bilibili.com\/\?spm_id_from=.*/.test(currentUrl)
|
||||
|| /https?:\/\/www.bilibili.com\/\?spm_id_from=(.)*/.test(currentUrl)
|
||||
) {
|
||||
// if current homepage is old version, redirect to new version
|
||||
if (`${getCookie('i-wanna-go-back')}` === '2') {
|
||||
setCookie('i-wanna-go-back', '-1', 1)
|
||||
location.reload()
|
||||
}
|
||||
else {
|
||||
document.querySelectorAll('script').forEach(script => script.remove())
|
||||
document.body.innerHTML = ''
|
||||
}
|
||||
|
||||
const container = document.createElement('div')
|
||||
const root = document.createElement('div')
|
||||
const styleEl = document.createElement('link')
|
||||
styleEl.setAttribute('rel', 'stylesheet')
|
||||
styleEl.setAttribute('href', browser.runtime.getURL('dist/contentScripts/style.css'))
|
||||
container.id = 'bewly'
|
||||
container.appendChild(styleEl)
|
||||
container.appendChild(root)
|
||||
document.body.appendChild(container)
|
||||
createApp(App).mount(root)
|
||||
|
||||
// inject svg icons
|
||||
const svgDiv = document.createElement('div')
|
||||
svgDiv.innerHTML = SVG_ICONS
|
||||
document.body.appendChild(svgDiv)
|
||||
}
|
||||
})()
|
||||
77
src/contentScripts/views/App.vue
Normal file
77
src/contentScripts/views/App.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<transition>
|
||||
<topbar v-if="isShowTopbar" class="fixed z-50"></topbar>
|
||||
</transition>
|
||||
<!-- is home page -->
|
||||
<home></home>
|
||||
<!-- button -->
|
||||
<div class="fixed bottom-5 right-5" flex="~ col">
|
||||
<button
|
||||
class="transform active:scale-90"
|
||||
w="45px"
|
||||
h="45px"
|
||||
p="3"
|
||||
m="b-3"
|
||||
bg="$bew-content-1"
|
||||
text="2xl $bew-text-1"
|
||||
font="leading-0"
|
||||
duration="300"
|
||||
rounded="$bew-radius"
|
||||
style="box-shadow: var(--bew-shadow-2); backdrop-filter: var(--bew-filter-glass);"
|
||||
@click="toggleDark()"
|
||||
>
|
||||
<tabler:moon-stars v-if="isDark" />
|
||||
<tabler:sun v-else />
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="leading-none transform active:scale-90"
|
||||
w="45px"
|
||||
h="45px"
|
||||
p="3"
|
||||
bg="$bew-content-1"
|
||||
text="2xl $bew-text-1"
|
||||
font="leading-0"
|
||||
duration="300"
|
||||
rounded="$bew-radius"
|
||||
style="box-shadow: var(--bew-shadow-2); backdrop-filter: var(--bew-filter-glass);"
|
||||
@click="toggle()"
|
||||
>
|
||||
<tabler-settings />
|
||||
</button>
|
||||
</div>
|
||||
<settings v-if="showSettings" @close="showSettings = false"></settings>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { accessKey, apperance, isShowTopbar } from '~/logic/storage'
|
||||
import { useToggle, useDark } from '@vueuse/core'
|
||||
import 'virtual:windi.css'
|
||||
import '~/styles/index.ts'
|
||||
import Home from './Home/index.vue'
|
||||
import { getUserID, grantAccessKey } from '~/utils'
|
||||
|
||||
const [showSettings, toggle] = useToggle(false)
|
||||
|
||||
// auto dark mode
|
||||
// const style = document.createElement('style')
|
||||
|
||||
const isDark = useDark()
|
||||
const toggleDark = useToggle(isDark)
|
||||
|
||||
// if (getUserID())
|
||||
// grantAccessKey()
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.v-enter-active,
|
||||
.v-leave-active {
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.v-enter-from,
|
||||
.v-leave-to {
|
||||
@apply opacity-0 transform -translate-y-full;
|
||||
}
|
||||
</style>
|
||||
265
src/contentScripts/views/Home/RecommendContent.vue
Normal file
265
src/contentScripts/views/Home/RecommendContent.vue
Normal file
@@ -0,0 +1,265 @@
|
||||
<template>
|
||||
<div
|
||||
m="x-22 b-0 t-0"
|
||||
grid="~ xl:cols-4 lg:cols-3 md:cols-2 gap-4"
|
||||
>
|
||||
<transition-group
|
||||
name="list"
|
||||
@enter="onEnter"
|
||||
>
|
||||
<div
|
||||
v-for="(video, index) in videoList"
|
||||
:key="video.idx"
|
||||
:data-index="index"
|
||||
class="video-card"
|
||||
>
|
||||
<a :href="'/video/av' + video.uri.split('/')[3]" target="_blank">
|
||||
<div class="thumbnail">
|
||||
<div class="duration">{{ calcCurrentTime(video.duration) }}</div>
|
||||
<div
|
||||
class="overflow-hidden w-full relative rounded-$bew-radius z-1"
|
||||
style="aspect-ratio: 16/9"
|
||||
>
|
||||
<img class="cover" :src="video.cover + '@672w_378h_1c'" loading="lazy" />
|
||||
</div>
|
||||
<img class="cover-shadow" :src="video.cover + '@672w_378h_1c'" loading="lazy" />
|
||||
</div>
|
||||
<div class="detail">
|
||||
<div class="flex">
|
||||
<a class="avatar" @click="gotoChannel(video.mid)">
|
||||
<img
|
||||
:src="(video.face + '').replace('http:', '') + '@60w_60h_1c'"
|
||||
width="48"
|
||||
height="48"
|
||||
loading="lazy"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<h3 class="video-title" :title="video.title">{{ video.title }}</h3>
|
||||
<div class="channel-name" @click="gotoChannel(video.mid)">{{ video.name }}</div>
|
||||
<div class="video-info">
|
||||
{{ numFormatter(video.play) }} views
|
||||
<span class="text-xs font-light">•</span>
|
||||
{{ calcTimeSince(new Date(video.ctime * 1000)) }} ago
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isLoading"
|
||||
w="full"
|
||||
h="46px"
|
||||
p="y-8"
|
||||
flex="~"
|
||||
justify="center"
|
||||
items="center"
|
||||
>
|
||||
<img
|
||||
src="https://s2.loli.net/2022/03/20/4YZhnF1cmya6tHO.gif"
|
||||
alt="loading"
|
||||
w="46px"
|
||||
h="46px"
|
||||
m="r-2"
|
||||
/>
|
||||
Loading more...
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!isLoading"
|
||||
style="height: 100px"
|
||||
w="full"
|
||||
p="y-8"
|
||||
flex="~"
|
||||
items="center"
|
||||
justify="center"
|
||||
>
|
||||
<button
|
||||
style="box-shadow: var(--bew-shadow-2);"
|
||||
bg="$bew-content-1"
|
||||
p="x-8 y-4"
|
||||
text="$bew-text-1"
|
||||
rounded="$bew-radius"
|
||||
flex="~"
|
||||
items="center"
|
||||
@click="onRefresh"
|
||||
>
|
||||
<tabler:refresh class="mr-2" />Click to refresh
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { accessKey } from '~/logic/storage'
|
||||
import { numFormatter, calcTimeSince, calcCurrentTime } from '~/utils'
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
videoList: [] as any[],
|
||||
isLoading: true,
|
||||
numFormatter,
|
||||
calcTimeSince,
|
||||
calcCurrentTime,
|
||||
MAX_LIMIT: 150 as const,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getRecommendVideo()
|
||||
this.getRecommendVideo()
|
||||
window.onscroll = async() => {
|
||||
if ((window.innerHeight + window.scrollY) >= document.body.scrollHeight) {
|
||||
if (!this.isLoading)
|
||||
return
|
||||
|
||||
if (this.videoList.length <= (this.MAX_LIMIT - 20)) {
|
||||
await this.getRecommendVideo(this.videoList[this.videoList.length - 1]?.idx)
|
||||
await this.getRecommendVideo(this.videoList[this.videoList.length - 1]?.idx)
|
||||
}
|
||||
else {
|
||||
this.isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async getRecommendVideo(idx?: number) {
|
||||
const response = await browser.runtime
|
||||
.sendMessage({
|
||||
contentScriptQuery: 'getRecommendVideo',
|
||||
idx,
|
||||
accessKey: accessKey.value,
|
||||
})
|
||||
|
||||
if (response.code === 0) {
|
||||
// when videoList has length property, it means it is the first time to load
|
||||
if (!this.videoList.length) {
|
||||
this.videoList = response.data
|
||||
}
|
||||
else {
|
||||
// else we concat the new data to the old data
|
||||
this.videoList = this.videoList.concat(response.data)
|
||||
}
|
||||
}
|
||||
},
|
||||
gotoChannel(mid: number) {
|
||||
window.open(`//space.bilibili.com/${mid}`)
|
||||
},
|
||||
onRefresh() {
|
||||
this.isLoading = true
|
||||
this.videoList = []
|
||||
},
|
||||
onEnter(el, done) {
|
||||
// el.dataset.index > 10
|
||||
const delay = (el.dataset.index / 10).toFixed(1).split('.')[1]
|
||||
el.style.transitionDelay = `${delay * 0.1}s`
|
||||
done()
|
||||
// gsap.to(el, {
|
||||
// opacity: 1,
|
||||
// height: '1.6em',
|
||||
// delay: el.dataset.index * 0.15,
|
||||
// onComplete: done,
|
||||
// })
|
||||
},
|
||||
},
|
||||
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.list-move,
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
.list-enter-from,
|
||||
.list-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.list-leave-active {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.video-card {
|
||||
@apply p-1 rounded-$bew-radius duration-300
|
||||
z-0
|
||||
active:bg-$bew-fill-2;
|
||||
|
||||
&:hover {
|
||||
@apply z-10;
|
||||
}
|
||||
|
||||
.cover-shadow {
|
||||
@apply absolute top-0 left-0 w-full h-full filter -z-1
|
||||
pointer-events-none duration-600 rounded-$bew-radius
|
||||
opacity-80;
|
||||
aspect-ratio: 16/9;
|
||||
}
|
||||
|
||||
&:hover .cover-shadow {
|
||||
@apply blur-lg transform;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
@apply w-full rounded-$bew-radius relative duration-300;
|
||||
aspect-ratio: 16/9;
|
||||
|
||||
.duration {
|
||||
@apply absolute z-2 bottom-0 right-0 px-2 py-1
|
||||
m-1 rounded-$bew-radius text-xs
|
||||
text-white
|
||||
bg-black bg-opacity-60;
|
||||
}
|
||||
|
||||
.cover {
|
||||
@apply w-full h-full bg-cover bg-center transform scale-110 duration-300
|
||||
absolute bg-$bew-fill-3;
|
||||
aspect-ratio: 16/9;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .thumbnail {
|
||||
@apply transform scale-105;
|
||||
}
|
||||
|
||||
&:hover .cover {
|
||||
@apply transform scale-100;
|
||||
}
|
||||
|
||||
.detail {
|
||||
@apply flex mt-4;
|
||||
|
||||
.avatar {
|
||||
@apply mr-4 h-48px rounded-$bew-radius overflow-hidden
|
||||
object-center object-cover
|
||||
bg-$bew-fill-3;
|
||||
}
|
||||
|
||||
.meta {
|
||||
@apply flex flex-col items-start;
|
||||
|
||||
.video-title {
|
||||
@apply text-lg max-h-13 overflow-hidden overflow-ellipsis whitespace-normal
|
||||
text-$bew-text-1;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.channel-name,
|
||||
.video-info {
|
||||
@apply text-base text-$bew-text-2;
|
||||
}
|
||||
|
||||
.channel-name {
|
||||
@apply mt-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
18
src/contentScripts/views/Home/index.vue
Normal file
18
src/contentScripts/views/Home/index.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<div bg="$bew-bg">
|
||||
<div class="banner flex justify-center items-center flex-col"></div>
|
||||
<recommend-content></recommend-content>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import RecommendContent from './RecommendContent.vue'
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.banner {
|
||||
height: 83px;
|
||||
max-height: unset;
|
||||
transition: 0.3s;
|
||||
}
|
||||
</style>
|
||||
14
src/env.ts
Normal file
14
src/env.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
const forbiddenProtocols = [
|
||||
'chrome-extension://',
|
||||
'chrome-search://',
|
||||
'chrome://',
|
||||
'devtools://',
|
||||
'edge://',
|
||||
'https://chrome.google.com/webstore',
|
||||
]
|
||||
|
||||
export function isForbiddenUrl(url: string): boolean {
|
||||
return forbiddenProtocols.some(protocol => url.startsWith(protocol))
|
||||
}
|
||||
|
||||
export const isFirefox = navigator.userAgent.includes('Firefox')
|
||||
6
src/global.d.ts
vendored
Normal file
6
src/global.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
declare const __DEV__: boolean
|
||||
|
||||
declare module '*.vue' {
|
||||
const component: any
|
||||
export default component
|
||||
}
|
||||
1
src/logic/index.ts
Normal file
1
src/logic/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './storage'
|
||||
6
src/logic/storage.ts
Normal file
6
src/logic/storage.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { useStorageLocal } from '~/composables/useStorageLocal'
|
||||
|
||||
export const storageDemo = useStorageLocal('webext-demo', 'Storage Demo', { listenToStorageChanges: true })
|
||||
export const isShowTopbar = useStorageLocal('isShowTopbar', true, { listenToStorageChanges: true })
|
||||
export const apperance = useStorageLocal('apperance', 'automatic', { listenToStorageChanges: true })
|
||||
export const accessKey = useStorageLocal('accessKey', null, { listenToStorageChanges: true })
|
||||
56
src/manifest.ts
Normal file
56
src/manifest.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import fs from 'fs-extra'
|
||||
import type { Manifest } from 'webextension-polyfill'
|
||||
import type PkgType from '../package.json'
|
||||
import { isDev, port, r } from '../scripts/utils'
|
||||
|
||||
export async function getManifest() {
|
||||
const pkg = (await fs.readJSON(r('package.json'))) as typeof PkgType
|
||||
|
||||
// update this file to update this manifest.json
|
||||
// can also be conditional based on your need
|
||||
const manifest: Manifest.WebExtensionManifest = {
|
||||
manifest_version: 2,
|
||||
name: pkg.displayName || pkg.name,
|
||||
version: pkg.version,
|
||||
description: pkg.description,
|
||||
// browser_action: {
|
||||
// default_icon: './assets/icon-512.png',
|
||||
// default_popup: './dist/popup/index.html',
|
||||
// },
|
||||
// options_ui: {
|
||||
// page: './dist/options/index.html',
|
||||
// open_in_tab: true,
|
||||
// chrome_style: false,
|
||||
// },
|
||||
background: {
|
||||
page: './dist/background/index.html',
|
||||
persistent: false,
|
||||
},
|
||||
icons: {
|
||||
16: './assets/icon-512.png',
|
||||
48: './assets/icon-512.png',
|
||||
128: './assets/icon-512.png',
|
||||
},
|
||||
permissions: ['tabs', 'storage', 'activeTab', 'https://app.bilibili.com/*', 'http://*/', 'https://*/'],
|
||||
content_scripts: [
|
||||
{
|
||||
matches: ['http://www.bilibili.com/*', 'https://www.bilibili.com/*'],
|
||||
js: ['./dist/contentScripts/index.global.js'],
|
||||
},
|
||||
],
|
||||
web_accessible_resources: ['dist/contentScripts/style.css'],
|
||||
}
|
||||
|
||||
if (isDev) {
|
||||
// for content script, as browsers will cache them for each reload,
|
||||
// we use a background script to always inject the latest version
|
||||
// see src/background/contentScriptHMR.ts
|
||||
delete manifest.content_scripts
|
||||
manifest.permissions?.push('webNavigation')
|
||||
|
||||
// this is required on dev for Vite script to load
|
||||
manifest.content_security_policy = `script-src \'self\' http://localhost:${port}; object-src \'self\'`
|
||||
}
|
||||
|
||||
return manifest
|
||||
}
|
||||
23
src/options/Options.vue
Normal file
23
src/options/Options.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<main class="px-4 py-10 text-center text-gray-700 dark:text-gray-200">
|
||||
<pixelarticons-sliders class="icon-btn mx-2 text-2xl" />
|
||||
<div>Options</div>
|
||||
<p class="mt-2 opacity-50">
|
||||
This is the options page
|
||||
</p>
|
||||
|
||||
<input v-model="storageDemo" class="border border-gray-400 rounded px-2 py-1 mt-2" />
|
||||
|
||||
<div class="mt-4">
|
||||
Powered by Vite <pixelarticons-zap class="align-middle" />
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { storageDemo } from '~/logic/storage'
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
12
src/options/index.html
Normal file
12
src/options/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<base target="_blank">
|
||||
<title>Options</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
6
src/options/main.ts
Normal file
6
src/options/main.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './Options.vue'
|
||||
import '../styles'
|
||||
|
||||
const app = createApp(App)
|
||||
app.mount('#app')
|
||||
50
src/popup/Popup.vue
Normal file
50
src/popup/Popup.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<main class="w-[300px] px-4 py-5 text-center text-gray-700">
|
||||
<Logo />
|
||||
<div>BewlyBewly</div>
|
||||
<p class="mt-2 opacity-50">
|
||||
This is the popup page
|
||||
</p>
|
||||
<div>
|
||||
Disable topbar (Compatible with bilibili evolved)
|
||||
<input v-model="isShowTopbar" type="checkbox" />
|
||||
{{ isShowTopbar }}
|
||||
</div>
|
||||
<button class="btn mt-2" @click="openOptionsPage">
|
||||
Open Options
|
||||
</button>
|
||||
<div class="mt-2">
|
||||
<span class="opacity-50">Storage:</span>
|
||||
{{ storageDemo }}
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { storageDemo, isShowTopbar } from '~/logic/storage'
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
isShowTopbar,
|
||||
storageDemo,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openOptionsPage() {
|
||||
browser.runtime.openOptionsPage()
|
||||
},
|
||||
},
|
||||
})
|
||||
// const isShowTopbar = false
|
||||
// console.log(isShowTopbar)
|
||||
|
||||
// function openOptionsPage() {
|
||||
// browser.runtime.openOptionsPage()
|
||||
// }
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
main {
|
||||
font-size: 12px!important;
|
||||
}
|
||||
</style>
|
||||
12
src/popup/index.html
Normal file
12
src/popup/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<base target="_blank">
|
||||
<title>Popup</title>
|
||||
</head>
|
||||
<body style="min-width: 100px">
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
6
src/popup/main.ts
Normal file
6
src/popup/main.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './Popup.vue'
|
||||
import '../styles'
|
||||
|
||||
const app = createApp(App)
|
||||
app.mount('#app')
|
||||
3
src/styles/index.ts
Normal file
3
src/styles/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import './reset.scss'
|
||||
import './main.scss'
|
||||
import 'virtual:windi.css'
|
||||
95
src/styles/main.scss
Executable file
95
src/styles/main.scss
Executable file
@@ -0,0 +1,95 @@
|
||||
:root {
|
||||
--bew-radius: 12px;
|
||||
|
||||
--bew-filter-glass: blur(20px) saturate(180%);
|
||||
|
||||
--bew-shadow-1: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
--bew-shadow-2: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
--bew-shadow-3: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
||||
--bew-shadow-4: 0 25px 50px -12px rgb(0 0 0 / 0.25);
|
||||
|
||||
--bew-theme-color: rgb(0, 161, 214);
|
||||
|
||||
--bew-logo-color: var(--bew-theme-color);
|
||||
|
||||
--bew-text-1: hsl(217, 33%, 17%);
|
||||
--bew-text-2: hsl(215, 19%, 35%);
|
||||
--bew-text-3: hsl(215, 19%, 55%);
|
||||
|
||||
--bew-bg: rgb(243 244 246);
|
||||
--bew-content-1: hsl(0 0% 100% / 60%);
|
||||
--bew-content-solid-1: hsl(0 0% 100%);
|
||||
|
||||
--bew-fill-1: rgb(120 120 128 / 12%);
|
||||
--bew-fill-2: rgb(120 120 128 / 22%);
|
||||
--bew-fill-3: rgb(120 120 128 / 32%);
|
||||
--bew-fill-4: rgb(120 120 128 / 42%);
|
||||
}
|
||||
|
||||
:root.dark {
|
||||
// dark mode
|
||||
--bew-logo-color: var(--bew-text-1);
|
||||
|
||||
--bew-text-1: hsl(215, 19%, 98%);
|
||||
--bew-text-2: hsl(215, 19%, 70%);
|
||||
--bew-text-3: hsl(215, 19%, 50%);
|
||||
|
||||
--bew-bg: hsl(230 12% 6%);
|
||||
--bew-content-1: hsl(230 12% 13% / 60%);
|
||||
--bew-content-solid-1: hsl(230 12% 13%);
|
||||
|
||||
--bew-fill-1: rgb(120 120 128 / 16%);
|
||||
--bew-fill-2: rgb(120 120 128 / 26%);
|
||||
--bew-fill-3: rgb(120 120 128 / 36%);
|
||||
--bew-fill-4: rgb(120 120 128 / 46%);
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bew-bg);
|
||||
color: var(--bew-text-1);
|
||||
}
|
||||
|
||||
.btn {
|
||||
@apply px-4 py-2 rounded-$bew-radius inline-block
|
||||
cursor-pointer transform duration-300
|
||||
filter outline-none
|
||||
border-2 border-solid border-$bew-theme-color
|
||||
bg-$bew-theme-color text-white
|
||||
active:scale-95 active:bg-$bew-theme-color active:brightness-110
|
||||
focus:bg-$bew-theme-color
|
||||
disabled:cursor-default disabled:bg-$bew-fill-4 disabled:opacity-50
|
||||
disabled:border-$bew-fill-4;
|
||||
}
|
||||
|
||||
.line-btn {
|
||||
@apply px-4 py-2 rounded-$bew-radius inline-block
|
||||
cursor-pointer transform duration-300
|
||||
filter outline-none
|
||||
border-2 border-solid border-$bew-fill-4
|
||||
text-$bew-fill-4
|
||||
active:scale-95 active:bg-$bew-theme-color active:brightness-110
|
||||
focus:bg-$bew-theme-color
|
||||
disabled:cursor-default disabled:bg-$bew-fill-4 disabled:opacity-50;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
@apply inline-block cursor-pointer select-none
|
||||
opacity-75 transition duration-200 ease-in-out
|
||||
hover:opacity-100 hover:text-$bew-theme-color;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.chk-btn {
|
||||
@apply flex px-4 py-2 items-center select-none;
|
||||
|
||||
input {
|
||||
@apply ml-4;
|
||||
}
|
||||
}
|
||||
23
src/styles/reset.scss
Normal file
23
src/styles/reset.scss
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* this stylesheet is used to reset the styles of the bilibili default style
|
||||
*/
|
||||
|
||||
html, body {
|
||||
font-size: 14px;
|
||||
min-width: unset;
|
||||
}
|
||||
|
||||
a,
|
||||
a:hover {
|
||||
color: unset;
|
||||
}
|
||||
|
||||
button {
|
||||
border: unset;
|
||||
background: unset;
|
||||
|
||||
&:focus,
|
||||
&:active {
|
||||
background: currentColor
|
||||
}
|
||||
}
|
||||
85
src/utils/auth-provider.ts
Normal file
85
src/utils/auth-provider.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/* eslint-disable no-throw-literal */
|
||||
import { accessKey } from '~/logic/storage'
|
||||
|
||||
/**
|
||||
* 感謝這份專案給出的獲取accessKey的方法
|
||||
* https://github.com/indefined/UserScripts/blob/42e20281d2e4d7bce16b5c8033b67ccb6ad312e9/bilibiliHome/bilibiliHome.user.js#L1149
|
||||
*/
|
||||
|
||||
export const revokeAccessKey = () => {
|
||||
accessKey.value = null
|
||||
}
|
||||
|
||||
export const grantAccessKey = (element?: HTMLButtonElement): void => {
|
||||
if (element)
|
||||
setBtnState(element, 'loading')
|
||||
|
||||
const tip = 'Failed to grant Access Key'
|
||||
|
||||
fetch(
|
||||
'https://passport.bilibili.com/login/app/third?appkey=27eb53fc9058f8c3'
|
||||
+ '&api=https%3A%2F%2Fwww.mcbbs.net%2Ftemplate%2Fmcbbs%2Fimage%2Fspecial_photo_bg.png&sign=04224646d1fea004e79606d3b038c84a',
|
||||
{
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
},
|
||||
)
|
||||
.then(res => res.json())
|
||||
.then((data) => {
|
||||
if (data.code || !data.data) throw { tip, msg: data.msg || data.message || data.code, data }
|
||||
else if (!data.data.has_login) throw { tip, msg: 'Please login to bilibili first', data }
|
||||
else if (!data.data.confirm_uri) throw { tip, msg: 'Unable to receive verified URL. Please go back and try againe.', data }
|
||||
else return data.data.confirm_uri
|
||||
})
|
||||
.then(
|
||||
url =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const iframe = document.createElement('iframe')
|
||||
iframe.src = url
|
||||
iframe.style.display = 'none'
|
||||
document.body.appendChild(iframe)
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
document.body.contains(iframe) && document.body.removeChild(iframe)
|
||||
reject(new Error(`${tip}: Request timeout`))
|
||||
}, 5000)
|
||||
|
||||
window.addEventListener('message', (ev) => {
|
||||
if (`${ev.origin}` !== 'https://www.mcbbs.net' || !ev.data) return
|
||||
const key = ev.data.match(/access_key=([0-9a-z]{32})/)
|
||||
if (key) {
|
||||
accessKey.value = key[1]
|
||||
clearTimeout(timeout)
|
||||
document.body.contains(iframe) && document.body.removeChild(iframe)
|
||||
resolve()
|
||||
}
|
||||
else {
|
||||
// eslint-disable-next-line prefer-promise-reject-errors
|
||||
reject({ tip, msg: 'Failed to get Access Key', data: ev })
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
.catch((error) => {
|
||||
// eslint-disable-next-line no-alert
|
||||
alert(`${error.tip}: ${error.msg}`)
|
||||
console.error(`${error.msg}: `, error)
|
||||
}).then(() => {
|
||||
if (element)
|
||||
setBtnState(element, 'default')
|
||||
})
|
||||
|
||||
function setBtnState(element: HTMLButtonElement, state: string) {
|
||||
const orginalInnerHTML = element.innerHTML
|
||||
if (state === 'loading') {
|
||||
element.innerHTML = `
|
||||
<span class="animate-pulse">Loading...</span>
|
||||
`
|
||||
element.style.pointerEvents = 'none'
|
||||
}
|
||||
else if (state === 'default') {
|
||||
element.innerHTML = orginalInnerHTML
|
||||
element.style.pointerEvents = 'auto'
|
||||
}
|
||||
}
|
||||
}
|
||||
52
src/utils/dataFormatter.ts
Normal file
52
src/utils/dataFormatter.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
export const numFormatter = (num: number) => {
|
||||
const digits = 1 // specify number of digits after decimal
|
||||
const lookup = [
|
||||
{ value: 1, symbol: '' },
|
||||
{ value: 1e3, symbol: 'K' },
|
||||
{ value: 1e6, symbol: 'M' },
|
||||
{ value: 1e9, symbol: 'G' },
|
||||
{ value: 1e12, symbol: 'T' },
|
||||
{ value: 1e15, symbol: 'P' },
|
||||
{ value: 1e18, symbol: 'E' },
|
||||
]
|
||||
const rx = /\.0+$|(\.[0-9]*[1-9])0+$/
|
||||
const item = lookup.slice().reverse().find((item) => {
|
||||
return num >= item.value
|
||||
})
|
||||
return item ? (num / item.value).toFixed(digits).replace(rx, '$1') + item.symbol : '0'
|
||||
}
|
||||
|
||||
export const calcTimeSince = (date: any) => {
|
||||
const seconds = Math.floor(((new Date() as any) - date) / 1000)
|
||||
let interval = seconds / 31536000
|
||||
if (interval > 1)
|
||||
return `${Math.floor(interval) > 1 ? `${Math.floor(interval)} years` : `${Math.floor(interval)} year`}`
|
||||
interval = seconds / 2592000
|
||||
if (interval > 1)
|
||||
return `${Math.floor(interval) > 1 ? `${Math.floor(interval)} months` : `${Math.floor(interval)} month`}`
|
||||
interval = seconds / 604800
|
||||
if (interval > 1)
|
||||
return `${Math.floor(interval) > 1 ? `${Math.floor(interval)} weeks` : `${Math.floor(interval)} week`}`
|
||||
interval = seconds / 86400
|
||||
if (interval > 1)
|
||||
return `${Math.floor(interval) > 1 ? `${Math.floor(interval)} days` : `${Math.floor(interval)} day`}`
|
||||
interval = seconds / 3600
|
||||
if (interval > 1)
|
||||
return `${Math.floor(interval) > 1 ? `${Math.floor(interval)} hours` : `${Math.floor(interval)} hour`}`
|
||||
interval = seconds / 60
|
||||
if (interval > 1)
|
||||
return `${Math.floor(interval) > 1 ? `${Math.floor(interval)} minutes` : `${Math.floor(interval)} minute`}`
|
||||
return `${Math.floor(seconds) > 1 ? `${Math.floor(seconds)} seconds` : `${Math.floor(seconds)}second`}`
|
||||
}
|
||||
|
||||
export const calcCurrentTime = (totalSeconds: number) => {
|
||||
const hours = Math.floor(totalSeconds / 3600)
|
||||
totalSeconds %= 3600
|
||||
const minutes = Math.floor(totalSeconds / 60)
|
||||
const seconds = totalSeconds % 60
|
||||
|
||||
if (hours <= 0)
|
||||
return `${minutes < 10 ? `0${minutes}` : minutes}:${seconds < 10 ? `0${seconds}` : seconds}`
|
||||
|
||||
return `${hours < 10 ? `0${hours}` : hours}:${minutes < 10 ? `0${minutes}` : minutes}:${seconds < 10 ? `0${seconds}` : seconds}`
|
||||
}
|
||||
43
src/utils/index.ts
Normal file
43
src/utils/index.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { grantAccessKey, revokeAccessKey } from './auth-provider'
|
||||
import { SVG_ICONS } from './svgIcons'
|
||||
|
||||
export {
|
||||
grantAccessKey,
|
||||
revokeAccessKey,
|
||||
SVG_ICONS,
|
||||
}
|
||||
export * from './dataFormatter'
|
||||
|
||||
/**
|
||||
* get cookie by name
|
||||
* @param name cookie name
|
||||
* @returns cookie value
|
||||
*/
|
||||
export const getCookie = (name: string) => {
|
||||
const value = `; ${document.cookie}`
|
||||
const parts: Array<string> = value.split(`; ${name}=`)
|
||||
if (parts.length === 2) return parts?.pop()?.split(';').shift()
|
||||
}
|
||||
|
||||
/**
|
||||
* set cookie
|
||||
* @param name cookie name
|
||||
* @param value cookie value
|
||||
*/
|
||||
export const setCookie = (name: string, value: any, expDays: number) => {
|
||||
const date = new Date()
|
||||
date.setTime(date.getTime() + (expDays * 24 * 60 * 60 * 1000))
|
||||
const expires = `expires=${date.toUTCString()}`
|
||||
document.cookie = `${name}=${value}; ${expires}; domain=.bilibili.com; path=/`
|
||||
}
|
||||
|
||||
/**
|
||||
* get current login user id
|
||||
* @returns userId
|
||||
*/
|
||||
export const getUserID = () => getCookie('DedeUserID')
|
||||
|
||||
/**
|
||||
* get csrf token
|
||||
*/
|
||||
export const getCSRF = () => getCookie('bili_jct')
|
||||
1
src/utils/svgIcons.ts
Normal file
1
src/utils/svgIcons.ts
Normal file
File diff suppressed because one or more lines are too long
24
tsconfig.json
Normal file
24
tsconfig.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"module": "ESNext",
|
||||
"target": "es2016",
|
||||
"lib": ["DOM", "ESNext"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"incremental": false,
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"noUnusedLocals": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"types": [
|
||||
"vite/client",
|
||||
"chrome",
|
||||
],
|
||||
"paths": {
|
||||
"~/*": ["src/*"],
|
||||
}
|
||||
},
|
||||
"exclude": ["dist", "node_modules"]
|
||||
}
|
||||
43
vite.config.content.ts
Normal file
43
vite.config.content.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import WindiCSS from 'vite-plugin-windicss'
|
||||
import { sharedConfig } from './vite.config'
|
||||
import { r, isDev } from './scripts/utils'
|
||||
import windiConfig from './windi.config'
|
||||
import packageJson from './package.json'
|
||||
|
||||
// bundling the content script using Vite
|
||||
export default defineConfig({
|
||||
...sharedConfig,
|
||||
build: {
|
||||
watch: isDev
|
||||
? {}
|
||||
: undefined,
|
||||
outDir: r('extension/dist/contentScripts'),
|
||||
cssCodeSplit: false,
|
||||
emptyOutDir: false,
|
||||
sourcemap: isDev ? 'inline' : false,
|
||||
lib: {
|
||||
entry: r('src/contentScripts/index.ts'),
|
||||
name: packageJson.name,
|
||||
formats: ['iife'],
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: 'index.global.js',
|
||||
extend: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
...sharedConfig.plugins!,
|
||||
|
||||
// https://github.com/antfu/vite-plugin-windicss
|
||||
WindiCSS({
|
||||
config: {
|
||||
...windiConfig,
|
||||
// disable preflight to avoid css population
|
||||
preflight: false,
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
108
vite.config.ts
Normal file
108
vite.config.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { dirname, relative } from 'path'
|
||||
import { defineConfig, UserConfig } from 'vite'
|
||||
import Vue from '@vitejs/plugin-vue'
|
||||
import Icons from 'unplugin-icons/vite'
|
||||
import IconsResolver from 'unplugin-icons/resolver'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import WindiCSS from 'vite-plugin-windicss'
|
||||
import windiConfig from './windi.config'
|
||||
import { r, port, isDev } from './scripts/utils'
|
||||
|
||||
export const sharedConfig: UserConfig = {
|
||||
root: r('src'),
|
||||
resolve: {
|
||||
alias: {
|
||||
'~/': `${r('src')}/`,
|
||||
},
|
||||
},
|
||||
define: {
|
||||
__DEV__: isDev,
|
||||
},
|
||||
plugins: [
|
||||
Vue(),
|
||||
|
||||
AutoImport({
|
||||
imports: [
|
||||
'vue',
|
||||
{
|
||||
'webextension-polyfill': [
|
||||
['*', 'browser'],
|
||||
],
|
||||
},
|
||||
],
|
||||
dts: r('src/auto-imports.d.ts'),
|
||||
}),
|
||||
|
||||
// https://github.com/antfu/unplugin-vue-components
|
||||
Components({
|
||||
dirs: [r('src/components')],
|
||||
// generate `components.d.ts` for ts support with Volar
|
||||
dts: true,
|
||||
resolvers: [
|
||||
// auto import icons
|
||||
IconsResolver({
|
||||
componentPrefix: '',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
|
||||
// https://github.com/antfu/unplugin-icons
|
||||
Icons(),
|
||||
|
||||
// rewrite assets to use relative path
|
||||
{
|
||||
name: 'assets-rewrite',
|
||||
enforce: 'post',
|
||||
apply: 'build',
|
||||
transformIndexHtml(html, { path }) {
|
||||
return html.replace(/"\/assets\//g, `"${relative(dirname(path), '/assets')}/`)
|
||||
},
|
||||
},
|
||||
],
|
||||
optimizeDeps: {
|
||||
include: [
|
||||
'vue',
|
||||
'@vueuse/core',
|
||||
'webextension-polyfill',
|
||||
],
|
||||
exclude: [
|
||||
'vue-demi',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export default defineConfig(({ command }) => ({
|
||||
...sharedConfig,
|
||||
base: command === 'serve' ? `http://localhost:${port}/` : '/dist/',
|
||||
server: {
|
||||
port,
|
||||
hmr: {
|
||||
host: 'localhost',
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: r('extension/dist'),
|
||||
emptyOutDir: false,
|
||||
sourcemap: isDev ? 'inline' : false,
|
||||
// https://developer.chrome.com/docs/webstore/program_policies/#:~:text=Code%20Readability%20Requirements
|
||||
terserOptions: {
|
||||
mangle: false,
|
||||
},
|
||||
rollupOptions: {
|
||||
input: {
|
||||
background: r('src/background/index.html'),
|
||||
options: r('src/options/index.html'),
|
||||
popup: r('src/popup/index.html'),
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
...sharedConfig.plugins!,
|
||||
|
||||
// https://github.com/antfu/vite-plugin-windicss
|
||||
WindiCSS({
|
||||
config: windiConfig,
|
||||
}),
|
||||
],
|
||||
}))
|
||||
11
windi.config.ts
Normal file
11
windi.config.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { resolve } from 'path'
|
||||
import { defineConfig } from 'windicss/helpers'
|
||||
|
||||
export default defineConfig({
|
||||
darkMode: 'class',
|
||||
// https://windicss.org/posts/v30.html#attributify-mode
|
||||
attributify: true,
|
||||
extract: {
|
||||
include: [resolve(__dirname, 'src/**/*.{vue,html}')],
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user