You've already forked bilibili-subtitle
Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
da7357c7eb | ||
![]() |
1ff47ee148 | ||
![]() |
1bf8188980 | ||
![]() |
3a9a8d9d56 | ||
![]() |
78b5d7a18b | ||
![]() |
ff6dae7a21 | ||
![]() |
3adb541e99 | ||
![]() |
1ef7537251 | ||
![]() |
be6b94164d | ||
![]() |
1f1d48b56a | ||
![]() |
02b7a09f42 | ||
![]() |
9320928b34 | ||
![]() |
1fbdeaa8f6 |
1
.cursorrules
Normal file
1
.cursorrules
Normal file
@@ -0,0 +1 @@
|
||||
This chrome extension project use typescript, react, tailwindcss, daisyui.
|
@@ -1 +1 @@
|
||||
VITE_ENV=web-dev
|
||||
VITE_ENV=web-dev
|
@@ -1,3 +1,3 @@
|
||||
NODE_ENV=production
|
||||
|
||||
VITE_ENV=chrome
|
||||
VITE_ENV=chrome
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "哔哩哔哩字幕列表",
|
||||
"description": "显示B站视频的字幕列表,可点击跳转与下载字幕,并支持翻译和总结字幕!",
|
||||
"version": "1.10.0",
|
||||
"version": "1.10.6",
|
||||
"manifest_version": 3,
|
||||
"permissions": [
|
||||
"storage"
|
||||
|
@@ -1,15 +1,14 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "bilibili-subtitle",
|
||||
"version": "1.10.0",
|
||||
"version": "1.10.6",
|
||||
"type": "module",
|
||||
"description": "哔哩哔哩字幕列表",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build -m production_chrome",
|
||||
"build_chrome": "pnpm run build && node fixChrome.cjs",
|
||||
"build_firefox": "pnpm run build && node fixFirefox.cjs",
|
||||
"build_chrome": "tsc && vite build -m production_chrome && node fixChrome.cjs",
|
||||
"build_firefox": "tsc && vite build -m production_chrome && node fixFirefox.cjs",
|
||||
"fix": "eslint --fix --quiet ."
|
||||
},
|
||||
"author": "IndieKKY",
|
||||
|
5407
pnpm-lock.yaml
generated
5407
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -38,6 +38,7 @@ import {openUrl} from '@kky002/kky-util'
|
||||
import useKeyService from '../hooks/useKeyService'
|
||||
import Ask from './Ask'
|
||||
import {v4} from 'uuid'
|
||||
import RateExtension from '../components/RateExtension'
|
||||
|
||||
const Body = () => {
|
||||
const dispatch = useAppDispatch()
|
||||
@@ -368,8 +369,8 @@ const Body = () => {
|
||||
{/* </div> */}
|
||||
{/* </div> */}
|
||||
</div>
|
||||
<div className='p-2'><RateExtension/></div>
|
||||
</div>
|
||||
|
||||
{/* recommend */}
|
||||
{/* <div className='p-0.5' style={{ */}
|
||||
{/* height: `${RECOMMEND_HEIGHT}px` */}
|
||||
|
@@ -75,26 +75,27 @@ const MoreBtn = (props: Props) => {
|
||||
return
|
||||
}
|
||||
|
||||
let s, fileName
|
||||
let fileName = title
|
||||
let s, suffix
|
||||
if (!downloadType || downloadType === 'text') {
|
||||
s = `${title??'无标题'}\n${url??'无链接'}\n\n`
|
||||
for (const item of data.body) {
|
||||
s += item.content + '\n'
|
||||
}
|
||||
fileName = 'download.txt'
|
||||
suffix = 'txt'
|
||||
} else if (downloadType === 'textWithTime') {
|
||||
s = `${title??'无标题'}\n${url??'无链接'}\n\n`
|
||||
for (const item of data.body) {
|
||||
s += formatTime(item.from) + ' ' + item.content + '\n'
|
||||
}
|
||||
fileName = 'download.txt'
|
||||
suffix = 'txt'
|
||||
} else if (downloadType === 'article') {
|
||||
s = `${title??'无标题'}\n${url??'无链接'}\n\n`
|
||||
for (const item of data.body) {
|
||||
s += item.content + ', '
|
||||
}
|
||||
s = s.substring(0, s.length - 1) // remove last ','
|
||||
fileName = 'download.txt'
|
||||
suffix = 'txt'
|
||||
} else if (downloadType === 'srt') {
|
||||
/**
|
||||
* 1
|
||||
@@ -113,7 +114,7 @@ const MoreBtn = (props: Props) => {
|
||||
s += ss
|
||||
}
|
||||
s = s.substring(0, s.length - 1)// remove last '\n'
|
||||
fileName = 'download.srt'
|
||||
suffix = 'srt'
|
||||
} else if (downloadType === 'vtt') {
|
||||
/**
|
||||
* WEBVTT title
|
||||
@@ -134,21 +135,22 @@ const MoreBtn = (props: Props) => {
|
||||
s += ss
|
||||
}
|
||||
s = s.substring(0, s.length - 1)// remove last '\n'
|
||||
fileName = 'download.vtt'
|
||||
suffix = 'vtt'
|
||||
} else if (downloadType === 'json') {
|
||||
s = JSON.stringify(data)
|
||||
fileName = 'download.json'
|
||||
suffix = 'json'
|
||||
} else if (downloadType === 'summarize') {
|
||||
s = `${title??'无标题'}\n${url??'无链接'}\n\n`
|
||||
const [success, content] = getSummarize(title, segments, curSummaryType)
|
||||
if (!success) return
|
||||
s += content
|
||||
fileName = '总结.txt'
|
||||
fileName += ' - 总结'
|
||||
suffix = 'txt'
|
||||
} else {
|
||||
return
|
||||
}
|
||||
if (download) {
|
||||
downloadText(s, fileName)
|
||||
downloadText(s, fileName+'.'+suffix)
|
||||
} else {
|
||||
navigator.clipboard.writeText(s).then(() => {
|
||||
toast.success('复制成功')
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import React, {PropsWithChildren, useCallback, useMemo, useState} from 'react'
|
||||
import {setEnvData, setPage} from '../redux/envReducer'
|
||||
import {setEnvData, setPage, setTempData} from '../redux/envReducer'
|
||||
import {useAppDispatch, useAppSelector} from '../hooks/redux'
|
||||
import {
|
||||
ASK_ENABLED_DEFAULT,
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
LANGUAGES,
|
||||
MODEL_DEFAULT,
|
||||
MODEL_MAP,
|
||||
MODEL_TIP,
|
||||
MODELS,
|
||||
PAGE_MAIN,
|
||||
PROMPT_DEFAULTS,
|
||||
@@ -257,13 +258,18 @@ const Settings = () => {
|
||||
{MODELS.map(model => <option key={model.code} value={model.code}>{model.name}</option>)}
|
||||
</select>
|
||||
</FormItem>
|
||||
<div className='desc text-xs'>
|
||||
{MODEL_TIP}
|
||||
</div>
|
||||
{modelValue === 'custom' && <FormItem title='模型名' htmlFor='customModel'>
|
||||
<input id='customModel' type='text' className='input input-sm input-bordered w-full' placeholder='llama2'
|
||||
value={customModelValue} onChange={onChangeCustomModelValue}/>
|
||||
</FormItem>}
|
||||
{modelValue === 'custom' && <FormItem title='Token上限' htmlFor='customModelTokens'>
|
||||
<input id='customModelTokens' type='number' className='input input-sm input-bordered w-full' placeholder={''+CUSTOM_MODEL_TOKENS}
|
||||
value={customModelTokensValue} onChange={e => setCustomModelTokensValue(e.target.value?parseInt(e.target.value):undefined)}/>
|
||||
<input id='customModelTokens' type='number' className='input input-sm input-bordered w-full'
|
||||
placeholder={'' + CUSTOM_MODEL_TOKENS}
|
||||
value={customModelTokensValue}
|
||||
onChange={e => setCustomModelTokensValue(e.target.value ? parseInt(e.target.value) : undefined)}/>
|
||||
</FormItem>}
|
||||
</Section>}
|
||||
|
||||
@@ -274,10 +280,12 @@ const Settings = () => {
|
||||
</FormItem>
|
||||
<div>
|
||||
<div className='desc text-xs'>
|
||||
<div>官方网址:<a className='link link-primary' href='https://makersuite.google.com/app/apikey' target='_blank'
|
||||
rel="noreferrer">Google AI Studio</a> (目前免费)
|
||||
<div>官方网址:<a className='link link-primary' href='https://makersuite.google.com/app/apikey'
|
||||
target='_blank'
|
||||
rel="noreferrer">Google AI Studio</a> (目前免费)
|
||||
</div>
|
||||
<div className='text-xs text-error flex items-center'><IoWarning className='text-sm text-warning'/>谷歌模型安全要求比较高,有些视频可能无法生成总结!
|
||||
</div>
|
||||
<div className='text-xs text-error flex items-center'><IoWarning className='text-sm text-warning'/>谷歌模型安全要求比较高,有些视频可能无法生成总结!</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>}
|
||||
@@ -400,6 +408,12 @@ const Settings = () => {
|
||||
<div className='flex justify-center gap-5'>
|
||||
<button className='btn btn-primary btn-sm' onClick={onSave}>保存</button>
|
||||
<button className='btn btn-sm' onClick={onCancel}>取消</button>
|
||||
{/* <button className='btn btn-sm' onClick={() => {
|
||||
dispatch(setTempData({
|
||||
reviewed: undefined,
|
||||
// reviewActions: 0
|
||||
}))
|
||||
}}>重置</button> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
58
src/components/RateExtension.tsx
Normal file
58
src/components/RateExtension.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FaStar } from 'react-icons/fa';
|
||||
import { IoMdClose } from 'react-icons/io';
|
||||
import { setTempData } from '../redux/envReducer';
|
||||
import { useAppDispatch, useAppSelector } from '../hooks/redux';
|
||||
import { openUrl } from '@kky002/kky-util';
|
||||
import { isEdgeBrowser } from '../util/util';
|
||||
|
||||
const RateExtension: React.FC = () => {
|
||||
const dispatch = useAppDispatch()
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const reviewed = useAppSelector(state => state.env.tempData.reviewed)
|
||||
|
||||
const handleRateClick = () => {
|
||||
dispatch(setTempData({
|
||||
reviewed: true
|
||||
}))
|
||||
// Chrome Web Store URL for your extension
|
||||
if (isEdgeBrowser()) {
|
||||
openUrl('https://microsoftedge.microsoft.com/addons/detail/lignnlhlpiefmcjkdkmfjdckhlaiajan')
|
||||
} else {
|
||||
openUrl('https://chromewebstore.google.com/webstore/detail/bciglihaegkdhoogebcdblfhppoilclp/reviews')
|
||||
}
|
||||
};
|
||||
|
||||
if (reviewed === true || reviewed === undefined) return null;
|
||||
|
||||
return (
|
||||
<div className="relative bg-gradient-to-r from-primary to-secondary text-primary-content p-4 rounded-lg shadow-lg text-sm transition-all duration-300 ease-in-out hover:shadow-xl">
|
||||
<button
|
||||
onClick={() => {
|
||||
dispatch(setTempData({
|
||||
reviewed: true
|
||||
}))
|
||||
}}
|
||||
className="absolute top-2 right-2 text-primary-content opacity-70 hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<IoMdClose size={20} />
|
||||
</button>
|
||||
<h3 className="text-lg font-bold mb-2 animate-pulse">喜欢这个扩展吗?</h3>
|
||||
<p className="mb-3">如果觉得有用,请给我们评分!</p>
|
||||
<button
|
||||
onClick={handleRateClick}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
className="btn btn-accent btn-sm gap-2 transition-all duration-300 ease-in-out hover:scale-105"
|
||||
>
|
||||
<FaStar className={`inline-block text-yellow-300 ${isHovered ? 'animate-spin' : ''}`} />
|
||||
去评分
|
||||
<span className="transition-transform duration-300 ease-in-out transform inline-block">
|
||||
{isHovered ? '🚀' : '→'}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RateExtension;
|
@@ -222,24 +222,21 @@ export const ASK_ENABLED_DEFAULT = true
|
||||
export const DEFAULT_SERVER_URL_OPENAI = 'https://api.openai.com'
|
||||
export const CUSTOM_MODEL_TOKENS = 16385
|
||||
|
||||
export const MODEL_TIP = '推荐gpt-4o-mini,能力强,价格低,token上限大'
|
||||
export const MODELS = [{
|
||||
code: 'gpt-3.5-turbo',
|
||||
name: 'gpt-3.5-turbo',
|
||||
tokens: 4096,
|
||||
code: 'gpt-4o-mini',
|
||||
name: 'gpt-4o-mini',
|
||||
tokens: 128000,
|
||||
}, {
|
||||
code: 'gpt-3.5-turbo-0125',
|
||||
name: 'gpt-3.5-turbo-0125',
|
||||
tokens: 16385,
|
||||
}, {
|
||||
code: 'gpt-3.5-turbo-1106',
|
||||
name: 'gpt-3.5-turbo-1106',
|
||||
tokens: 16385,
|
||||
}, {
|
||||
code: 'custom',
|
||||
name: '自定义',
|
||||
}]
|
||||
export const GEMINI_TOKENS = 32768
|
||||
export const MODEL_DEFAULT = MODELS[1].code
|
||||
export const MODEL_DEFAULT = MODELS[0].code
|
||||
export const MODEL_MAP: {[key: string]: typeof MODELS[number]} = {}
|
||||
for (const model of MODELS) {
|
||||
MODEL_MAP[model.code] = model
|
||||
|
@@ -1,13 +1,24 @@
|
||||
import {useAppDispatch} from './redux'
|
||||
import {useAppDispatch, useAppSelector} from './redux'
|
||||
import React, {useCallback} from 'react'
|
||||
import {setNeedScroll} from '../redux/envReducer'
|
||||
import {setNeedScroll, setReviewAction, setTempData} from '../redux/envReducer'
|
||||
|
||||
const useSubtitle = () => {
|
||||
const dispatch = useAppDispatch()
|
||||
const reviewed = useAppSelector(state => state.env.tempData.reviewed)
|
||||
const reviewAction = useAppSelector(state => state.env.reviewAction)
|
||||
const reviewActions = useAppSelector(state => state.env.tempData.reviewActions)
|
||||
|
||||
const move = useCallback((time: number, togglePause: boolean) => {
|
||||
window.parent.postMessage({type: 'move', time, togglePause}, '*')
|
||||
}, [])
|
||||
|
||||
//review action
|
||||
if (reviewed === undefined && !reviewAction) {
|
||||
dispatch(setReviewAction(true))
|
||||
dispatch(setTempData({
|
||||
reviewActions: (reviewActions ?? 0) + 1
|
||||
}))
|
||||
}
|
||||
}, [dispatch, reviewAction, reviewActions, reviewed])
|
||||
|
||||
const scrollIntoView = useCallback((ref: React.RefObject<HTMLDivElement>) => {
|
||||
ref.current?.scrollIntoView({behavior: 'smooth', block: 'center'})
|
||||
|
@@ -13,6 +13,7 @@ import {
|
||||
setTitle,
|
||||
setTotalHeight,
|
||||
setUrl,
|
||||
setTempData,
|
||||
} from '../redux/envReducer'
|
||||
import {EventBusContext} from '../Router'
|
||||
import {EVENT_EXPAND, GEMINI_TOKENS, TOTAL_HEIGHT_MAX, TOTAL_HEIGHT_MIN, WORDS_MIN, WORDS_RATE} from '../const'
|
||||
@@ -39,6 +40,17 @@ const useSubtitleService = () => {
|
||||
const transResults = useAppSelector(state => state.env.transResults)
|
||||
const hideOnDisableAutoTranslate = useAppSelector(state => state.env.envData.hideOnDisableAutoTranslate)
|
||||
const autoTranslate = useAppSelector(state => state.env.autoTranslate)
|
||||
const reviewed = useAppSelector(state => state.env.tempData.reviewed)
|
||||
const reviewActions = useAppSelector(state => state.env.tempData.reviewActions)
|
||||
|
||||
//如果reviewActions达到15次,则设置reviewed为false
|
||||
useEffect(() => {
|
||||
if (reviewed === undefined && reviewActions && reviewActions >= 15) {
|
||||
dispatch(setTempData({
|
||||
reviewed: false
|
||||
}))
|
||||
}
|
||||
}, [reviewActions, dispatch, reviewed])
|
||||
|
||||
// 监听消息
|
||||
useEffect(() => {
|
||||
|
@@ -9,7 +9,9 @@ import {
|
||||
setLastTransTime,
|
||||
setSummaryContent,
|
||||
setSummaryError,
|
||||
setSummaryStatus
|
||||
setSummaryStatus,
|
||||
setReviewAction,
|
||||
setTempData
|
||||
} from '../redux/envReducer'
|
||||
import {
|
||||
LANGUAGE_DEFAULT,
|
||||
@@ -38,6 +40,9 @@ const useTranslate = () => {
|
||||
const language = LANGUAGES_MAP[envData.language??LANGUAGE_DEFAULT]
|
||||
const summarizeLanguage = LANGUAGES_MAP[envData.summarizeLanguage??SUMMARIZE_LANGUAGE_DEFAULT]
|
||||
const title = useAppSelector(state => state.env.title)
|
||||
const reviewed = useAppSelector(state => state.env.tempData.reviewed)
|
||||
const reviewAction = useAppSelector(state => state.env.reviewAction)
|
||||
const reviewActions = useAppSelector(state => state.env.tempData.reviewActions)
|
||||
|
||||
/**
|
||||
* 获取下一个需要翻译的行
|
||||
@@ -137,6 +142,14 @@ const useTranslate = () => {
|
||||
}, [data?.body, envData, language.name, title, dispatch])
|
||||
|
||||
const addSummarizeTask = useCallback(async (type: SummaryType, segment: Segment) => {
|
||||
//review action
|
||||
if (reviewed === undefined && !reviewAction) {
|
||||
dispatch(setReviewAction(true))
|
||||
dispatch(setTempData({
|
||||
reviewActions: (reviewActions ?? 0) + 1
|
||||
}))
|
||||
}
|
||||
|
||||
if (segment.text.length >= SUMMARIZE_THRESHOLD) {
|
||||
let subtitles = ''
|
||||
for (const item of segment.items) {
|
||||
|
@@ -48,6 +48,9 @@ interface EnvState {
|
||||
|
||||
searchText: string
|
||||
searchResult: Set<number>
|
||||
|
||||
//当前视频是否计算过操作
|
||||
reviewAction: boolean
|
||||
}
|
||||
|
||||
const initialState: EnvState = {
|
||||
@@ -77,6 +80,8 @@ const initialState: EnvState = {
|
||||
searchResult: new Set(),
|
||||
|
||||
asks: [],
|
||||
|
||||
reviewAction: false,
|
||||
}
|
||||
|
||||
export const slice = createSlice({
|
||||
@@ -98,6 +103,9 @@ export const slice = createSlice({
|
||||
...action.payload,
|
||||
}
|
||||
},
|
||||
setReviewAction: (state, action: PayloadAction<boolean>) => {
|
||||
state.reviewAction = action.payload
|
||||
},
|
||||
setTempReady: (state) => {
|
||||
state.tempReady = true
|
||||
},
|
||||
@@ -319,6 +327,7 @@ export const {
|
||||
setAutoTranslate,
|
||||
setAutoScroll,
|
||||
setNoVideo,
|
||||
setReviewAction,
|
||||
setNeedScroll,
|
||||
setCurIdx,
|
||||
setEnvData,
|
||||
|
2
src/typings.d.ts
vendored
2
src/typings.d.ts
vendored
@@ -40,6 +40,8 @@ interface TempData {
|
||||
curSummaryType: SummaryType
|
||||
downloadType?: string
|
||||
compact?: boolean // 是否紧凑视图
|
||||
reviewActions?: number // 点击或总结行为达到一定次数后,显示评分(一个视频最多只加1次)
|
||||
reviewed?: boolean // 是否点击过评分,undefined: 不显示;true: 已点击;false: 未点击(需要显示)
|
||||
}
|
||||
|
||||
interface TaskDef {
|
||||
|
@@ -1,5 +1,10 @@
|
||||
import {SyntheticEvent} from 'react'
|
||||
|
||||
export const isEdgeBrowser = () => {
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
return userAgent.includes('edg/') && !userAgent.includes('edge/');
|
||||
}
|
||||
|
||||
export const formatTime = (time: number) => {
|
||||
if (!time) return '00:00'
|
||||
|
||||
|
@@ -12,7 +12,7 @@ export default ({mode}) => {
|
||||
visualizer() as PluginOption,
|
||||
]
|
||||
// @ts-ignore
|
||||
if (mode === 'production_chrome') {
|
||||
if (mode === 'production_chrome' || mode === 'production_edge') {
|
||||
plugins.push(crx({
|
||||
manifest,
|
||||
}))
|
||||
|
Reference in New Issue
Block a user