├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── 1-Bug_report.md │ ├── 2-Question.md │ └── 3-Feature_request.md ├── config.yml └── workflows │ ├── deploy-doc.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── Writerside ├── c.list ├── cfg │ └── buildprofiles.xml ├── dp.tree ├── images │ ├── 8bitdo-micro-l.png │ ├── 8bitdo-zero2-l.png │ ├── ai-chat.png │ ├── change-chat-topic.png │ ├── favorite-full.png │ ├── logo.png │ ├── machine-translation.png │ ├── open-ai-api-setting.png │ ├── setting-storage.png │ ├── shortcut.png │ ├── split-video-preview.png │ └── split-video.png ├── redirection-rules.xml ├── topics │ ├── AI-Chat.md │ ├── AI-Subtitles.md │ ├── Config-OpenAI-API.md │ ├── Config-Shortcut.md │ ├── Config-Tencent-API.md │ ├── Config-Translate.md │ ├── Config-YouDao-API.md │ ├── Download-Video.md │ ├── Home.topic │ ├── Installation.md │ ├── Introduction.md │ ├── Software-Recommendation.md │ ├── Split-Long-Video.md │ ├── Theory.md │ ├── Usage.md │ ├── Use-Bluetooth-Game-Controller.md │ ├── favorite.md │ └── storage.md ├── v.list └── writerside.cfg ├── assets ├── assets.d.ts ├── entitlements.mac.plist ├── icons │ ├── 128x128.png │ ├── 16x16.png │ ├── 24x24.png │ ├── 256x256.png │ ├── 32x32.png │ ├── 48x48.png │ ├── 64x64.png │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ ├── icon@2x.png │ ├── install.gif │ └── install.png ├── loading.svg ├── logo-dark.png └── logo-light.png ├── components.json ├── drizzle.config.ts ├── drizzle └── migrations │ ├── 0000_funny_mauler.sql │ └── meta │ ├── 0000_snapshot.json │ └── _journal.json ├── forge.config.ts ├── forge.env.d.ts ├── index.html ├── package.json ├── postcss.config.js ├── scripts ├── download.mjs ├── download_video.bat └── download_video.sh ├── src ├── app.tsx ├── backend │ ├── controllers │ │ ├── AiFuncController.ts │ │ ├── AiTransController.ts │ │ ├── ConvertController.ts │ │ ├── DownloadVideoController.ts │ │ ├── DpTaskController.ts │ │ ├── FavoriteClipsController.ts │ │ ├── MediaController.ts │ │ ├── SrtTimeAdjustController.ts │ │ ├── StorageController.ts │ │ ├── SubtitleController.ts │ │ ├── SystemController.ts │ │ ├── TagController.ts │ │ └── WatchHistoryController.ts │ ├── db │ │ ├── db.ts │ │ ├── index.ts │ │ ├── migrate.ts │ │ └── tables │ │ │ ├── clipTagRelation.ts │ │ │ ├── dpTask.ts │ │ │ ├── kvs.ts │ │ │ ├── sentenceTranslates.ts │ │ │ ├── stems.ts │ │ │ ├── subtitleTimestampAdjustment.ts │ │ │ ├── tag.ts │ │ │ ├── videoClip.ts │ │ │ ├── watchHistory.ts │ │ │ ├── wordTranslates.ts │ │ │ └── words.ts │ ├── dispatcher.ts │ ├── errors │ │ ├── AssertionError.ts │ │ └── errors.ts │ ├── interfaces │ │ ├── controller.ts │ │ └── postConstruce.ts │ ├── ioc │ │ ├── inversify.config.ts │ │ ├── logger.ts │ │ └── types.ts │ ├── objs │ │ ├── ChildProcessTask.ts │ │ ├── FfmpegTask.ts │ │ ├── OpenAiTtsRequest.ts │ │ ├── OpenAiWhisperRequest.ts │ │ ├── TencentClient.ts │ │ ├── YouDaoClient.ts │ │ ├── config-tender.ts │ │ └── dl-video │ │ │ ├── DlpDownloadVideo.ts │ │ │ └── DlpFetchFileName.ts │ ├── services │ │ ├── AiProviderService.ts │ │ ├── AiServiceImpl.ts │ │ ├── AiTransServiceImpl.ts │ │ ├── CacheService.ts │ │ ├── ChatService.ts │ │ ├── CheckUpdate.ts │ │ ├── ClientProviderService.ts │ │ ├── ConvertService.ts │ │ ├── DlVideoService.ts │ │ ├── DpTaskService.ts │ │ ├── FavouriteClipsService.ts │ │ ├── FfmpegService.ts │ │ ├── LocationService.ts │ │ ├── MediaService.ts │ │ ├── OpenAiService.ts │ │ ├── OssService.ts │ │ ├── ScheduleServiceImpl.ts │ │ ├── SettingService.ts │ │ ├── SplitVideoService.ts │ │ ├── SrtTimeAdjustService.ts │ │ ├── SubtitleService.ts │ │ ├── SystemService.ts │ │ ├── TagService.ts │ │ ├── TtsService.ts │ │ ├── WatchHistoryService.ts │ │ ├── WhisperService.ts │ │ ├── impl │ │ │ ├── AbstractOssServiceImpl.ts │ │ │ ├── CacheService.ts │ │ │ ├── ChatServiceImpl.ts │ │ │ ├── ClipOssServiceImpl.ts │ │ │ ├── ConvertServiceImpl.ts │ │ │ ├── DlVideoServiceImpl.ts │ │ │ ├── DpTaskServiceImpl.ts │ │ │ ├── FavouriteClipsServiceImpl.ts │ │ │ ├── FfmpegServiceImpl.ts │ │ │ ├── LocationServiceImpl.ts │ │ │ ├── MediaServiceImpl.ts │ │ │ ├── OpenAIServiceImpl.ts │ │ │ ├── SettingServiceImpl.ts │ │ │ ├── SplitVideoServiceImpl.ts │ │ │ ├── SrtTimeAdjustServiceImpl.ts │ │ │ ├── SubtitleServiceImpl.ts │ │ │ ├── SystemServiceImpl.ts │ │ │ ├── TagServiceImpl.ts │ │ │ ├── TranslateServiceImpl.ts │ │ │ ├── WatchHistoryServiceImpl.ts │ │ │ ├── WhisperServiceImpl.ts │ │ │ └── clients │ │ │ │ ├── AiProviderServiceImpl.ts │ │ │ │ ├── TencentProvider.ts │ │ │ │ └── YouDaoProvider.ts │ │ └── prompts │ │ │ ├── analyze-grammer.ts │ │ │ ├── analyze-phrases.ts │ │ │ ├── analyze-word.ts │ │ │ ├── example-sentence.ts │ │ │ ├── phraseGroupPropmt.ts │ │ │ ├── prompt-punctuation.ts │ │ │ ├── prompt.ts │ │ │ └── synonymous-sentence.ts │ ├── store.ts │ ├── test │ │ ├── sentence.test.ts │ │ └── whisper.test.ts │ └── utils │ │ ├── FileUtil.ts │ │ ├── LocationUtil.ts │ │ ├── MatchSrt.ts │ │ ├── ObjUtil.ts │ │ └── TypeGuards.ts ├── common │ ├── api │ │ ├── api-def.ts │ │ ├── dto.ts │ │ └── register.ts │ ├── constants │ │ └── error-constants.ts │ ├── interfaces │ │ └── index.ts │ ├── types │ │ ├── DlVideoType.ts │ │ ├── SentenceC.tsx │ │ ├── SentenceStruct.ts │ │ ├── SettingType.ts │ │ ├── Types.ts │ │ ├── WatchHistoryVO.ts │ │ ├── YdRes.ts │ │ ├── aiRes │ │ │ ├── AiAnalyseGrammarsRes.ts │ │ │ ├── AiAnalyseNewPhrasesRes.ts │ │ │ ├── AiAnalyseNewWordsRes.ts │ │ │ ├── AiFuncExplainSelectRes.ts │ │ │ ├── AiFuncExplainSelectWithContextRes.ts │ │ │ ├── AiFuncFormatSplit.ts │ │ │ ├── AiFuncPolish.ts │ │ │ ├── AiFuncTranslateWithContextRes.ts │ │ │ ├── AiMakeExampleSentencesRes.ts │ │ │ ├── AiPhraseGroupRes.ts │ │ │ └── AiPunctuationResp.ts │ │ ├── chapter-result.ts │ │ ├── clipMeta │ │ │ ├── ClipMetaDataV1.ts │ │ │ ├── base.ts │ │ │ └── index.ts │ │ ├── dl-progress.ts │ │ ├── msg │ │ │ ├── AiCtxMenuExplainSelectMessage.ts │ │ │ ├── AiCtxMenuExplainSelectWithContextMessage.ts │ │ │ ├── AiCtxMenuPolishMessage.ts │ │ │ ├── AiNormalMessage.ts │ │ │ ├── AiWelcomeMessage.ts │ │ │ ├── HumanNormalMessage.ts │ │ │ ├── HumanTopicMessage.ts │ │ │ └── interfaces │ │ │ │ └── CustomMessage.ts │ │ ├── release.ts │ │ ├── store_schema.ts │ │ ├── tonvert-type.ts │ │ └── video-info.ts │ └── utils │ │ ├── AudioPlayer.ts │ │ ├── CollUtil.ts │ │ ├── Lock.ts │ │ ├── MediaUtil.ts │ │ ├── PathUtil.ts │ │ ├── RateLimiter.ts │ │ ├── SqliteBuilder.ts │ │ ├── SrtUtil.ts │ │ ├── TimeUtil.ts │ │ ├── TransHolder.ts │ │ ├── UndoRedo.ts │ │ ├── UrlUtil.ts │ │ ├── Util.ts │ │ ├── func-util.ts │ │ ├── praser │ │ └── chapter-parser.ts │ │ ├── srtSlice.ts │ │ └── str-util.ts ├── fronted │ ├── components │ │ ├── Button.tsx │ │ ├── ControlBox.tsx │ │ ├── ControlButton.tsx │ │ ├── Eb.tsx │ │ ├── FallBack.tsx │ │ ├── FileBrowser.tsx │ │ ├── FileDrop.tsx │ │ ├── MainSubtitle.tsx │ │ ├── NewTips.tsx │ │ ├── NormalLine.tsx │ │ ├── Notification.tsx │ │ ├── PlaySpeedToaster.tsx │ │ ├── Player.tsx │ │ ├── PlayerControlPanel.tsx │ │ ├── PlayerSrtLayout.tsx │ │ ├── PlayerToaster.tsx │ │ ├── Separtor.tsx │ │ ├── SideBar.tsx │ │ ├── SideSentence.tsx │ │ ├── Subtitle.tsx │ │ ├── Tag.tsx │ │ ├── TagSelector.tsx │ │ ├── ThemePreview.tsx │ │ ├── TitleBar │ │ │ ├── TitleBar.tsx │ │ │ ├── TitleBarMac.tsx │ │ │ ├── TitleBarWindows.css │ │ │ ├── TitleBarWindows.css.map │ │ │ ├── TitleBarWindows.scss │ │ │ ├── TitleBarWindows.tsx │ │ │ └── index.ts │ │ ├── TranscriptItem.tsx │ │ ├── VolumeSlider.tsx │ │ ├── bg │ │ │ ├── AboutBg.css │ │ │ ├── AboutBg.tsx │ │ │ └── Background.tsx │ │ ├── chat │ │ │ ├── Chat.tsx │ │ │ ├── ChatCenter.tsx │ │ │ ├── ChatLeftGrammers.tsx │ │ │ ├── ChatLeftPhrases.tsx │ │ │ ├── ChatLeftWords.tsx │ │ │ ├── ChatRightSentences.tsx │ │ │ ├── ChatTopicSelector.tsx │ │ │ ├── Playable.tsx │ │ │ ├── icons.tsx │ │ │ ├── markdown.tsx │ │ │ ├── message.tsx │ │ │ ├── msg │ │ │ │ ├── AiCtxMenuExplainSelectMsg.tsx │ │ │ │ ├── AiCtxMenuExplainSelectWithContextMsg.tsx │ │ │ │ ├── AiCtxMenuPolishMsg.tsx │ │ │ │ ├── AiNormalMsg.tsx │ │ │ │ ├── AiWelcomeMsg.tsx │ │ │ │ ├── HumanNormalMsg.tsx │ │ │ │ ├── HumanTopicMsg.tsx │ │ │ │ └── MsgDelete.tsx │ │ │ └── spinner.tsx │ │ ├── fileBowser │ │ │ ├── FileSelector.tsx │ │ │ ├── FolderSelector.tsx │ │ │ ├── ProjItem2.tsx │ │ │ ├── VideoItem2.tsx │ │ │ ├── music-card.tsx │ │ │ ├── project-list-card.tsx │ │ │ ├── project-list-comp.tsx │ │ │ └── project-list-item.tsx │ │ ├── playerSubtitle │ │ │ ├── FullscreenButton.tsx │ │ │ ├── PlayerNormalLine.tsx │ │ │ ├── PlayerSubtitle.tsx │ │ │ ├── PlayerSubtitleControlPannel.tsx │ │ │ └── PlayerSubtitlePanel.tsx │ │ ├── query │ │ │ ├── DatePickerWithRange.tsx │ │ │ ├── StringQuery.tsx │ │ │ └── TagQuery.tsx │ │ ├── setting │ │ │ ├── Combobox.tsx │ │ │ ├── FooterWrapper.tsx │ │ │ ├── Header.tsx │ │ │ ├── ItemWrapper.tsx │ │ │ ├── SettingInput.tsx │ │ │ ├── SliderInput.tsx │ │ │ ├── Title.tsx │ │ │ └── index.ts │ │ ├── short-cut │ │ │ ├── GlobalShortCut.tsx │ │ │ └── PlayerShortCut.tsx │ │ ├── speed-slider.tsx │ │ ├── srt-cops │ │ │ ├── atoms │ │ │ │ ├── translatable-line-core.tsx │ │ │ │ ├── word-pop.tsx │ │ │ │ └── word.tsx │ │ │ ├── fullscreen-translatable-line.tsx │ │ │ ├── podcast-viewer.tsx │ │ │ ├── translatable-line-podcast.tsx │ │ │ └── translatable-line.tsx │ │ ├── subtitle-viewer │ │ │ └── viewer-control-panel.tsx │ │ ├── toasts │ │ │ └── ModeSwitchToast.tsx │ │ └── ui │ │ │ ├── aspect-ratio.tsx │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── calendar.tsx │ │ │ ├── card.tsx │ │ │ ├── carousel.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── command.tsx │ │ │ ├── context-menu.tsx │ │ │ ├── dialog.tsx │ │ │ ├── drawer.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── form.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── popover.tsx │ │ │ ├── progress.tsx │ │ │ ├── resizable.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── select.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── slider.tsx │ │ │ ├── sonner.tsx │ │ │ ├── switch.tsx │ │ │ ├── table.tsx │ │ │ ├── tabs.tsx │ │ │ ├── textarea.tsx │ │ │ ├── toast.tsx │ │ │ ├── toaster.tsx │ │ │ ├── toggle.tsx │ │ │ ├── tooltip.tsx │ │ │ └── use-toast.ts │ ├── hooks │ │ ├── use-copy-to-clipboard.ts │ │ ├── useBoundary.ts │ │ ├── useChatPanel.ts │ │ ├── useConvert.ts │ │ ├── useCopyModeController.ts │ │ ├── useDpTask.ts │ │ ├── useDpTaskCenter.ts │ │ ├── useDpTaskViewer.ts │ │ ├── useFavouriteClip.ts │ │ ├── useFile.ts │ │ ├── useLayout.ts │ │ ├── usePlayerController.ts │ │ ├── usePlayerControllerSlices │ │ │ ├── SliceTypes.ts │ │ │ ├── createControllerSlice.ts │ │ │ ├── createInternalSlice.ts │ │ │ ├── createModeSlice.ts │ │ │ ├── createPlayerSlice.ts │ │ │ ├── createSentenceSlice.ts │ │ │ └── createSubtitleSlice.ts │ │ ├── usePlayerToaster.ts │ │ ├── usePointer.ts │ │ ├── useSetting.ts │ │ ├── useSettingForm.ts │ │ ├── useSplit.ts │ │ ├── useSubtitleScroll.ts │ │ ├── useSystem.ts │ │ └── useTranscript.ts │ ├── lib │ │ ├── SrtTender.ts │ │ ├── swr-util.ts │ │ └── utils.ts │ ├── pages │ │ ├── About.tsx │ │ ├── DownloadVideo.tsx │ │ ├── HomePage.tsx │ │ ├── Layout.tsx │ │ ├── PlayerWithControlsPage.tsx │ │ ├── TieleBarLayout.tsx │ │ ├── convert │ │ │ ├── Convert.tsx │ │ │ ├── ConvertFileSelector.tsx │ │ │ ├── FolderSelector.tsx │ │ │ └── convert-item.tsx │ │ ├── favourite │ │ │ ├── Favorite.tsx │ │ │ ├── FavouriteItem.tsx │ │ │ ├── FavouriteMainSrt.tsx │ │ │ └── FavouritePlayer.tsx │ │ ├── index.ts │ │ ├── setting │ │ │ ├── AppearanceSetting.tsx │ │ │ ├── CheckUpdate.tsx │ │ │ ├── OpenAiSetting.tsx │ │ │ ├── SettingLayout.tsx │ │ │ ├── ShortcutSetting.tsx │ │ │ ├── StorageSetting.tsx │ │ │ ├── TenantSetting.tsx │ │ │ └── YouDaoSetting.tsx │ │ ├── split │ │ │ ├── Split.tsx │ │ │ ├── SplitFile.tsx │ │ │ └── split-preview.tsx │ │ └── transcript │ │ │ ├── Transcript.tsx │ │ │ ├── TranscriptFile.tsx │ │ │ └── TranscriptTable.tsx │ ├── preload.d.ts │ └── styles │ │ ├── style.ts │ │ └── topic.css ├── index.css ├── main.ts ├── preload.ts ├── renderer.ts └── types.d.ts ├── tailwind.config.js ├── tsconfig.json ├── vite.base.config.ts ├── vite.main.config.ts ├── vite.preload.config.ts ├── vite.renderer.config.ts └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/fronted/components/ui 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:import/recommended", 12 | "plugin:import/electron", 13 | "plugin:import/typescript", 14 | "react-app" 15 | ], 16 | "parser": "@typescript-eslint/parser", 17 | "settings": { 18 | "import/resolver": { 19 | "node": { 20 | "extensions": [".js", ".jsx", ".ts", ".tsx"], 21 | "paths": ["src"] 22 | }, 23 | "alias": { 24 | "map": [["@", "./src"]], 25 | "extensions": [".js", ".jsx", ".ts", ".tsx"] 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | *.exe binary 3 | *.png binary 4 | *.jpg binary 5 | *.jpeg binary 6 | *.ico binary 7 | *.icns binary 8 | *.eot binary 9 | *.otf binary 10 | *.ttf binary 11 | *.woff binary 12 | *.woff2 binary 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-Question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question.❓ 4 | labels: 'question' 5 | --- 6 | 7 | ## Summary 8 | 9 | 10 | 11 | 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3-Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: You want something added to the boilerplate. 🎉 4 | labels: 'enhancement' 5 | --- 6 | 7 | 16 | -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | requiredHeaders: 2 | - Prerequisites 3 | - Expected Behavior 4 | - Current Behavior 5 | - Possible Solution 6 | - Your Environment 7 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/release.yml 2 | name: Release app 3 | on: 4 | workflow_dispatch: 5 | permissions: 6 | contents: write 7 | jobs: 8 | build: 9 | strategy: 10 | matrix: 11 | os: 12 | [ 13 | { name: 'linux', image: 'ubuntu-latest' }, 14 | { name: 'windows', image: 'windows-latest' }, 15 | { name: 'macos-x64', image: 'macos-13' }, 16 | { name: 'macos-arm64', image: 'macos-14' }, 17 | ] 18 | runs-on: ${{ matrix.os.image }} 19 | steps: 20 | - name: Github checkout 21 | uses: actions/checkout@v4 22 | - name: Use Node.js 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: 20 26 | - run: yarn install --frozen-lockfile 27 | - name: Publish app 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | run: npm run publish 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | .DS_Store 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # TypeScript cache 43 | *.tsbuildinfo 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | .env.test 63 | 64 | # parcel-bundler cache (https://parceljs.org/) 65 | .cache 66 | 67 | # next.js build output 68 | .next 69 | 70 | # nuxt.js build output 71 | .nuxt 72 | 73 | # vuepress build output 74 | .vuepress/dist 75 | 76 | # Serverless directories 77 | .serverless/ 78 | 79 | # FuseBox cache 80 | .fusebox/ 81 | 82 | # DynamoDB Local files 83 | .dynamodb/ 84 | 85 | # Webpack 86 | .webpack/ 87 | 88 | # Vite 89 | .vite/ 90 | 91 | # Electron-Forge 92 | out/ 93 | 94 | .idea 95 | 96 | lib/ 97 | /dist/ 98 | -------------------------------------------------------------------------------- /Writerside/c.list: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Writerside/cfg/buildprofiles.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | logo.png 7 | https://solidspoon.xyz/DashPlayer/home.html 8 | Vivid 9 | halloween 10 | Get DashPlayer 11 | https://github.com/solidSpoon/DashPlayer 12 | true 13 | false 14 | 15 | 16 | 17 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Writerside/dp.tree: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Writerside/images/8bitdo-micro-l.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidSpoon/DashPlayer/4499055708c86a52390a9861ebde88f5561f68e7/Writerside/images/8bitdo-micro-l.png -------------------------------------------------------------------------------- /Writerside/images/8bitdo-zero2-l.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidSpoon/DashPlayer/4499055708c86a52390a9861ebde88f5561f68e7/Writerside/images/8bitdo-zero2-l.png -------------------------------------------------------------------------------- /Writerside/images/ai-chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidSpoon/DashPlayer/4499055708c86a52390a9861ebde88f5561f68e7/Writerside/images/ai-chat.png -------------------------------------------------------------------------------- /Writerside/images/change-chat-topic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidSpoon/DashPlayer/4499055708c86a52390a9861ebde88f5561f68e7/Writerside/images/change-chat-topic.png -------------------------------------------------------------------------------- /Writerside/images/favorite-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidSpoon/DashPlayer/4499055708c86a52390a9861ebde88f5561f68e7/Writerside/images/favorite-full.png -------------------------------------------------------------------------------- /Writerside/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidSpoon/DashPlayer/4499055708c86a52390a9861ebde88f5561f68e7/Writerside/images/logo.png -------------------------------------------------------------------------------- /Writerside/images/machine-translation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidSpoon/DashPlayer/4499055708c86a52390a9861ebde88f5561f68e7/Writerside/images/machine-translation.png -------------------------------------------------------------------------------- /Writerside/images/open-ai-api-setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidSpoon/DashPlayer/4499055708c86a52390a9861ebde88f5561f68e7/Writerside/images/open-ai-api-setting.png -------------------------------------------------------------------------------- /Writerside/images/setting-storage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidSpoon/DashPlayer/4499055708c86a52390a9861ebde88f5561f68e7/Writerside/images/setting-storage.png -------------------------------------------------------------------------------- /Writerside/images/shortcut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidSpoon/DashPlayer/4499055708c86a52390a9861ebde88f5561f68e7/Writerside/images/shortcut.png -------------------------------------------------------------------------------- /Writerside/images/split-video-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidSpoon/DashPlayer/4499055708c86a52390a9861ebde88f5561f68e7/Writerside/images/split-video-preview.png -------------------------------------------------------------------------------- /Writerside/images/split-video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidSpoon/DashPlayer/4499055708c86a52390a9861ebde88f5561f68e7/Writerside/images/split-video.png -------------------------------------------------------------------------------- /Writerside/redirection-rules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | Created after removal of "About Dash Player" from Dash Player 11 | AboutDashPlayer.html 12 | 13 | 14 | " from Dash Player]]> 15 | Software-Recommendation.html 16 | 17 | 18 | Created after removal of "使用第三方工具生成字幕" from Dash Player 19 | 使用第三方工具生成字幕.html 20 | 21 | 22 | Created after removal of "DashPlayer 简介" from Dash Player 23 | Introduction.html 24 | 25 | 26 | Created after removal of "使用 Python 脚本生成字幕" from Dash Player 27 | 使用-Python-脚本生成字幕.html 28 | 29 | 30 | Created after removal of "使用内置的转录功能生成字幕" from Dash Player 31 | 使用内置的转录功能生成字幕.html 32 | 33 | 34 | Created after removal of "软件推荐" from Dash Player 35 | Software-Recommendation.html 36 | 37 | 38 | -------------------------------------------------------------------------------- /Writerside/topics/AI-Chat.md: -------------------------------------------------------------------------------- 1 | # AI 整句学习 2 | 3 | 当您遇到看不懂的句子或者想查询句子中的词组或语法时,可以使用 AI 整句学习功能。 4 | 5 | ## 如何使用 6 | 7 | ### 调出 AI 整句学习界面 8 | 9 | 看视频时,遇到长难句,点击键盘上 `?` 键,即可弹出 AI 整句学习界面。 10 | 11 | ![ai-chat.png](ai-chat.png) 12 | 13 | 这会展示当前句子的语法、生词、词组等信息,同时给出相关例句,帮助您理解这个句子。 14 | 15 | ### 切换学习句子 16 | 17 | 当您的句子跨了多行字幕时,请在右上角选择您想要学习的句子,右键点击用选择内容新建对话。 18 | 19 | ![change-chat-topic.png](change-chat-topic.png) 20 | 21 | ### 其他内置功能 22 | 23 | 该界面的右键菜单内置了查单词,发音,例句等常用功能,方便您学习。 24 | -------------------------------------------------------------------------------- /Writerside/topics/AI-Subtitles.md: -------------------------------------------------------------------------------- 1 | # 人工智能字幕 2 | 3 | DashPlayer 的核心功能要求必须有视频的 `srt` 字幕文件,当您没有字幕文件时,可以使用人工智能来生成字幕。 4 | 5 | DashPlayer 目前支持调用 OpenAI 的 `Whisper` 接口生成字幕,这个模型的准确度很高,价格也比较便宜。 6 | 7 | 8 | 配置 OpenAI 密钥 9 | 进入Transcript页面 10 | 在左侧文件浏览器中找到相应的文件后点击添加到转录队列 11 | 点击转录按钮 12 | 13 | 14 | > 转录时调用接口响应的时间会比较长,实际测试发现代理服务可能会切断这种长时间的连接,如果转录失败,请尝试在代理中将您配置的 15 | > OpenAI 域名排除。 16 | > {style="note"} 17 | 18 | 转录时您可以离开这个界面继续观看视频,但请不要关闭 DashPlayer。转录完成后会自动更新相关视频的字幕。 19 | 20 | -------------------------------------------------------------------------------- /Writerside/topics/Config-OpenAI-API.md: -------------------------------------------------------------------------------- 1 | # 配置 OpenAI 密钥 2 | 3 | > 如果你不知道 OpenAI 是什么,可以参考 [OpenAI 官网](https://www.openai.com/)。 4 | > 5 | > 简而言之,OpenAI 提供了一些强大又便宜的人工智能 API,可以用来做各种有趣的事情。 6 | 7 | 您需要配置 OpenAI 密钥才能使用转录字幕,整句学习相关功能。 8 | 9 | OpenAI 官方接口可能不好获得,您可以通过第三方的中转服务来获取 OpenAI 密钥,通常这些中转服务会提供更便宜的价格: 10 | 11 | - https://one.gptnb.me/ 12 | - https://www.gptapi.us/ 13 | - https://aihubmix.com/ 14 | 15 | 注册充值后将 key 和 endpoint 填入 DashPlayer 的设置中。 16 | 17 | ![open-ai-api-setting.png](open-ai-api-setting.png) 18 | 19 | 目前 DashPlayer 会调用 `gpt-3.5-turbo` 和 `whisper-1` 这两个接口,价格比较便宜。 请确保您使用的中转站支持这两个接口。 20 | -------------------------------------------------------------------------------- /Writerside/topics/Config-Shortcut.md: -------------------------------------------------------------------------------- 1 | # 配置快捷键 2 | 3 | Start typing here... -------------------------------------------------------------------------------- /Writerside/topics/Config-Tencent-API.md: -------------------------------------------------------------------------------- 1 | 2 | # 配置腾讯云密钥 3 | 4 | 您需要腾讯云的密钥才能使用腾讯云的翻译服务。 5 | 6 | ## 简要说明 7 | 8 | 1. 官方网站:[机器翻译_智能翻译_自动翻译-腾讯云](https://cloud.tencent.com/product/tmt) 9 | 2. 官方资费说明:[机器翻译 计费概述-购买指南-文档中心-腾讯云](https://cloud.tencent.com/document/product/551/35017) 10 | 3. 付费版每月的前 500 万字符免费,超出的部分会按照 58 元 / 百万字符收取费用,请关注使用额度,以免意外扣费。 11 | 4. DashPlayer 会通过缓存和懒加载等技术,尽可能减少翻译次数,因此免费额度足够个人使用。 12 | 13 | ## 申请步骤 14 | 15 | 1. 打开 [腾讯云官网](https://cloud.tencent.com/) 并登录,登录成功后,鼠标移动到页面右上角的头像上,选择“账号信息”进行个人认证。使用腾讯云机器翻译必须进行个人认证,已认证过的话可以跳过。 16 | 2. 打开 [机器翻译_智能翻译_自动翻译-腾讯云](https://cloud.tencent.com/product/tmt),点击“立即使用”按钮。登录之后,会进入腾讯机器翻译服务控制台。 17 | 3. 选择开通付费版。 18 | 4. 创建访问密钥。将鼠标悬停在网页右上角的头像上,然后选择 [访问管理](https://console.cloud.tencent.com/cam/overview),然后在左侧菜单选择 [访问密钥 \- API 密钥管理](https://console.cloud.tencent.com/cam/capi),最好不要直接创建密钥,因为主账号创建的密钥可以访问调用你账号里的所有资源,因此保险起见选择创建一个子账号,在“用户权限”这一项进行搜索“机器翻译”,只勾选这一项。 19 | 5. 成功创建后,会看到这个子账户的“SecretId”和“SecretKey”。将其填入 DashPlayer 即可! 20 | 6. 完成🎉,如有疑惑的地方,请在 [issue](https://github.com/solidSpoon/DashPlayer/issues) 反馈。 21 | -------------------------------------------------------------------------------- /Writerside/topics/Config-Translate.md: -------------------------------------------------------------------------------- 1 | # 配置API 2 | 3 | 您需要配置 API 密钥才能使用翻译相关功能。 4 | 5 | DashPlayer 目前支持使用: 6 | 7 | - 腾讯云翻译字幕 8 | - 有道云翻译单词(鼠标**放置**在视频下方字幕行的单词上) 9 | - OpenAI 转录字幕,整句学习 10 | 11 | 12 | -------------------------------------------------------------------------------- /Writerside/topics/Config-YouDao-API.md: -------------------------------------------------------------------------------- 1 | # 配置有道云密钥 2 | 3 | 您需要有道智云的密钥才能使用有道的单词翻译服务。 4 | 5 | ## 简要说明 6 | 7 | 1. 官方网站:[有道智云 AI 开放平台](http://ai.youdao.com/) 8 | 2. 单词翻译(文本翻译)需要搭配语音合成使用。 9 | 2. 官方资费说明:[文本翻译](https://ai.youdao.com/DOCSIRMA/html/trans/price/wbfy/index.html) [语音合成](https://ai.youdao.com/DOCSIRMA/html/tts/price/yyhc/index.html) 10 | 3. 有道翻译官方接口会提供 50 元免费体验金,用完之后就要收费了。 11 | 4. DashPlayer 会缓存翻译结果,所以不会每次都调用有道翻译接口。 12 | 13 | ## 申请步骤 14 | 15 | 16 | 打开 有道智云 AI 开放平台 并点击右上角的注册。 17 | 打开 文本翻译服务页面,点击创建应用按钮,填写如下信息: 18 | 19 |

应用名称:DashPlayer

20 |

选择服务

21 | 22 |
  • 自然语言翻译服务 | 文本翻译
  • 23 |
  • 智能语音服务 | 语音合成
  • 24 |
    25 |

    接入方式:API

    26 |

    应用类别:实用工具

    27 |

    点击确定完成创建。

    28 |
    29 | 30 | 打开 应用总览页面,在应用列表中找到刚才创建的「应用名称」为「DashPlayer」的应用,然后就会看到「应用 ID」和「密钥/包名/Bundle ID」。将其填入 DashPlayer 即可! 31 |
    32 | 33 | 如有疑惑的地方,请在 [issue](https://github.com/solidSpoon/DashPlayer/issues) 反馈。 34 | -------------------------------------------------------------------------------- /Writerside/topics/Download-Video.md: -------------------------------------------------------------------------------- 1 | # 下载在线视频 2 | 3 | 目前 DashPlayer 只支持本地视频文件,您可以使用内置的下载功能(Beta)将在线视频下载到本地。 4 | 5 | 6 | 找到您想要下载的视频,复制视频的链接地址 7 | 打开 DashPlayer,到Download页面 8 | 粘贴视频链接到输入框中,点击Download按钮 9 | 等待一会,下方就会展示下载进度 10 | 下载完成后,您可以在系统下载文件夹中找到视频文件 11 | 12 | 13 | 视频下载目前处于 Beta 版本,如果效果不理想,您可以使用视频下载工具。 14 | 15 | - Windows 平台:[Internet Download Manager (IDM)](https://www.internetdownloadmanager.com/) 16 | - macOS 平台:[Downie](https://software.charliemonroe.net/downie/) 17 | -------------------------------------------------------------------------------- /Writerside/topics/Installation.md: -------------------------------------------------------------------------------- 1 | # 安装指南 2 | 3 | DashPlayer 目前并没有进行应用签名,因此在安装过程中可能会遭到操作系统的警告,当您遇到安装问题时请阅读下面的指南 4 | 5 | ## Windows 6 | 7 | 1. 在 [Latest Release](https://github.com/solidSpoon/DashPlayer/releases/latest) 页面下载以 `.exe` 结尾的安装包 8 | 2. 下载完成后双击安装包进行安装 9 | 3. 如果提示不安全,可以点击 `更多信息` -> `仍要运行` 进行安装 10 | 4. 开始使用吧! 11 | 12 | ## MacOS 13 | 14 | ### 手动安装 15 | 16 | 1. 去 [Latest Release](https://github.com/solidSpoon/DashPlayer/releases/latest) 页面下载对应芯片以 `.dmg` 的安装包 17 | 2. 下载完成后双击安装包进行安装,然后将 `DashPlayer` 拖动到 `Applications` 文件夹。 18 | 3. 开始使用吧! 19 | 20 | ### 故障排除 21 | 22 | #### "DashPlayer" can’t be opened because the developer cannot be verified. 23 | 24 |

    25 | image 26 |

    27 | 28 | 点击 `Cancel` 按钮,然后去 `设置` -> `隐私与安全性` 页面,点击 `仍要打开` 按钮,然后在弹出窗口里点击 `打开` 29 | 按钮即可,以后打开 `DashPlayer` 就再也不会有任何弹窗告警了 🎉 30 | 31 | | ![img](https://user-images.githubusercontent.com/39454841/226151875-03f79da9-45fc-4c0d-9d12-8cc9666ff904.png){width="200"} | ![img](https://user-images.githubusercontent.com/39454841/226151917-6b59f228-2bb9-4f12-9584-32bca9699d8e.png){width="200"} | 32 | |----------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------| 33 | 34 | #### XYZ is damaged and can’t be opened. You should move it to the Trash 35 | 36 | XYZ 已损坏,无法打开。您应该将其移动到垃圾桶中。 37 | 38 | 在控制台中输入以下命令: 39 | 40 | ```bash 41 | xattr -c 42 | ``` 43 | 44 | 示例: 45 | 46 | ```bash 47 | xattr -c /Applications/DashPlayer.app 48 | ``` 49 | -------------------------------------------------------------------------------- /Writerside/topics/Introduction.md: -------------------------------------------------------------------------------- 1 | # DashPlayer 简介 2 | 3 | ## 主要特性 4 | 5 | DashPlayer 的目标就是方便你观看英文视频。无论你是想泛听,还是想一句句精听,亦或是想要查询生词,DashPlayer 精心打磨的手感让您始终心情愉悦。 6 | 7 | - **高效的界面布局**:DashPlayer 的界面布局旨在最大化视频占用的空间,让你看得清。 8 | - **按字幕跳转:** 打开对应字幕文件,可以按句子快速跳转视频位置,轻松精听。 9 | - **支持字幕机器翻译:** 机器翻译相较于人工翻译更忠实于原文。DashPlayer 同时支持展示机器翻译与双语字幕中的翻译,学习更高效。 10 | - **查词查询**:鼠标悬停生词可快速查询,不打断学习进程。 11 | - **可调整界面尺寸:** 界面尺寸可调,适应不同屏幕和学习场景。 12 | - **记录播放位置:** 自动记录上次播放位置,方便下次接着学习。 13 | - **蓝牙遥控操作:** 支持蓝牙遥控,让你随时调整音量、跳转视频,学习更轻松! 14 | - **多彩主题**:内置多款不同亮度的主题,完美适应您的学习环境。 15 | 16 | ## 屏幕截图 17 | 18 | 多种亮度不同的主题: 19 | 20 |

    21 | image 22 |

    23 | 24 | 播放历史记录: 25 | 26 |

    27 | image 28 |

    29 | 30 | 字幕翻译和单词翻译: 31 | 32 |

    33 | image 34 |

    35 | 36 | 可以隐藏字幕: 37 | 38 |

    39 | image 40 |

    41 | -------------------------------------------------------------------------------- /Writerside/topics/Software-Recommendation.md: -------------------------------------------------------------------------------- 1 | # 软件推荐 2 | 3 | 观看喜欢的视频来学英语是个越来越流行的理论, 市面上有一些想法类似但专注于其他场景的播放器. 4 | 5 | DashPlayer 目前专注于播放本地视频, 如果你想要在线观看视频, 或者移动端观看, 可以尝试以下软件: 6 | 7 | ## 桌面端 8 | 9 | - [Language REACTOR](https://www.languagereactor.com/) 10 | - [Trancy](https://www.trancy.org/) 11 | 12 | ## 移动端 13 | 14 | - [雪球英语](https://bit.ly/m/snowball) 15 | -------------------------------------------------------------------------------- /Writerside/topics/Theory.md: -------------------------------------------------------------------------------- 1 | # 理论 2 | 3 | 也许你和我一样,曾经认为学习英语就必须努力背单词、学习语法,只要能记住七八千个单词,就能掌握英语。甚至对那些看美剧、阅读英文小说的人有些偏见,认为他们“不是在认真学习,而是在玩”,或者“他们的英语本来就好,所以能看懂那些内容。等我背完七八千个单词,我也能看懂。” 4 | 5 | 然而,经过多年这样的学习,许多人(包括我自己)发现并没有记住多少单词,感觉受了不少苦,却没有取得实质性的进步。其中一个原因是试卷上的题目太无聊,即便翻译成中文也提不起兴趣,更不用说用英文了。学习英语,应该用自己喜欢的素材: 6 | 7 | - 如果你喜欢看美剧,那就看美剧;喜欢小说,就看小说。 8 | - 如果想学习编程或者专业知识,可以看外国人录制的教程。 9 | - YouTube 上有各种各样的视频,总有你会喜欢的内容。 10 | 11 | 举个例子,当我开始意识到这一点时,我对编程非常感兴趣,便买了一本厚厚的编程书。遇到不懂的地方,我就查字典。虽然过程很辛苦,但因为内容非常吸引我,所以我坚持下来了,逐渐地我越看越熟练,最终愉快地完成了整本书的阅读。 12 | 13 | 读英文书确实辛苦,我现在更推荐观看视频。你可能会担心:“我的英语不好,听不懂怎么办?”其实,听不懂是很正常的,这里有几个建议: 14 | 15 | - 每个人都是从听不懂开始的,你并不孤单。不需要等到背完所有单词才开始听,可以从现在就开始。 16 | - 如果听不懂,可以重复听,真的听不懂就跳过去。英语视频资源非常丰富,如果这句重要的话,你会在其他视频中再次听到。 17 | - 如果想跟读,但觉得某句太难,可以先跳过,下次遇到时再尝试。 18 | - 可能需要近一年的时间你才会感觉到进步,所以即使前几个月没有显著感觉,也不要灰心。 19 | - 既然是自己喜欢的内容,就不会感到痛苦,可以慢慢来。 20 | - 试着找一些你感兴趣的主题,专注于这些主题的视频。同一主题的视频中词汇和表达方式往往会有重复,这会让你更容易理解。 21 | - 随着听力的提高,你会发现口语、阅读和写作也会相应提高。 22 | 23 | 很多英语学习博主也持有类似观点。我推荐你看看[Tinyfool](https://www.youtube.com/@tinyEnglish)老师的视频《[如何学习英语。为什么我们应该建立以听为主导的英语学习方法](https://www.youtube.com/watch?v=_l8Rn6tPs6o)》,他用一些列视频详细解释了为什么通过大量观看母语人士的视频来学习英语是有效的,为什么听力的重要性远大于阅读和写作,以及听力提高后,阅读和写作水平如何随之提高。 24 | 25 | YouTube 上还有许多其他优秀的英语学习博主,他们的视频同样富有启发性。 26 | 你在 YouTube 上搜索“english learning” 就会有很多相关视频,这类视频用词简单,语速较慢,也是很好的入门素材。 27 | 28 | 这里推荐几位英语学习博主(我随便挑的,你可以自己搜索): 29 | 30 | - [Steve Kaufmann](https://www.youtube.com/@Thelinguist) 31 | - [linguamarina](https://www.youtube.com/@linguamarina) 32 | - [Accent's Way English with Hadar](https://www.youtube.com/@hadar.shemesh) 33 | -------------------------------------------------------------------------------- /Writerside/topics/Usage.md: -------------------------------------------------------------------------------- 1 | # 基础用法 2 | 3 | ## 如何播放视频 4 | 5 | DashPlayer 支持常见的视频格式,以及 srt 字幕格式。 6 | 7 | - 使用 Open File 按钮打开视频和字幕文件 8 | - 使用 Open Folder 按钮打开视频文件夹,DashPlayer 会自动查找视频和字幕文件 9 | - 进入播放界面后,点击右下角的圆形按钮可以显示控制台 10 | 11 | ## 如何控制播放 12 | 13 | ### 通过鼠标/键盘快捷键控制播放 14 | 15 | DashPlayer 默认快捷键如下 16 | 17 | - 上一句:“←” 或 “a” 18 | - 下一句:“→” 或 “d” 19 | - 重复当前句:“↓” 或 “s” 20 | - 暂停/播放:“上” 或 “w” 或 “space” 21 | - 单句重复:“r”(repeat) 22 | - 展示/隐藏英文字幕:“e”(english) 23 | - 展示/隐藏中文字幕:“c”(chinese) 24 | - 展示/隐藏中英文字幕:“b”(both) 25 | - 切换主题:“t”(theme) 26 | - 调整当前句开始时间,提前 0.2 秒:“z” 27 | - 调整当前句开始时间,延后 0.2 秒:“x” 28 | - 打开整句学习:“?” 29 | 30 | 具体快捷键可在设置界面查看 31 | 32 | image 33 | -------------------------------------------------------------------------------- /Writerside/topics/Use-Bluetooth-Game-Controller.md: -------------------------------------------------------------------------------- 1 | # 使用蓝牙手柄控制播放 2 | 3 | 使用 DashPlayer 播放视频时, 您可能会高频率的使用快捷键, 但是键盘上的快捷键并不是很方便, 所以我们可以使用蓝牙手柄来控制播放. 4 | 5 | > 本文以[八位堂](https://www.8bitdo.cn)家的手柄为例, 您可购买任意**具有键盘模式**的蓝牙手柄. 6 | > 7 | > {style="note"} 8 | 9 | ## 控制的原理 10 | 11 | 八位堂家的 [Micro](https://www.8bitdo.cn/micro/) 和 [Zero2](https://www.8bitdo.cn/zero2/) 蓝牙手柄可当做蓝牙键盘使用。它们非常小巧, 单手握持很舒服, 所以可以用它来操控 DashPlayer。 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
    产品名称图片
    Micro 蓝牙手柄
    Zero2 蓝牙手柄
    31 | 32 | ## 操作步骤 33 | 34 | 1. 将手柄通过键盘模式链接到电脑 35 | 2. 进入 DashPlayer 快捷键设置, 设置手柄对应按键为快捷键 36 | 1. 打开 DashPlayer 设置界面 37 | 2. 进入快捷键设置 38 | 3. 点击对应的快捷键输入框 39 | 4. 按下手柄对应按键 40 | 5. 重复 3-4 步骤, 直到设置完成 41 | 6. 点击保存按钮 42 | 3. 开始使用吧! 43 | 44 | > 多个按键可以绑定到同一个快捷键, 只需用 `,` 分隔. 45 | 46 | ![shortcut.png](shortcut.png) 47 | 48 | ## 键盘映射软件 49 | 50 | DashPlayer 可以使用蓝牙手柄来控制播放, 但是你可能也想使用手柄来控制其他软件, 这时你就需要一个键盘映射软件. 51 | 52 | ### Windows 平台 53 | 54 | - [PowerToys](https://github.com/microsoft/PowerToys) 55 | 56 | ### macOS 平台 57 | 58 | - [Karabiner-Elements](https://karabiner-elements.pqrs.org/) 59 | -------------------------------------------------------------------------------- /Writerside/topics/favorite.md: -------------------------------------------------------------------------------- 1 | # 收藏视频片段 2 | 3 | ![favorite-full.png](favorite-full.png) 4 | 5 | 使用快捷键 `Shift + L` 收藏 / 取消收藏当前字幕片段。此时会有后台任务将当前片段剪切、压缩并保存到指定路径中。 6 | 待处理完成后,您可以在 Favorite 页面查看所有收藏的片段。 7 | 8 | 9 | -------------------------------------------------------------------------------- /Writerside/topics/storage.md: -------------------------------------------------------------------------------- 1 | # 存储 2 | 3 | 目前下载的视频,以及收藏的视频片段都会保存到这个文件夹中,可以在这修改一下存储路径。 4 | 5 | 修改完后,建议重启软件,以确保新的路径生效。 6 | 7 | ![setting-storage.png](setting-storage.png) 8 | -------------------------------------------------------------------------------- /Writerside/v.list: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Writerside/writerside.cfg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /assets/assets.d.ts: -------------------------------------------------------------------------------- 1 | type Styles = Record; 2 | 3 | declare module '*.svg' { 4 | export const ReactComponent: React.FC>; 5 | 6 | const content: string; 7 | export default content; 8 | } 9 | 10 | declare module '*.png' { 11 | const content: string; 12 | export default content; 13 | } 14 | 15 | declare module '*.jpg' { 16 | const content: string; 17 | export default content; 18 | } 19 | 20 | declare module '*.scss' { 21 | const content: Styles; 22 | export default content; 23 | } 24 | 25 | declare module '*.sass' { 26 | const content: Styles; 27 | export default content; 28 | } 29 | 30 | declare module '*.css' { 31 | const content: Styles; 32 | export default content; 33 | } 34 | -------------------------------------------------------------------------------- /assets/entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-unsigned-executable-memory 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /assets/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidSpoon/DashPlayer/4499055708c86a52390a9861ebde88f5561f68e7/assets/icons/128x128.png -------------------------------------------------------------------------------- /assets/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidSpoon/DashPlayer/4499055708c86a52390a9861ebde88f5561f68e7/assets/icons/16x16.png -------------------------------------------------------------------------------- /assets/icons/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidSpoon/DashPlayer/4499055708c86a52390a9861ebde88f5561f68e7/assets/icons/24x24.png -------------------------------------------------------------------------------- /assets/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidSpoon/DashPlayer/4499055708c86a52390a9861ebde88f5561f68e7/assets/icons/256x256.png -------------------------------------------------------------------------------- /assets/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidSpoon/DashPlayer/4499055708c86a52390a9861ebde88f5561f68e7/assets/icons/32x32.png -------------------------------------------------------------------------------- /assets/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidSpoon/DashPlayer/4499055708c86a52390a9861ebde88f5561f68e7/assets/icons/48x48.png -------------------------------------------------------------------------------- /assets/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidSpoon/DashPlayer/4499055708c86a52390a9861ebde88f5561f68e7/assets/icons/64x64.png -------------------------------------------------------------------------------- /assets/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidSpoon/DashPlayer/4499055708c86a52390a9861ebde88f5561f68e7/assets/icons/icon.icns -------------------------------------------------------------------------------- /assets/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidSpoon/DashPlayer/4499055708c86a52390a9861ebde88f5561f68e7/assets/icons/icon.ico -------------------------------------------------------------------------------- /assets/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidSpoon/DashPlayer/4499055708c86a52390a9861ebde88f5561f68e7/assets/icons/icon.png -------------------------------------------------------------------------------- /assets/icons/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidSpoon/DashPlayer/4499055708c86a52390a9861ebde88f5561f68e7/assets/icons/icon@2x.png -------------------------------------------------------------------------------- /assets/icons/install.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidSpoon/DashPlayer/4499055708c86a52390a9861ebde88f5561f68e7/assets/icons/install.gif -------------------------------------------------------------------------------- /assets/icons/install.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidSpoon/DashPlayer/4499055708c86a52390a9861ebde88f5561f68e7/assets/icons/install.png -------------------------------------------------------------------------------- /assets/logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidSpoon/DashPlayer/4499055708c86a52390a9861ebde88f5561f68e7/assets/logo-dark.png -------------------------------------------------------------------------------- /assets/logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidSpoon/DashPlayer/4499055708c86a52390a9861ebde88f5561f68e7/assets/logo-light.png -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/index.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/fronted/components", 15 | "utils": "@/fronted/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'drizzle-kit'; 2 | import path from 'path'; 3 | 4 | const DEV_LIB_PATH = '/Users/solidspoon/Desktop/DashPlayer'; 5 | 6 | export default { 7 | dialect: 'sqlite', 8 | schema: './src/backend/db/tables', 9 | out: './drizzle/migrations', 10 | dbCredentials: { 11 | url: path.join( 12 | DEV_LIB_PATH, 'data', 'dp_db.sqlite3' 13 | ), 14 | }, 15 | } as Config; 16 | -------------------------------------------------------------------------------- /drizzle/migrations/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "sqlite", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "6", 8 | "when": 1731730527741, 9 | "tag": "0000_funny_mauler", 10 | "breakpoints": true 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /forge.env.d.ts: -------------------------------------------------------------------------------- 1 | export {}; // Make this a module 2 | 3 | declare global { 4 | // This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Vite 5 | // plugin that tells the Electron app where to look for the Vite-bundled app code (depending on 6 | // whether you're running in development or production). 7 | const MAIN_WINDOW_VITE_DEV_SERVER_URL: string; 8 | const MAIN_WINDOW_VITE_NAME: string; 9 | 10 | namespace NodeJS { 11 | interface Process { 12 | // Used for hot reload after preload scripts. 13 | viteDevServers: Record; 14 | } 15 | } 16 | 17 | type VitePluginConfig = ConstructorParameters[0]; 18 | 19 | interface VitePluginRuntimeKeys { 20 | VITE_DEV_SERVER_URL: `${string}_VITE_DEV_SERVER_URL`; 21 | VITE_NAME: `${string}_VITE_NAME`; 22 | } 23 | } 24 | 25 | declare module 'vite' { 26 | interface ConfigEnv { 27 | root: string; 28 | forgeConfig: VitePluginConfig; 29 | forgeConfigSelf: VitePluginConfig[K][number]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DashPlayer 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /scripts/download_video.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal 3 | 4 | if "%~1"=="" ( 5 | echo Error: No library path provided. 6 | goto eof 7 | ) 8 | if "%~2"=="" ( 9 | echo Error: No video URL provided. 10 | goto eof 11 | ) 12 | 13 | set "LIB_PATH=%~1" 14 | set "VIDEO_URL=%~2" 15 | 16 | pushd "%USERPROFILE%\Downloads" || ( 17 | echo Failed to change directory to %USERPROFILE%\Downloads 18 | goto eof 19 | ) 20 | 21 | set "PATH=%LIB_PATH%;%PATH%" 22 | 23 | yt-dlp -S "res:1080,ext" "%VIDEO_URL%" 24 | 25 | echo. 26 | echo. 27 | echo Note: 28 | echo. 29 | echo The video has been successfully downloaded to %USERPROFILE%\Downloads 30 | echo Command used: yt-dlp -S "res:1080,ext" "%VIDEO_URL%" 31 | echo Tools such as ffmpeg, ffprobe, and yt-dlp are pre-set in the current command line and can be utilized. 32 | echo. 33 | 34 | pause 35 | 36 | cmd /k 37 | 38 | popd 39 | endlocal 40 | -------------------------------------------------------------------------------- /scripts/download_video.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if the required parameters are passed 4 | if [ -z "$1" ]; then 5 | echo "Error: No library path provided." 6 | exit 1 7 | fi 8 | 9 | if [ -z "$2" ]; then 10 | echo "Error: No video URL provided." 11 | exit 1 12 | fi 13 | 14 | LIB_PATH=$1 15 | VIDEO_URL=$2 16 | 17 | # Change directory to Downloads or exit if it fails 18 | cd "$HOME/Downloads" || { echo "Failed to change directory to $HOME/Downloads"; exit 1; } 19 | 20 | # Update PATH to include the library path 21 | export PATH="$LIB_PATH:$PATH" 22 | 23 | # Download the video using yt-dlp 24 | yt-dlp -S "res:1080,ext" "$VIDEO_URL" -P 25 | 26 | echo 27 | echo 28 | echo "Note:" 29 | echo 30 | echo "The video has been successfully downloaded to $HOME/Downloads" 31 | echo "Command used: yt-dlp -S \"res:1080,ext\" \"$VIDEO_URL\"" 32 | echo "Tools such as ffmpeg, ffprobe, and yt-dlp are pre-set in the current command line and can be utilized." 33 | echo 34 | 35 | # Keep the terminal open (comment this out if you run from a terminal and want the script to exit after download) 36 | read -p "Press [Enter] key to continue..." 37 | 38 | -------------------------------------------------------------------------------- /src/backend/controllers/AiTransController.ts: -------------------------------------------------------------------------------- 1 | import registerRoute from '@/common/api/register'; 2 | import { YdRes } from '@/common/types/YdRes'; 3 | import Controller from '@/backend/interfaces/controller'; 4 | import { inject, injectable } from 'inversify'; 5 | import TYPES from '@/backend/ioc/types'; 6 | import TranslateService from '@/backend/services/AiTransServiceImpl'; 7 | 8 | @injectable() 9 | export default class AiTransController implements Controller { 10 | @inject(TYPES.TranslateService) 11 | private translateService!: TranslateService; 12 | 13 | public async batchTranslate(sentences: string[]): Promise> { 14 | return this.translateService.transSentences(sentences); 15 | } 16 | 17 | public async youDaoTrans(str: string): Promise { 18 | return this.translateService.transWord(str); 19 | } 20 | 21 | registerRoutes(): void { 22 | registerRoute('ai-trans/batch-translate', (p) => this.batchTranslate(p)); 23 | registerRoute('ai-trans/word', (p) => this.youDaoTrans(p)); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/backend/controllers/ConvertController.ts: -------------------------------------------------------------------------------- 1 | import registerRoute from '@/common/api/register'; 2 | import { FolderVideos } from '@/common/types/tonvert-type'; 3 | import Controller from '@/backend/interfaces/controller'; 4 | import { inject, injectable } from 'inversify'; 5 | import TYPES from '@/backend/ioc/types'; 6 | import DpTaskService from '@/backend/services/DpTaskService'; 7 | import ConvertService from '@/backend/services/ConvertService'; 8 | import FfmpegService from '@/backend/services/FfmpegService'; 9 | 10 | @injectable() 11 | export default class ConvertController implements Controller { 12 | @inject(TYPES.DpTaskService) 13 | private dpTaskService!: DpTaskService; 14 | 15 | @inject(TYPES.ConvertService) 16 | private convertService!: ConvertService; 17 | 18 | @inject(TYPES.FfmpegService) 19 | private ffmpegService!: FfmpegService; 20 | 21 | public async toMp4(file: string): Promise { 22 | const taskId = await this.dpTaskService.create(); 23 | this.convertService.toMp4(taskId, file).then(); 24 | return taskId; 25 | } 26 | 27 | public async fromFolder(folders: string[]): Promise { 28 | return this.convertService.fromFolder(folders); 29 | } 30 | 31 | public async videoLength(filePath: string): Promise { 32 | return this.ffmpegService.duration(filePath); 33 | } 34 | 35 | registerRoutes(): void { 36 | registerRoute('convert/to-mp4', (p) => this.toMp4(p)); 37 | registerRoute('convert/from-folder', (p) => this.fromFolder(p)); 38 | registerRoute('convert/video-length', (p) => this.videoLength(p)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/backend/controllers/DownloadVideoController.ts: -------------------------------------------------------------------------------- 1 | import registerRoute from '@/common/api/register'; 2 | import { inject, injectable } from 'inversify'; 3 | import Controller from '@/backend/interfaces/controller'; 4 | import TYPES from '@/backend/ioc/types'; 5 | import LocationService, { LocationType } from '@/backend/services/LocationService'; 6 | import DpTaskService from '@/backend/services/DpTaskService'; 7 | import DlVideoService from '@/backend/services/DlVideoService'; 8 | import { COOKIE } from '@/common/types/DlVideoType'; 9 | 10 | @injectable() 11 | export default class DownloadVideoController implements Controller { 12 | @inject(TYPES.DlVideo) 13 | private dlVideoService!: DlVideoService; 14 | 15 | 16 | @inject(TYPES.LocationService) 17 | private locationService!: LocationService; 18 | 19 | @inject(TYPES.DpTaskService) 20 | private dpTaskService!: DpTaskService; 21 | 22 | async downloadVideo({ url,cookies }: { 23 | url: string, 24 | cookies: COOKIE 25 | }): Promise { 26 | // 系统下载文件夹 27 | const taskId = await this.dpTaskService.create(); 28 | const downloadFolder = this.locationService.getDetailLibraryPath(LocationType.VIDEOS); 29 | this.dlVideoService.dlVideo(taskId, url,cookies, downloadFolder).then(); 30 | return taskId; 31 | } 32 | 33 | registerRoutes(): void { 34 | registerRoute('download-video/url', (p) => this.downloadVideo(p)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/backend/controllers/DpTaskController.ts: -------------------------------------------------------------------------------- 1 | import registerRoute from '@/common/api/register'; 2 | import { DpTask } from '@/backend/db/tables/dpTask'; 3 | import Controller from '@/backend/interfaces/controller'; 4 | import { inject, injectable } from 'inversify'; 5 | import TYPES from '@/backend/ioc/types'; 6 | import DpTaskService from '@/backend/services/DpTaskService'; 7 | 8 | /** 9 | * AI 翻译 10 | * @param str 11 | */ 12 | @injectable() 13 | export default class DpTaskController implements Controller { 14 | @inject(TYPES.DpTaskService) 15 | private dpTaskService!: DpTaskService; 16 | 17 | public async detail(id: number) { 18 | return this.dpTaskService.detail(id); 19 | } 20 | 21 | public async details(ids: number[]): Promise> { 22 | return this.dpTaskService.details(ids); 23 | } 24 | 25 | public async cancel(id: number) { 26 | this.dpTaskService.cancel(id); 27 | } 28 | 29 | registerRoutes(): void { 30 | registerRoute('dp-task/detail', (p) => this.detail(p)); 31 | registerRoute('dp-task/cancel', (p) => this.cancel(p)); 32 | registerRoute('dp-task/details', (p) => this.details(p)); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/backend/controllers/SrtTimeAdjustController.ts: -------------------------------------------------------------------------------- 1 | import {InsertSubtitleTimestampAdjustment} from '@/backend/db/tables/subtitleTimestampAdjustment'; 2 | import registerRoute from '@/common/api/register'; 3 | import Controller from '@/backend/interfaces/controller'; 4 | import { inject, injectable } from 'inversify'; 5 | import TYPES from '@/backend/ioc/types'; 6 | import SrtTimeAdjustService from '@/backend/services/SrtTimeAdjustService'; 7 | 8 | /** 9 | * 调整字幕时间 10 | */ 11 | @injectable() 12 | export default class SrtTimeAdjustController implements Controller { 13 | @inject(TYPES.SrtTimeAdjustService) 14 | private srtTimeAdjustService!: SrtTimeAdjustService; 15 | 16 | /** 17 | * 记录调整时间 18 | * @param e 19 | */ 20 | public async record(e: InsertSubtitleTimestampAdjustment): Promise { 21 | await this.srtTimeAdjustService.record(e); 22 | } 23 | 24 | /** 25 | * 删除调整时间 26 | * @param key 27 | */ 28 | public async deleteByKey(key: string): Promise { 29 | await this.srtTimeAdjustService.deleteByKey(key); 30 | } 31 | 32 | /** 33 | * 删除调整时间 34 | * @param fileHash 35 | */ 36 | public async deleteByFile(fileHash: string): Promise { 37 | await this.srtTimeAdjustService.deleteByFile(fileHash); 38 | } 39 | 40 | registerRoutes(): void { 41 | registerRoute('subtitle-timestamp/delete/by-file-hash', (p) => this.deleteByFile(p)); 42 | registerRoute('subtitle-timestamp/delete/by-key', (p) => this.deleteByKey(p)); 43 | registerRoute('subtitle-timestamp/update', (p) => this.record(p)); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/backend/controllers/StorageController.ts: -------------------------------------------------------------------------------- 1 | import { SettingKey } from '@/common/types/store_schema'; 2 | import registerRoute from '@/common/api/register'; 3 | import Controller from '@/backend/interfaces/controller'; 4 | import { inject, injectable } from 'inversify'; 5 | import TYPES from '@/backend/ioc/types'; 6 | import SettingService from '@/backend/services/SettingService'; 7 | import LocationService from '@/backend/services/LocationService'; 8 | import FileUtil from '@/backend/utils/FileUtil'; 9 | 10 | @injectable() 11 | export default class StorageController implements Controller { 12 | @inject(TYPES.SettingService) private settingService!: SettingService; 13 | @inject(TYPES.LocationService) private locationService!: LocationService; 14 | 15 | public async storeSet({ key, value }: { key: SettingKey, value: string }): Promise { 16 | await this.settingService.set(key, value); 17 | } 18 | 19 | public async storeGet(key: SettingKey): Promise { 20 | return this.settingService.get(key); 21 | } 22 | 23 | public async queryCacheSize(): Promise { 24 | return await FileUtil.calculateReadableFolderSize(this.locationService.getBaseLibraryPath()); 25 | } 26 | 27 | public async listCollectionPaths(): Promise { 28 | return this.locationService.listCollectionPaths(); 29 | } 30 | 31 | 32 | registerRoutes(): void { 33 | registerRoute('storage/put', (p) => this.storeSet(p)); 34 | registerRoute('storage/get', (p) => this.storeGet(p)); 35 | registerRoute('storage/cache/size', (p) => this.queryCacheSize()); 36 | registerRoute('storage/collection/paths', () => this.listCollectionPaths()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/backend/controllers/SubtitleController.ts: -------------------------------------------------------------------------------- 1 | import registerRoute from '@/common/api/register'; 2 | import { SrtSentence } from '@/common/types/SentenceC'; 3 | import { inject, injectable } from 'inversify'; 4 | import TYPES from '@/backend/ioc/types'; 5 | import Controller from '@/backend/interfaces/controller'; 6 | import SubtitleService from '@/backend/services/SubtitleService'; 7 | 8 | 9 | @injectable() 10 | export default class SubtitleController implements Controller { 11 | 12 | @inject(TYPES.SubtitleService) 13 | private subtitleService!: SubtitleService; 14 | 15 | public async parseSrt(path: string): Promise { 16 | return this.subtitleService.parseSrt(path); 17 | } 18 | 19 | registerRoutes(): void { 20 | registerRoute('subtitle/srt/parse-to-sentences', (p) => this.parseSrt(p)); 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /src/backend/controllers/TagController.ts: -------------------------------------------------------------------------------- 1 | import Controller from '@/backend/interfaces/controller'; 2 | import { inject, injectable } from 'inversify'; 3 | import TYPES from '@/backend/ioc/types'; 4 | import TagService from '@/backend/services/TagService'; 5 | import registerRoute from '@/common/api/register'; 6 | import { Tag } from '@/backend/db/tables/tag'; 7 | import { FavouriteClipsService } from '@/backend/services/FavouriteClipsService'; 8 | 9 | @injectable() 10 | export default class TagController implements Controller { 11 | @inject(TYPES.TagService) private tagService!: TagService; 12 | @inject(TYPES.FavouriteClips) private favouriteClipsService!: FavouriteClipsService; 13 | 14 | public async addTag(name: string): Promise { 15 | return this.tagService.addTag(name); 16 | } 17 | 18 | public async deleteTag(id: number): Promise { 19 | return this.tagService.deleteTag(id); 20 | } 21 | 22 | public async updateTag({ id, name }: { id: number, name: string }) { 23 | return this.favouriteClipsService.renameTag(id, name); 24 | } 25 | 26 | public async search(keyword: string): Promise { 27 | return this.tagService.search(keyword); 28 | } 29 | 30 | registerRoutes(): void { 31 | registerRoute('tag/add', (p) => this.addTag(p)); 32 | registerRoute('tag/delete', (p) => this.deleteTag(p)); 33 | registerRoute('tag/update', (p) => this.updateTag(p)); 34 | registerRoute('tag/search', (p) => this.search(p)); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/backend/db/index.ts: -------------------------------------------------------------------------------- 1 | import db from './db'; 2 | 3 | export default db; 4 | -------------------------------------------------------------------------------- /src/backend/db/migrate.ts: -------------------------------------------------------------------------------- 1 | import { migrate } from 'drizzle-orm/better-sqlite3/migrator'; 2 | import fs from 'fs'; 3 | import db, { clearDB } from './db'; 4 | 5 | const isDev = process.env.NODE_ENV === 'development'; 6 | const config = { 7 | migrationsFolder: isDev 8 | ? 'drizzle/migrations' 9 | : `${process.resourcesPath}/drizzle/migrations`, 10 | }; 11 | const runMigrate = async () => { 12 | // migrate(db, config); 13 | try { 14 | migrate(db, config); 15 | } catch (error) { 16 | console.log('run migrate failed, clear db and retry'); 17 | await clearDB(); 18 | migrate(db, config); 19 | } 20 | } 21 | 22 | console.log('runMigrate', config); 23 | console.log('runMigrate', process.resourcesPath); 24 | fs.readdirSync(config.migrationsFolder).forEach((file) => { 25 | console.log('file', file); 26 | }); 27 | export default runMigrate; 28 | -------------------------------------------------------------------------------- /src/backend/db/tables/clipTagRelation.ts: -------------------------------------------------------------------------------- 1 | import { integer, sqliteTable, text, primaryKey } from 'drizzle-orm/sqlite-core'; 2 | import { sql } from 'drizzle-orm'; 3 | 4 | export const clipTagRelation = sqliteTable('dp_clip_tag_relation', { 5 | clip_key: text('clip_key').notNull(), 6 | tag_id: integer('tag_id', { mode: 'number' }), 7 | created_at: text('created_at') 8 | .notNull() 9 | .default(sql`CURRENT_TIMESTAMP`), 10 | updated_at: text('updated_at') 11 | .notNull() 12 | .default(sql`CURRENT_TIMESTAMP`) 13 | }, (table) => { 14 | return { 15 | pk: primaryKey({ columns: [table.clip_key, table.tag_id] }) 16 | }; 17 | }); 18 | 19 | 20 | export type ClipTagRelation = typeof clipTagRelation.$inferSelect; // return type when queried 21 | export type InsertClipTagRelation = typeof clipTagRelation.$inferInsert; // insert type 22 | -------------------------------------------------------------------------------- /src/backend/db/tables/dpTask.ts: -------------------------------------------------------------------------------- 1 | import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; 2 | import { sql } from 'drizzle-orm'; 3 | 4 | export enum DpTaskState { 5 | INIT = 'init', 6 | IN_PROGRESS = 'in_progress', 7 | DONE = 'done', 8 | CANCELLED = 'cancelled', 9 | FAILED = 'failed', 10 | } 11 | 12 | export const dpTask = sqliteTable('dp_task', { 13 | id: integer('id', { mode: 'number' }).primaryKey({ autoIncrement: true }), 14 | status: text('status').notNull().default(DpTaskState.INIT), 15 | progress: text('description').default('任务创建成功'), 16 | result: text('result'), 17 | created_at: text('created_at') 18 | .notNull() 19 | .default(sql`CURRENT_TIMESTAMP`), 20 | updated_at: text('updated_at') 21 | .notNull() 22 | .default(sql`CURRENT_TIMESTAMP`), 23 | }); 24 | 25 | export type DpTask = typeof dpTask.$inferSelect; // return type when queried 26 | export type InsertDpTask = typeof dpTask.$inferInsert; // insert type 27 | -------------------------------------------------------------------------------- /src/backend/db/tables/kvs.ts: -------------------------------------------------------------------------------- 1 | import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; 2 | import { sql } from 'drizzle-orm'; 3 | 4 | export const kvs = sqliteTable('dp_kvs', { 5 | id: integer('id', { mode: 'number' }).primaryKey({ autoIncrement: true }), 6 | key: text('key').notNull().unique(), 7 | value: text('value'), 8 | created_at: text('created_at') 9 | .notNull() 10 | .default(sql`CURRENT_TIMESTAMP`), 11 | updated_at: text('updated_at') 12 | .notNull() 13 | .default(sql`CURRENT_TIMESTAMP`), 14 | }); 15 | 16 | export type Kv = typeof kvs.$inferSelect; // return type when queried 17 | export type InsertKv = typeof kvs.$inferInsert; // insert type 18 | -------------------------------------------------------------------------------- /src/backend/db/tables/sentenceTranslates.ts: -------------------------------------------------------------------------------- 1 | import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; 2 | import { sql } from 'drizzle-orm'; 3 | 4 | export const sentenceTranslates = sqliteTable('dp_sentence_translates', { 5 | id: integer('id', { mode: 'number' }).primaryKey({ autoIncrement: true }), 6 | sentence: text('sentence').notNull().unique(), 7 | translate: text('translate'), 8 | created_at: text('created_at') 9 | .notNull() 10 | .default(sql`CURRENT_TIMESTAMP`), 11 | updated_at: text('updated_at') 12 | .notNull() 13 | .default(sql`CURRENT_TIMESTAMP`), 14 | }); 15 | 16 | export type SentenceTranslate = typeof sentenceTranslates.$inferSelect; // return type when queried 17 | export type InsertSentenceTranslate = typeof sentenceTranslates.$inferInsert; // insert type 18 | -------------------------------------------------------------------------------- /src/backend/db/tables/stems.ts: -------------------------------------------------------------------------------- 1 | import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; 2 | import { sql } from 'drizzle-orm'; 3 | 4 | export const stems = sqliteTable('dp_stems', { 5 | id: integer('id', { mode: 'number' }).primaryKey({ autoIncrement: true }), 6 | stem: text('stem').notNull().unique(), 7 | familiar: integer('familiar', { mode: 'boolean' }).notNull().default(false), 8 | created_at: text('created_at') 9 | .notNull() 10 | .default(sql`CURRENT_TIMESTAMP`), 11 | updated_at: text('updated_at') 12 | .notNull() 13 | .default(sql`CURRENT_TIMESTAMP`), 14 | }); 15 | 16 | export type Stem = typeof stems.$inferSelect; // return type when queried 17 | export type InsertStem = typeof stems.$inferInsert; // insert type 18 | -------------------------------------------------------------------------------- /src/backend/db/tables/subtitleTimestampAdjustment.ts: -------------------------------------------------------------------------------- 1 | import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; 2 | import { sql } from 'drizzle-orm'; 3 | 4 | export const subtitleTimestampAdjustments = sqliteTable( 5 | 'dp_subtitle_timestamp_adjustment', 6 | { 7 | id: integer('id', { mode: 'number' }).primaryKey({ 8 | autoIncrement: true, 9 | }), 10 | key: text('key').notNull().unique(), 11 | /** 12 | * @Deprecated 13 | */ 14 | subtitle_path: text('subtitle_name'), 15 | subtitle_hash: text('subtitle_hash'), 16 | start_at: integer('start_at'), 17 | end_at: integer('end_at'), 18 | created_at: text('created_at') 19 | .notNull() 20 | .default(sql`CURRENT_TIMESTAMP`), 21 | updated_at: text('updated_at') 22 | .notNull() 23 | .default(sql`CURRENT_TIMESTAMP`), 24 | } 25 | ); 26 | 27 | export type SubtitleTimestampAdjustment = 28 | typeof subtitleTimestampAdjustments.$inferSelect; // return type when queried 29 | export type InsertSubtitleTimestampAdjustment = 30 | typeof subtitleTimestampAdjustments.$inferInsert; // insert type 31 | -------------------------------------------------------------------------------- /src/backend/db/tables/tag.ts: -------------------------------------------------------------------------------- 1 | import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; 2 | import { sql } from 'drizzle-orm'; 3 | 4 | export const tag = sqliteTable('dp_tag', { 5 | id: integer('id', { mode: 'number' }).primaryKey({ autoIncrement: true }), 6 | name: text('name').notNull().unique(), 7 | created_at: text('created_at') 8 | .notNull() 9 | .default(sql`CURRENT_TIMESTAMP`), 10 | updated_at: text('updated_at') 11 | .notNull() 12 | .default(sql`CURRENT_TIMESTAMP`), 13 | }); 14 | 15 | export type Tag = typeof tag.$inferSelect; // return type when queried 16 | export type InsertTag = typeof tag.$inferInsert; // insert type 17 | -------------------------------------------------------------------------------- /src/backend/db/tables/videoClip.ts: -------------------------------------------------------------------------------- 1 | import { sqliteTable, text } from 'drizzle-orm/sqlite-core'; 2 | import { sql } from 'drizzle-orm'; 3 | 4 | export const videoClip = sqliteTable('dp_video_clip', { 5 | key: text('key').primaryKey(), 6 | video_name: text('video_name'), 7 | /** 8 | * 收藏的行 9 | */ 10 | srt_clip: text('srt_clip'), 11 | /** 12 | * 周围的字幕 13 | */ 14 | srt_context: text('srt_context'), 15 | created_at: text('created_at') 16 | .notNull() 17 | .default(sql`CURRENT_TIMESTAMP`), 18 | updated_at: text('updated_at') 19 | .notNull() 20 | .default(sql`CURRENT_TIMESTAMP`), 21 | }); 22 | 23 | export type VideoClip = typeof videoClip.$inferSelect; // return type when queried 24 | export type InsertVideoClip = typeof videoClip.$inferInsert; // insert type 25 | -------------------------------------------------------------------------------- /src/backend/db/tables/watchHistory.ts: -------------------------------------------------------------------------------- 1 | import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; 2 | import { sql } from 'drizzle-orm'; 3 | 4 | export const watchHistory = sqliteTable('dp_watch_history', { 5 | id: text('id').primaryKey(), 6 | base_path: text('project_name').notNull(), 7 | file_name: text('project_path').notNull(), 8 | project_type: integer('project_type', { mode: 'number' }).notNull(), 9 | current_position: integer('current_position').notNull().default(0), 10 | srt_file: text('srt_file'), 11 | created_at: text('created_at') 12 | .notNull() 13 | .default(sql`CURRENT_TIMESTAMP`), 14 | updated_at: text('updated_at') 15 | .notNull() 16 | .default(sql`CURRENT_TIMESTAMP`), 17 | }, (table) => ({ 18 | basePathFileNameIdx: index('base_path_file_name_idx').on(table.base_path, table.file_name), 19 | })); 20 | 21 | export type WatchHistory = typeof watchHistory.$inferSelect; // return type when queried 22 | export type InsertWatchHistory = typeof watchHistory.$inferInsert; // insert type 23 | export enum WatchHistoryType { 24 | FILE = 1, 25 | DIRECTORY = 2, 26 | } 27 | -------------------------------------------------------------------------------- /src/backend/db/tables/wordTranslates.ts: -------------------------------------------------------------------------------- 1 | import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; 2 | import { sql } from 'drizzle-orm'; 3 | 4 | export const wordTranslates = sqliteTable('dp_word_translates', { 5 | id: integer('id', { mode: 'number' }).primaryKey({ autoIncrement: true }), 6 | word: text('word').notNull().unique(), 7 | translate: text('translate'), 8 | created_at: text('created_at') 9 | .notNull() 10 | .default(sql`CURRENT_TIMESTAMP`), 11 | updated_at: text('updated_at') 12 | .notNull() 13 | .default(sql`CURRENT_TIMESTAMP`), 14 | }); 15 | 16 | export type WordTranslate = typeof wordTranslates.$inferSelect; // return type when queried 17 | export type InsertWordTranslate = typeof wordTranslates.$inferInsert; // insert type 18 | -------------------------------------------------------------------------------- /src/backend/db/tables/words.ts: -------------------------------------------------------------------------------- 1 | import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; 2 | import { sql } from 'drizzle-orm'; 3 | 4 | export const words = sqliteTable('dp_words', { 5 | id: integer('id', { mode: 'number' }).primaryKey({ autoIncrement: true }), 6 | word: text('word').notNull().unique(), 7 | stem: text('stem'), 8 | translate: text('translate'), 9 | note: text('note'), 10 | created_at: text('created_at') 11 | .notNull() 12 | .default(sql`CURRENT_TIMESTAMP`), 13 | updated_at: text('updated_at') 14 | .notNull() 15 | .default(sql`CURRENT_TIMESTAMP`), 16 | }); 17 | 18 | export type Word = typeof words.$inferSelect; // return type when queried 19 | export type InsertWord = typeof words.$inferInsert; // insert type 20 | -------------------------------------------------------------------------------- /src/backend/dispatcher.ts: -------------------------------------------------------------------------------- 1 | import Controller from '@/backend/interfaces/controller'; 2 | import container from '@/backend/ioc/inversify.config'; 3 | import TYPES from '@/backend/ioc/types'; 4 | import SystemService from '@/backend/services/SystemService'; 5 | import { BrowserWindow } from 'electron'; 6 | 7 | export default function registerHandler(mainWindowRef: { current: BrowserWindow | null }) { 8 | const controllerBeans = container.getAll(TYPES.Controller); 9 | controllerBeans.forEach((bean) => { 10 | bean.registerRoutes(); 11 | }); 12 | container.get(TYPES.SystemService).setMainWindow(mainWindowRef); 13 | } 14 | -------------------------------------------------------------------------------- /src/backend/errors/AssertionError.ts: -------------------------------------------------------------------------------- 1 | export default class AssertionError extends Error { 2 | constructor(message: string) { 3 | super(message); 4 | this.name = 'AssertionError'; 5 | Error.captureStackTrace(this, AssertionError); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/backend/errors/errors.ts: -------------------------------------------------------------------------------- 1 | import { ExtendableError } from 'ts-error'; 2 | 3 | /** 4 | * Whisper 相应格式错误 5 | */ 6 | export class WhisperResponseFormatError extends ExtendableError { 7 | } 8 | 9 | /** 10 | * 任务被用户取消 11 | */ 12 | export class CancelByUserError extends ExtendableError { 13 | } 14 | -------------------------------------------------------------------------------- /src/backend/interfaces/controller.ts: -------------------------------------------------------------------------------- 1 | export default interface Controller { 2 | registerRoutes(): void; 3 | } 4 | -------------------------------------------------------------------------------- /src/backend/interfaces/postConstruce.ts: -------------------------------------------------------------------------------- 1 | export default interface PostConstruct { 2 | postConstruct(): void; 3 | } 4 | -------------------------------------------------------------------------------- /src/backend/ioc/logger.ts: -------------------------------------------------------------------------------- 1 | import log from "electron-log/main"; 2 | import path from "path"; 3 | import LocationServiceImpl from '@/backend/services/impl/LocationServiceImpl'; 4 | import { LocationType } from '@/backend/services/LocationService'; 5 | import LocationUtil from '@/backend/utils/LocationUtil'; 6 | 7 | 8 | log.initialize({ preload: true }); 9 | 10 | 11 | const logPath = LocationUtil.staticGetStoragePath(LocationType.LOGS); 12 | 13 | log.transports.file.level = "info"; 14 | log.transports.file.resolvePathFn = () => 15 | path.join(logPath, "main.log"); 16 | log.errorHandler.startCatching(); 17 | const dpLog = log; 18 | 19 | export default dpLog; 20 | -------------------------------------------------------------------------------- /src/backend/ioc/types.ts: -------------------------------------------------------------------------------- 1 | const TYPES = { 2 | ClipOssService: Symbol('ClipOssService'), 3 | FavouriteClips: Symbol('FavouriteClips'), 4 | Controller: Symbol('Controller'), 5 | WatchProject: Symbol('WatchProject'), 6 | DlVideo: Symbol('DlVideo'), 7 | TagService: Symbol('TagService'), 8 | SubtitleService: Symbol('SubtitleService'), 9 | SrtTimeAdjustService: Symbol('SubtitleTimestampAdjustmentService'), 10 | SystemService: Symbol('SystemService'), 11 | CacheService: Symbol('CacheService'), 12 | SettingService: Symbol('SettingService'), 13 | LocationService: Symbol('LocationService'), 14 | DpTaskService: Symbol('DpTaskService'), 15 | AiService: Symbol('AiService'), 16 | ChatService: Symbol('ChatService'), 17 | FfmpegService: Symbol('FfmpegService'), 18 | SplitVideoService: Symbol('SplitVideoService'), 19 | WhisperService: Symbol('WhisperService'), 20 | ConvertService: Symbol('ConvertService'), 21 | MediaService: Symbol('MediaService'), 22 | TranslateService: Symbol('TranslateService'), 23 | WatchHistoryService: Symbol('WatchHistoryService'), 24 | OpenAiService: Symbol('OpenAiService'), 25 | // Clients 26 | YouDaoClientProvider: Symbol('YouDaoClientProvider'), 27 | TencentClientProvider: Symbol('TencentClientProvider'), 28 | AiProviderService: Symbol('AiProviderService'), 29 | }; 30 | 31 | export default TYPES; 32 | -------------------------------------------------------------------------------- /src/backend/objs/ChildProcessTask.ts: -------------------------------------------------------------------------------- 1 | import { Cancelable } from '@/common/interfaces'; 2 | import { ChildProcess } from 'child_process'; 3 | 4 | export default class ChildProcessTask implements Cancelable { 5 | private readonly process: ChildProcess; 6 | 7 | constructor(process: ChildProcess) { 8 | this.process = process; 9 | } 10 | 11 | cancel(): void { 12 | this.process.kill(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/backend/objs/FfmpegTask.ts: -------------------------------------------------------------------------------- 1 | import { Cancelable } from '@/common/interfaces'; 2 | import Ffmpeg from 'fluent-ffmpeg'; 3 | 4 | export default class FfmpegTask implements Cancelable { 5 | private readonly command: Ffmpeg.FfmpegCommand; 6 | 7 | constructor(command: Ffmpeg.FfmpegCommand) { 8 | this.command = command; 9 | } 10 | 11 | cancel(): void { 12 | this.command.kill('SIGKILL'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/backend/objs/config-tender.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | 5 | /** 6 | * 配置文件托管类 7 | * @template T 配置类型 8 | * @template S Zod Schema 类型 9 | */ 10 | export class ConfigTender> { 11 | private readonly configPath: string; 12 | private readonly schema: S; 13 | 14 | constructor(configPath: string, schema: S, defaultValue?: T) { 15 | this.configPath = configPath; 16 | this.schema = schema; 17 | 18 | // 确保目录存在 19 | const dir = path.dirname(configPath); 20 | if (!fs.existsSync(dir)) { 21 | fs.mkdirSync(dir, { recursive: true }); 22 | } 23 | 24 | // 如果文件不存在且提供了默认值,则创建文件 25 | if (!fs.existsSync(configPath) && defaultValue) { 26 | this.save(defaultValue); 27 | } 28 | } 29 | 30 | /** 31 | * 读取整个配置 32 | */ 33 | get(): T { 34 | try { 35 | const content = fs.readFileSync(this.configPath, 'utf-8'); 36 | const parsed = JSON.parse(content); 37 | return this.schema.parse(parsed); 38 | } catch (error) { 39 | throw new Error(`Failed to read config: ${error}`); 40 | } 41 | } 42 | 43 | /** 44 | * 保存整个配置 45 | */ 46 | save(config: T): void { 47 | try { 48 | const validated = this.schema.parse(config); 49 | fs.writeFileSync(this.configPath, JSON.stringify(validated, null, 2)); 50 | } catch (error) { 51 | throw new Error(`Failed to save config: ${error}`); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/backend/services/AiProviderService.ts: -------------------------------------------------------------------------------- 1 | import { LanguageModelV1 } from 'ai'; 2 | 3 | export default interface AiProviderService { 4 | getModel(): LanguageModelV1 | null; 5 | } 6 | -------------------------------------------------------------------------------- /src/backend/services/AiTransServiceImpl.ts: -------------------------------------------------------------------------------- 1 | import { YdRes } from '@/common/types/YdRes'; 2 | 3 | export default interface TranslateService { 4 | transWord(str: string): Promise; 5 | transSentences(sentences: string[]): Promise>; 6 | } 7 | 8 | -------------------------------------------------------------------------------- /src/backend/services/CacheService.ts: -------------------------------------------------------------------------------- 1 | import { SrtSentence } from '@/common/types/SentenceC'; 2 | 3 | export type CacheType ={ 4 | 'cache:srt': SrtSentence; 5 | } 6 | 7 | export default interface CacheService { 8 | get(type: T, key: string): CacheType[T] | null; 9 | set(type: T, key: string, value: CacheType[T]): void; 10 | delete(type: T, key: string): void; 11 | clear(): void; 12 | } 13 | -------------------------------------------------------------------------------- /src/backend/services/ChatService.ts: -------------------------------------------------------------------------------- 1 | import { ZodObject } from 'zod'; 2 | import { CoreMessage } from 'ai'; 3 | 4 | export default interface ChatService { 5 | chat(taskId: number, msgs: CoreMessage[]): Promise; 6 | run(taskId: number, resultSchema: ZodObject, promptStr: string): Promise; 7 | } 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/backend/services/CheckUpdate.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { app } from 'electron'; 3 | import { compareVersions } from 'compare-versions'; 4 | import Release from "@/common/types/release"; 5 | 6 | let cache: Release[] = []; 7 | let cacheUpdateTime = 0; 8 | 9 | export const checkUpdate = async (): Promise => { 10 | const now = Date.now(); 11 | // 5分钟内不重复检查更新 12 | if (now - cacheUpdateTime < 5 * 60 * 1000) { 13 | return cache; 14 | } 15 | 16 | const currentVersion = app.getVersion(); 17 | 18 | const result = await axios 19 | .get( 20 | 'https://api.github.com/repos/solidSpoon/DashPlayer/releases' 21 | ) 22 | .catch((err) => { 23 | console.error(err); 24 | return null; 25 | }); 26 | 27 | if (result?.status !== 200) { 28 | return []; 29 | } 30 | 31 | const releases: Release[] = result.data.map((release: { 32 | html_url: string; 33 | tag_name: string; 34 | body: string; 35 | }) => ({ 36 | url: release.html_url, 37 | version: release.tag_name, 38 | content: release.body, 39 | })); 40 | console.log('releases', releases); 41 | cache = releases 42 | .filter(release => compareVersions(release.version, `v${currentVersion}`) > 0) 43 | .sort((a, b) => compareVersions(b.version, a.version)); 44 | cacheUpdateTime = now; 45 | return cache; 46 | }; 47 | 48 | export const appVersion = (): string => { 49 | return app.getVersion(); 50 | }; 51 | -------------------------------------------------------------------------------- /src/backend/services/ClientProviderService.ts: -------------------------------------------------------------------------------- 1 | export default interface ClientProviderService { 2 | getClient(): T | null; 3 | } 4 | -------------------------------------------------------------------------------- /src/backend/services/ConvertService.ts: -------------------------------------------------------------------------------- 1 | import { FolderVideos } from '@/common/types/tonvert-type'; 2 | 3 | 4 | export default interface ConvertService { 5 | toMp4(taskId: number, file: string): Promise; 6 | 7 | fromFolder(folders: string[]): Promise; 8 | } 9 | 10 | -------------------------------------------------------------------------------- /src/backend/services/DlVideoService.ts: -------------------------------------------------------------------------------- 1 | export default interface DlVideoService { 2 | dlVideo(taskId: number, url: string,cookies: string, savePath: string): Promise; 3 | } 4 | -------------------------------------------------------------------------------- /src/backend/services/DpTaskService.ts: -------------------------------------------------------------------------------- 1 | import { DpTask, InsertDpTask } from '@/backend/db/tables/dpTask'; 2 | import { Cancelable } from '@/common/interfaces'; 3 | 4 | 5 | export default interface DpTaskService { 6 | detail(id: number): Promise; 7 | 8 | details(ids: number[]): Promise>; 9 | 10 | create(): Promise; 11 | 12 | update(task: InsertDpTask): void; 13 | 14 | process(id: number, info: InsertDpTask): void; 15 | 16 | finish(id: number, info: InsertDpTask): void; 17 | 18 | fail(id: number, info: InsertDpTask): void; 19 | 20 | cancel(id: number): void; 21 | 22 | checkCancel(id: number): void; 23 | 24 | cancelAll(): Promise; 25 | 26 | registerTask(taskId: number, process: Cancelable): void; 27 | } 28 | -------------------------------------------------------------------------------- /src/backend/services/LocationService.ts: -------------------------------------------------------------------------------- 1 | export enum LocationType { 2 | FAVORITE_CLIPS = 'favorite_clips', 3 | TEMP = 'temp', 4 | LOGS = 'logs', 5 | VIDEOS = 'videos', 6 | TEMP_OSS = 'temp_oss', 7 | DATA = 'data', 8 | } 9 | 10 | export enum ProgramType { 11 | FFMPEG = 'ffmpeg', 12 | FFPROBE = 'ffprobe', 13 | YT_DL = 'yt-dlp', 14 | LIB = 'lib', 15 | } 16 | 17 | export default interface LocationService { 18 | getDetailLibraryPath(type: LocationType): string; 19 | 20 | getBaseLibraryPath(): string; 21 | 22 | getBaseClipPath(): string; 23 | 24 | getThirdLibPath(type: ProgramType): string; 25 | 26 | listCollectionPaths(): string[]; 27 | } 28 | -------------------------------------------------------------------------------- /src/backend/services/MediaService.ts: -------------------------------------------------------------------------------- 1 | export default interface MediaService { 2 | thumbnail(inputFile: string, time?: number): Promise; 3 | duration(inputFile: string): Promise; 4 | } 5 | -------------------------------------------------------------------------------- /src/backend/services/OpenAiService.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from 'openai'; 2 | 3 | export interface OpenAiService { 4 | getOpenAi(): OpenAI; 5 | } 6 | -------------------------------------------------------------------------------- /src/backend/services/OssService.ts: -------------------------------------------------------------------------------- 1 | import { ClipMeta, OssBaseMeta } from '@/common/types/clipMeta'; 2 | 3 | 4 | export interface ClipOssService extends OssService { 5 | 6 | putClip(key: string, sourcePath: string, metadata: ClipMeta): Promise; 7 | 8 | updateTags(key: string, tags: string[]): Promise; 9 | } 10 | 11 | export interface OssService { 12 | putFile(key: string, fileName: string, sourcePath: string): Promise; 13 | 14 | delete(key: string): Promise; 15 | 16 | get(key: string): Promise 17 | 18 | updateMetadata(key: string, newMetadata: Partial): Promise; 19 | 20 | list(): Promise; 21 | } 22 | -------------------------------------------------------------------------------- /src/backend/services/ScheduleServiceImpl.ts: -------------------------------------------------------------------------------- 1 | import { injectable, postConstruct } from 'inversify'; 2 | import dpLog from '@/backend/ioc/logger'; 3 | 4 | 5 | @injectable() 6 | export class ScheduleServiceImpl { 7 | @postConstruct() 8 | init() { 9 | dpLog.info('ScheduleServiceImpl init'); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/backend/services/SettingService.ts: -------------------------------------------------------------------------------- 1 | import { SettingKey } from '@/common/types/store_schema'; 2 | 3 | export default interface SettingService { 4 | set(key: SettingKey, value: string): Promise; 5 | get(key: SettingKey): Promise; 6 | } 7 | -------------------------------------------------------------------------------- /src/backend/services/SplitVideoService.ts: -------------------------------------------------------------------------------- 1 | 2 | import { ChapterParseResult } from '@/common/types/chapter-result'; 3 | 4 | 5 | 6 | export default interface SplitVideoService { 7 | previewSplit(str: string): Promise; 8 | 9 | splitByChapters({ 10 | videoPath, 11 | srtPath, 12 | chapters 13 | }: { 14 | videoPath: string, 15 | srtPath: string | null, 16 | chapters: ChapterParseResult[] 17 | }): Promise; 18 | } 19 | -------------------------------------------------------------------------------- /src/backend/services/SrtTimeAdjustService.ts: -------------------------------------------------------------------------------- 1 | import { 2 | InsertSubtitleTimestampAdjustment, 3 | SubtitleTimestampAdjustment 4 | } from '@/backend/db/tables/subtitleTimestampAdjustment'; 5 | 6 | /** 7 | * 调整字幕时间 8 | */ 9 | export default interface SrtTimeAdjustService { 10 | 11 | /** 12 | * 记录调整 13 | * @param e 14 | */ 15 | record(e: InsertSubtitleTimestampAdjustment): Promise; 16 | 17 | /** 18 | * 删除调整 19 | * @param key 20 | */ 21 | deleteByKey(key: string): Promise; 22 | 23 | /** 24 | * 删除调整 25 | * @param fileHash 26 | */ 27 | deleteByFile(fileHash: string): Promise; 28 | 29 | /** 30 | * 获取调整 31 | * @param key 32 | */ 33 | getByKey(key: string): Promise; 34 | 35 | /** 36 | * 获取调整 37 | * @param subtitlePath 38 | */ 39 | getByPath(subtitlePath: string): Promise; 40 | 41 | /** 42 | * 获取调整 43 | * @param h 44 | */ 45 | getByHash(h: string): Promise; 46 | } 47 | -------------------------------------------------------------------------------- /src/backend/services/SubtitleService.ts: -------------------------------------------------------------------------------- 1 | import { SrtSentence } from '@/common/types/SentenceC'; 2 | 3 | export default interface SubtitleService { 4 | parseSrt(path: string): Promise; 5 | } 6 | -------------------------------------------------------------------------------- /src/backend/services/SystemService.ts: -------------------------------------------------------------------------------- 1 | import { WindowState } from '@/common/types/Types'; 2 | import { BrowserWindow } from 'electron'; 3 | 4 | /** 5 | * SystemService 6 | */ 7 | export default interface SystemService { 8 | 9 | changeWindowSize(state: WindowState): void; 10 | 11 | windowState(): WindowState; 12 | 13 | isWindows(): boolean; 14 | 15 | sendErrorToRenderer(error: Error): void; 16 | 17 | sendInfoToRenderer(info: string): void; 18 | 19 | mainWindow(): Electron.BrowserWindow; 20 | 21 | setMainWindow(mainWindowRef: { current: BrowserWindow | null }): void; 22 | } 23 | -------------------------------------------------------------------------------- /src/backend/services/TagService.ts: -------------------------------------------------------------------------------- 1 | import { Tag } from '@/backend/db/tables/tag'; 2 | 3 | export default interface TagService { 4 | addTag(name: string): Promise; 5 | 6 | deleteTag(id: number): Promise; 7 | 8 | updateTag(id: number, name: string): Promise; 9 | 10 | search(keyword: string): Promise; 11 | } 12 | -------------------------------------------------------------------------------- /src/backend/services/WatchHistoryService.ts: -------------------------------------------------------------------------------- 1 | import WatchHistoryVO from '@/common/types/WatchHistoryVO'; 2 | 3 | interface WatchHistoryService { 4 | list(basePath: string): Promise; 5 | 6 | detail(folder: string): Promise; 7 | 8 | /** 9 | * 添加媒体文件 10 | * @param filePaths 路径列表,可以是文件或文件夹 11 | */ 12 | create(filePaths: string[]): Promise; 13 | 14 | updateProgress(file: string, currentPosition: number): Promise; 15 | 16 | attachSrt(videoPath: string, srtPath: string): Promise; 17 | 18 | groupDelete(id: string): Promise; 19 | 20 | analyseFolder(path: string): Promise<{ supported: number, unsupported: number }>; 21 | 22 | /** 23 | * 获取推荐的字幕文件 24 | * @param file 视频文件路径 25 | */ 26 | suggestSrt(file: string): Promise; 27 | } 28 | 29 | export default WatchHistoryService; 30 | 31 | -------------------------------------------------------------------------------- /src/backend/services/WhisperService.ts: -------------------------------------------------------------------------------- 1 | export default interface WhisperService { 2 | transcript(taskId: number, filePath: string): Promise; 3 | } 4 | -------------------------------------------------------------------------------- /src/backend/services/impl/CacheService.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify'; 2 | import CacheService, { CacheType } from '@/backend/services/CacheService'; 3 | 4 | 5 | @injectable() 6 | export class CacheServiceImpl implements CacheService { 7 | private cache: Map = new Map(); 8 | 9 | get(type: T, key: string): CacheType[T] | null { 10 | const value = this.cache.get(this.mapKey(type, key)); 11 | if (value === undefined || value === null) { 12 | return null; 13 | } 14 | return JSON.parse(value) as CacheType[T]; 15 | } 16 | 17 | set(type: T, key: string, value: CacheType[T]): void { 18 | if (value === null) { 19 | return; 20 | } 21 | const value1: string = JSON.stringify(value); 22 | this.cache.set(this.mapKey(type, key), value1); 23 | } 24 | 25 | 26 | delete(type: T, key: string): void { 27 | this.cache.delete(this.mapKey(type, key)); 28 | } 29 | 30 | clear(): void { 31 | this.cache.clear(); 32 | } 33 | 34 | private mapKey(type: string, key: string) { 35 | return type + '::=::' + key; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/backend/services/impl/OpenAIServiceImpl.ts: -------------------------------------------------------------------------------- 1 | import { OpenAiService } from '@/backend/services/OpenAiService'; 2 | import OpenAI from 'openai'; 3 | import { injectable } from 'inversify'; 4 | import { storeGet } from '@/backend/store'; 5 | import StrUtil from '@/common/utils/str-util'; 6 | import fs from "fs"; 7 | import { TranscriptionVerbose } from 'openai/src/resources/audio/transcriptions'; 8 | @injectable() 9 | export class OpenAIServiceImpl implements OpenAiService { 10 | private openai: OpenAI | null = null; 11 | private apiKey: string | null = null; 12 | private endpoint: string | null = null; 13 | 14 | 15 | public getOpenAi(): OpenAI { 16 | const ak = storeGet('apiKeys.openAi.key'); 17 | const ep = storeGet('apiKeys.openAi.endpoint'); 18 | if (StrUtil.hasBlank(ak, ep)) { 19 | throw new Error('未设置 OpenAI 密钥'); 20 | } 21 | if (this.openai && this.apiKey === ak && this.endpoint === ep) { 22 | return this.openai; 23 | } 24 | this.apiKey = ak; 25 | this.endpoint = ep; 26 | this.openai = new OpenAI({ 27 | baseURL: ep + '/v1', 28 | apiKey: ak 29 | }); 30 | return this.openai; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/backend/services/impl/SettingServiceImpl.ts: -------------------------------------------------------------------------------- 1 | import { SettingKey } from '@/common/types/store_schema'; 2 | import { storeGet, storeSet } from '../../store'; 3 | import SystemService from '@/backend/services/SystemService'; 4 | import { inject, injectable } from 'inversify'; 5 | import TYPES from '@/backend/ioc/types'; 6 | import SettingService from '@/backend/services/SettingService'; 7 | 8 | 9 | 10 | @injectable() 11 | export default class SettingServiceImpl implements SettingService { 12 | @inject(TYPES.SystemService) private systemService!: SystemService; 13 | public async set(key: SettingKey, value: string): Promise { 14 | if (storeSet(key, value)) { 15 | this.systemService.mainWindow()?.webContents.send('store-update', key, value); 16 | } 17 | } 18 | 19 | public async get(key: SettingKey): Promise { 20 | return storeGet(key); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/backend/services/impl/TagServiceImpl.ts: -------------------------------------------------------------------------------- 1 | import { tag, Tag } from '@/backend/db/tables/tag'; 2 | import { injectable } from 'inversify'; 3 | import db from '@/backend/db'; 4 | import { eq, like } from 'drizzle-orm'; 5 | import StrUtil from '@/common/utils/str-util'; 6 | import TagService from '@/backend/services/TagService'; 7 | 8 | 9 | @injectable() 10 | export default class TagServiceImpl implements TagService { 11 | public async addTag(name: string): Promise { 12 | if (StrUtil.isBlank(name)) { 13 | throw new Error('name is blank'); 14 | } 15 | const e: Tag[] = await db.insert(tag).values({ name }) 16 | .onConflictDoUpdate({ 17 | target: [tag.name], 18 | set: { name } 19 | }) 20 | .returning(); 21 | return e[0]; 22 | } 23 | 24 | public async deleteTag(id: number): Promise { 25 | await db.delete(tag).where(eq(tag.id, id)); 26 | } 27 | 28 | public async updateTag(id: number, name: string): Promise { 29 | await db.update(tag).set({ name }).where(eq(tag.id, id)); 30 | } 31 | 32 | public async search(keyword: string): Promise { 33 | return db.select().from(tag) 34 | .where(like(tag.name, `${keyword}%`)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/backend/services/impl/clients/AiProviderServiceImpl.ts: -------------------------------------------------------------------------------- 1 | import { storeGet } from '@/backend/store'; 2 | import StrUtil from '@/common/utils/str-util'; 3 | import { joinUrl } from '@/common/utils/Util'; 4 | import { injectable } from 'inversify'; 5 | import AiProviderService from '@/backend/services/AiProviderService'; 6 | import { createOpenAI } from '@ai-sdk/openai'; 7 | import { LanguageModelV1 } from 'ai'; 8 | 9 | 10 | @injectable() 11 | export default class AiProviderServiceImpl implements AiProviderService { 12 | 13 | public getModel():LanguageModelV1 | null { 14 | const apiKey = storeGet('apiKeys.openAi.key'); 15 | const endpoint = storeGet('apiKeys.openAi.endpoint'); 16 | let model = storeGet('model.gpt.default'); 17 | if (StrUtil.isBlank(model)) { 18 | model = 'gpt-4o-mini'; 19 | } 20 | if (StrUtil.hasBlank(apiKey, endpoint)) { 21 | return null; 22 | } 23 | const openai = createOpenAI({ 24 | compatibility: 'compatible', 25 | baseURL: joinUrl(endpoint, '/v1'), 26 | apiKey: apiKey 27 | }); 28 | return openai(model); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/backend/services/impl/clients/TencentProvider.ts: -------------------------------------------------------------------------------- 1 | import { storeGet } from '@/backend/store'; 2 | import { injectable } from 'inversify'; 3 | import ClientProviderService from '@/backend/services/ClientProviderService'; 4 | import StrUtil from '@/common/utils/str-util'; 5 | import TencentClient from '@/backend/objs/TencentClient'; 6 | 7 | 8 | @injectable() 9 | export default class TencentProvider implements ClientProviderService { 10 | private client: TencentClient | null = null; 11 | private localSecretId: string | null = null; 12 | private localSecretKey: string | null = null; 13 | 14 | 15 | private initClient(): void { 16 | if (StrUtil.isBlank(this.localSecretId) || StrUtil.isBlank(this.localSecretKey)) { 17 | return; 18 | } 19 | const clientConfig = { 20 | credential: { 21 | secretId: this.localSecretId, 22 | secretKey: this.localSecretKey 23 | }, 24 | region: 'ap-shanghai' 25 | }; 26 | this.client = new TencentClient(clientConfig); 27 | } 28 | 29 | public getClient(): TencentClient | null { 30 | const secretId = storeGet('apiKeys.tencent.secretId'); 31 | const secretKey = storeGet('apiKeys.tencent.secretKey'); 32 | if (StrUtil.hasBlank(secretId, secretKey)) { 33 | return null; 34 | } 35 | if (this.localSecretId === secretId && this.localSecretKey === secretKey && this.client) { 36 | return this.client; 37 | } 38 | this.localSecretId = secretId; 39 | this.localSecretKey = secretKey; 40 | this.initClient(); 41 | return this.client; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/backend/services/impl/clients/YouDaoProvider.ts: -------------------------------------------------------------------------------- 1 | import { storeGet } from '@/backend/store'; 2 | import { injectable } from 'inversify'; 3 | import ClientProviderService from '@/backend/services/ClientProviderService'; 4 | import StrUtil from '@/common/utils/str-util'; 5 | import YouDaoClient, { YouDaoConfig } from '@/backend/objs/YouDaoClient'; 6 | 7 | 8 | @injectable() 9 | export default class YouDaoProvider implements ClientProviderService { 10 | private youDao = new YouDaoClient({ 11 | from: 'zh_CHS', // zh-CHS(中文) || ja(日语) || EN(英文) || fr(法语) ... 12 | to: 'EN', 13 | appKey: '', // https://ai.youdao.com 在有道云上进行注册 14 | secretKey: '' 15 | }); 16 | 17 | public getClient(): YouDaoClient | null { 18 | const secretId = storeGet('apiKeys.youdao.secretId'); 19 | const secretKey = storeGet('apiKeys.youdao.secretKey'); 20 | if (StrUtil.hasBlank(secretId, secretKey)) { 21 | return null; 22 | } 23 | const c: YouDaoConfig = { 24 | from: 'zh_CHS', // zh-CHS(中文) || ja(日语) || EN(英文) || fr(法语) ... 25 | to: 'EN', 26 | appKey: secretId, 27 | secretKey 28 | }; 29 | this.youDao.updateConfig(c); 30 | return this.youDao; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/backend/services/prompts/analyze-grammer.ts: -------------------------------------------------------------------------------- 1 | const analyzeGrammarPrompt = (s:string):string => 2 | `You are a professional grammar analyzer, your job is helping Chinese people learn English grammar. 3 | Please explain the grammar of the following sentence using Chinese 4 | 5 | """ 6 | ${s} 7 | """ 8 | ` 9 | 10 | 11 | export default analyzeGrammarPrompt; 12 | -------------------------------------------------------------------------------- /src/backend/services/prompts/analyze-phrases.ts: -------------------------------------------------------------------------------- 1 | const analyzePhrasesPrompt = (s:string):string => 2 | `你正在帮助用户学习英语。从句子中找出词组/短语/固定搭配, 并给出对应的中文翻译. 3 | 4 | ${s} 5 | ` 6 | export default analyzePhrasesPrompt; 7 | -------------------------------------------------------------------------------- /src/backend/services/prompts/analyze-word.ts: -------------------------------------------------------------------------------- 1 | const analyzeWordsPrompt = (s:string):string => 2 | `从句子中找出中等英文水平的人可能不懂的单词, 并给出对应的中文翻译. 3 | 4 | ${s} 5 | ` 6 | export default analyzeWordsPrompt; 7 | -------------------------------------------------------------------------------- /src/backend/services/prompts/example-sentence.ts: -------------------------------------------------------------------------------- 1 | const exampleSentences = (ps: string[]) :string=> 2 | `你现在是一个英语学习程序,我会给你一些知识点, 你需要根据这些知识点来生成 5 个例句, 来帮助用户理解知识点 3 | 例句中的单词应该中等难度, 不应该太难或太简单, 你需要确保例句的语法正确 . 4 | 5 | 注意, 你必须生成 5 个例句 6 | 7 | 知识点: 8 | ${ps.join('\n')} 9 | ` 10 | export default exampleSentences; 11 | -------------------------------------------------------------------------------- /src/backend/services/prompts/phraseGroupPropmt.ts: -------------------------------------------------------------------------------- 1 | const phraseGroupPrompt = (s: string): string => ` 2 | 分析下面三个单引号包裹的英文句子的意群,最好在 comment 字段指出意群在句子中的作用(主语 谓语 宾语 等) 3 | 4 | ''' 5 | ${s} 6 | ''' 7 | ` 8 | 9 | export default phraseGroupPrompt; 10 | -------------------------------------------------------------------------------- /src/backend/services/prompts/prompt-punctuation.ts: -------------------------------------------------------------------------------- 1 | // const schema = z.object({ 2 | // sentences: z.object({ 3 | // break: z.boolean().describe("Whether the sentence is broken into multiple lines"), 4 | // sentence: z.string().describe("The complete sentence"), 5 | // }).describe("Analyse whether the sentence is broken into multiple lines and return the complete sentence"), 6 | // }); 7 | 8 | // const promptPunctuation = 9 | // `你现在是一个英语学习播放器, 播放器当前字幕可能被换行打断成多行, 不利于用户理解, 我把当前字幕行和它的上下文给你, 请你分析一下这个句子, 看看这个句子是否被换行打断成多行, 如果是, 请你把这个句子合并成一个完整的句子. 10 | // 为了避免混淆, 我会将上下文和当前句子用三个单引号包裹 11 | // 12 | // 上下文:''' 13 | // {context} 14 | // ''' 15 | // 16 | // 这个句子是:''' 17 | // {sentence} 18 | // ''' 19 | // 20 | // 请你判断这个句子是否被换行打断成多行, 如果是, 请你把这个句子合并成一个完整的句子. 21 | // ` 22 | 23 | // const promptPunctuation = ` 24 | // {time} 25 | // A complete sentence may be broken into multiple lines due to various reasons. 26 | // Given the isolated sentence below, use context to determine if it is a subpart of a complete sentence. try to provide a complete version of the sentence by combining the isolated sentence with the context. 27 | // Remember to consider the grammatical structure and the meaning conveyed by the words in the context and the sentence. 28 | // 29 | // sentence''' 30 | // {sentence} 31 | // ''' 32 | // 33 | // context''' 34 | // {context} 35 | // ''' 36 | // ` 37 | 38 | 39 | const promptPunctuation = ` 40 | '''srt 41 | {srt} 42 | ''' 43 | 44 | {sentence}这行是完整的吗? 如果不是, 完整的句子是什么? 45 | ` 46 | export default promptPunctuation; 47 | -------------------------------------------------------------------------------- /src/backend/services/prompts/prompt.ts: -------------------------------------------------------------------------------- 1 | export const mainPrompt = 2 | `你现在是一个播放器软件中的,英语学习程序,你的任务是帮助具有中等英文水平且母语是中文的人理解英语字幕,字幕中的生词、涉及到的语法。由于字幕可能在句子中间换行,为了帮助你分析,我会把当前行的上下文也一同发给你,你需要依次做下面几件事情。 3 | 4 | - 根据上下文润色当前行 5 | - 翻译这个句子 6 | - 标记生词和短语 7 | - 分析语法 8 | 9 | 由于你是一个程序,所以当被要求分析句子时,必须严格按照以下md格式返回: 10 | 11 | <格式示例> 12 | ## 英文: 13 | !todo 14 | ## 翻译: 15 | !todo 16 | ## 生词|短语: 17 | !toto in list 18 | ## 语法: 19 | !todo 20 | 21 | 22 | 下面,请分析下面内容 23 | <请分析> 24 | <上下文> 25 | {ctx} 26 | 27 | <当前行> 28 | {s} 29 | 30 | ` 31 | -------------------------------------------------------------------------------- /src/backend/services/prompts/synonymous-sentence.ts: -------------------------------------------------------------------------------- 1 | const synonymousSentence = (s: string): string => 2 | `请写出下面由三个单引号包裹的那句英文的三个同义句 3 | ''' 4 | ${s} 5 | ''' 6 | ` 7 | export default synonymousSentence; 8 | -------------------------------------------------------------------------------- /src/backend/store.ts: -------------------------------------------------------------------------------- 1 | import Store from 'electron-store'; 2 | import {SettingKey, SettingKeyObj} from "@/common/types/store_schema"; 3 | import StrUtil from '@/common/utils/str-util'; 4 | 5 | 6 | const store = new Store(); 7 | 8 | export const storeSet = (key: SettingKey, value: string | undefined | null): boolean => { 9 | if (StrUtil.isBlank(value)) { 10 | value = SettingKeyObj[key]; 11 | } 12 | const oldValue = store.get(key, SettingKeyObj[key]); 13 | if (oldValue === value) { 14 | return false; 15 | } 16 | store.set(key, value); 17 | return true; 18 | }; 19 | 20 | export const storeGet = (key: SettingKey): string => { 21 | return store.get(key, SettingKeyObj[key]) as string; 22 | } 23 | 24 | -------------------------------------------------------------------------------- /src/backend/test/sentence.test.ts: -------------------------------------------------------------------------------- 1 | // sum.test.js 2 | import { expect, test } from 'vitest' 3 | 4 | // import { processSentence } from '@/backend/services/SubtitleService'; 5 | // test("i'm a boy", () => { 6 | // let sentenceStruct = processSentence("I think that's a good idea."); 7 | // console.log(JSON.stringify(sentenceStruct, null, 2)); 8 | // }) 9 | // test("i'm a boy2", () => { 10 | // const sentenceStruct = processSentence("Orson don't cramp the boy's style"); 11 | // console.log(JSON.stringify(sentenceStruct, null, 2)); 12 | // }) 13 | // test("i'm a boy2", () => { 14 | // const sentenceStruct = processSentence("Have you enjoyed working here at mrs.van de kamp's"); 15 | // console.log(JSON.stringify(sentenceStruct, null, 2)); 16 | // }) 17 | -------------------------------------------------------------------------------- /src/backend/test/whisper.test.ts: -------------------------------------------------------------------------------- 1 | // import { expect, test } from 'vitest' 2 | // import path from 'path'; 3 | // import OpenAI from 'openai'; 4 | // import fs from 'fs'; 5 | // const openai = new OpenAI({ 6 | // apiKey: "sk-oxTSqN28uqFTRfJy515b24Dd06Be45Ca9c3071757862882d", 7 | // baseURL: "https://oneapi.gptnb.me/v1/", 8 | // }); 9 | // test("whisper", async () => { 10 | // const filePath = path.resolve(__dirname, "test.mp3"); 11 | // console.log('transcript', filePath); 12 | // const readStream = fs.createReadStream(filePath); 13 | // console.log(readStream) 14 | // const transcription = await openai.audio.transcriptions.create({ 15 | // file: readStream, 16 | // model: "whisper-1", 17 | // response_format: 'srt' 18 | // }); 19 | // console.log('transcription.text'); 20 | // console.log(transcription); 21 | // }) 22 | -------------------------------------------------------------------------------- /src/backend/utils/LocationUtil.ts: -------------------------------------------------------------------------------- 1 | import { LocationType } from '@/backend/services/LocationService'; 2 | import path from 'path'; 3 | import StrUtil from '@/common/utils/str-util'; 4 | import { storeGet, storeSet } from '@/backend/store'; 5 | import { app } from 'electron'; 6 | 7 | export default class LocationUtil { 8 | private static readonly isDev = process.env.NODE_ENV === 'development'; 9 | 10 | public static staticGetStoragePath(type: LocationType | string) { 11 | const basePath = this.getStorageBathPath(); 12 | return path.join(basePath, type); 13 | } 14 | 15 | public static getStorageBathPath() { 16 | let p = storeGet('storage.path'); 17 | if (StrUtil.isBlank(p)) { 18 | const documentsPath = app.getPath('documents'); 19 | const folderName = 'DashPlayer'; 20 | p = path.join(documentsPath, folderName); 21 | storeSet('storage.path', p); 22 | } 23 | // 如果是开发环境,确保路径末尾有 "dev" 后缀 24 | // 如果是生产环境,确保路径末尾没有 "dev" 后缀 25 | const dirName = path.basename(p); 26 | const parentDir = path.dirname(p); 27 | let newDirName = dirName; 28 | if (this.isDev && !dirName.endsWith('-dev')) { 29 | newDirName += '-dev'; 30 | } else if (!this.isDev && dirName.endsWith('-dev')) { 31 | newDirName = dirName.slice(0, -3); 32 | } 33 | return path.join(parentDir, newDirName); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/backend/utils/ObjUtil.ts: -------------------------------------------------------------------------------- 1 | import objectHash from 'object-hash'; 2 | import StrUtil from '@/common/utils/str-util'; 3 | import { Nullable } from '@/common/types/Types'; 4 | 5 | export class ObjUtil { 6 | public static hash(obj: objectHash.NotUndefined): string { 7 | return objectHash(obj); 8 | } 9 | 10 | public static isNull(obj: T | null | undefined): obj is null | undefined { 11 | return obj === null || obj === undefined; 12 | } 13 | 14 | public static isHash(str: Nullable) { 15 | if (StrUtil.isBlank(str)) { 16 | return false; 17 | } 18 | // 假设 key 是一个固定长度的十六进制字符串(如 SHA-1) 19 | return /^[a-f0-9]{40}$/i.test(str); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/backend/utils/TypeGuards.ts: -------------------------------------------------------------------------------- 1 | import AssertionError from '@/backend/errors/AssertionError'; 2 | 3 | export class TypeGuards { 4 | /** 5 | * 检查一个值是否为 null 或 undefined 6 | * @param value 要检查的值 7 | * @returns 如果值为 null 或 undefined 则返回 true,否则返回 false 8 | */ 9 | public static isNull(value: T | null | undefined): value is null | undefined { 10 | return value === null || value === undefined; 11 | } 12 | 13 | /** 14 | * 检查一个值是否不为 null 且不为 undefined 15 | * @param value 要检查的值 16 | * @returns 如果值既不为 null 也不为 undefined 则返回 true,否则返回 false 17 | */ 18 | public static isNotNull(value: T | null | undefined): value is NonNullable { 19 | return value !== null && value !== undefined; 20 | } 21 | 22 | /** 23 | * 断言一个值不为 null 且不为 undefined 24 | * @param value 要断言的值 25 | * @param message 可选的错误消息 26 | * @throws 如果值为 null 或 undefined,则抛出错误 27 | */ 28 | public static assertNotNull(value: T | null | undefined, message?: string): asserts value is NonNullable { 29 | if (this.isNull(value)) { 30 | throw new AssertionError(message || "Value is null or undefined"); 31 | } 32 | } 33 | 34 | public static assertType(value: any, type: string, message?: string): asserts value is T { 35 | if (typeof value !== type) { 36 | throw new AssertionError(message || `Value is not a ${type}`); 37 | } 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /src/common/api/dto.ts: -------------------------------------------------------------------------------- 1 | import { DateRange } from "react-day-picker"; 2 | 3 | export type ClipQuery = { 4 | keyword: string; 5 | keywordRange: 'context' | 'clip'; 6 | tags: number[]; 7 | tagsRelation: 'and' | 'or'; 8 | date: DateRange; 9 | includeNoTag: boolean; 10 | } 11 | -------------------------------------------------------------------------------- /src/common/api/register.ts: -------------------------------------------------------------------------------- 1 | import {ipcMain} from "electron"; 2 | import {ApiMap} from "@/common/api/api-def"; 3 | import SystemService from '@/backend/services/SystemService'; 4 | import dpLog from '@/backend/ioc/logger'; 5 | import container from '@/backend/ioc/inversify.config'; 6 | import TYPES from '@/backend/ioc/types'; 7 | 8 | 9 | export default function registerRoute(path: K, func: ApiMap[K]) { 10 | ipcMain.handle(path, (_event, param) => { 11 | dpLog.log('api-call', path, JSON.stringify(param)); 12 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 13 | // @ts-ignore 14 | return func(param).catch((e: Error) => { 15 | dpLog.error('api-error', path, e); 16 | container.get(TYPES.SystemService).sendErrorToRenderer(e); 17 | throw e 18 | }); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /src/common/constants/error-constants.ts: -------------------------------------------------------------------------------- 1 | enum ErrorConstants { 2 | CACHE_NOT_FOUND = 'Cache not found', 3 | } 4 | 5 | export default ErrorConstants; 6 | -------------------------------------------------------------------------------- /src/common/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export interface Cloneable { 2 | clone(): this; 3 | } 4 | 5 | export interface Convertible { 6 | convert(): T; 7 | } 8 | 9 | 10 | export interface Cancelable { 11 | cancel(): void; 12 | } 13 | -------------------------------------------------------------------------------- /src/common/types/DlVideoType.ts: -------------------------------------------------------------------------------- 1 | export type COOKIE = 'chrome' | 'firefox' | 'safari' | 'edge' | 'no-cookie'; 2 | export const cookieType =(str: COOKIE): string => { 3 | return str; 4 | } 5 | 6 | export interface DlVideoContext { 7 | taskId: number; 8 | url: string; 9 | cookies: COOKIE; 10 | savePath: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/common/types/SentenceC.tsx: -------------------------------------------------------------------------------- 1 | import { SentenceStruct } from '@/common/types/SentenceStruct'; 2 | 3 | 4 | export interface SrtSentence { 5 | fileHash: string; 6 | filePath: string; 7 | sentences: Sentence[]; 8 | } 9 | 10 | export interface Sentence { 11 | fileHash: string; 12 | index: number; 13 | 14 | start: number; 15 | 16 | end: number; 17 | 18 | adjustedStart: number | null; 19 | adjustedEnd: number | null; 20 | 21 | /** 22 | * 字幕英文原文 23 | */ 24 | text: string; 25 | 26 | /** 27 | * 字幕中文原文 28 | */ 29 | textZH: string; 30 | 31 | 32 | /** 33 | * 字幕机器翻译 34 | */ 35 | msTranslate: string | null; 36 | 37 | key: string; 38 | 39 | /** 40 | * 批量翻译的分组, 从1开始 41 | */ 42 | transGroup: number; 43 | 44 | struct: SentenceStruct; 45 | } 46 | -------------------------------------------------------------------------------- /src/common/types/SentenceStruct.ts: -------------------------------------------------------------------------------- 1 | export interface SentenceBlockPart { 2 | content: string; 3 | implicit: string; 4 | isWord: boolean; 5 | } 6 | 7 | export interface SentenceBlockBySpace { 8 | blockParts: SentenceBlockPart[]; 9 | } 10 | 11 | export interface SentenceStruct { 12 | original: string; 13 | blocks: SentenceBlockBySpace[]; 14 | } 15 | -------------------------------------------------------------------------------- /src/common/types/SettingType.ts: -------------------------------------------------------------------------------- 1 | export const defaultShortcut: ShortCutValue = { 2 | last: 'left,a', 3 | next: 'right,d', 4 | repeat: 'down,s', 5 | space: 'space,up,w', 6 | singleRepeat: 'r', 7 | showEn: 'e', 8 | showCn: 'c', 9 | showWordLevel: 'l', 10 | sowEnCn: 'b', 11 | nextTheme: 't', 12 | prevTheme: 'y', 13 | }; 14 | export interface ShortCutValue { 15 | /** 16 | * 上一句 17 | */ 18 | last: string; 19 | /** 20 | * 下一句 21 | */ 22 | next: string; 23 | /** 24 | * 重复 25 | */ 26 | repeat: string; 27 | /** 28 | * 播放/暂停 29 | */ 30 | space: string; 31 | /** 32 | * 单句重复 33 | */ 34 | singleRepeat: string; 35 | /** 36 | * 显示/隐藏英文 37 | */ 38 | showEn: string; 39 | /** 40 | * 显示/隐藏中文 41 | */ 42 | showCn: string; 43 | /** 44 | * 显示/隐藏中英 45 | */ 46 | sowEnCn: string; 47 | /** 48 | * 显示/隐藏单词等级 49 | */ 50 | showWordLevel: string; 51 | /** 52 | * next theme 53 | */ 54 | nextTheme: string; 55 | /** 56 | * previous theme 57 | */ 58 | prevTheme: string; 59 | } 60 | -------------------------------------------------------------------------------- /src/common/types/Types.ts: -------------------------------------------------------------------------------- 1 | export type WindowState = 2 | | 'normal' 3 | | 'maximized' 4 | | 'minimized' 5 | | 'fullscreen' 6 | | 'closed' 7 | | 'home' 8 | | 'player'; 9 | export type Nullable = T | null | undefined; 10 | -------------------------------------------------------------------------------- /src/common/types/WatchHistoryVO.ts: -------------------------------------------------------------------------------- 1 | type WatchHistoryVO = { 2 | id: string; 3 | basePath: string; 4 | fileName: string; 5 | isFolder: boolean; 6 | updatedAt: Date; 7 | duration: number; 8 | current_position: number; 9 | srtFile: string; 10 | playing: boolean; 11 | }; 12 | export default WatchHistoryVO; 13 | -------------------------------------------------------------------------------- /src/common/types/YdRes.ts: -------------------------------------------------------------------------------- 1 | export interface YdRes { 2 | errorCode: string; 3 | query: string; 4 | isDomainSupport: string; 5 | translation: string[]; 6 | basic: Basic; 7 | webdict: { 8 | "url": string; 9 | }, 10 | l: string; 11 | tSpeakUrl: string; 12 | speakUrl: string; 13 | } 14 | 15 | export interface Basic { 16 | exam_type: string[]; 17 | phonetic: string; 18 | 'uk-phonetic': string; 19 | 'us-phonetic': string; 20 | 'uk-speech': string; 21 | 'us-speech': string; 22 | explains: string[]; 23 | } 24 | -------------------------------------------------------------------------------- /src/common/types/aiRes/AiAnalyseGrammarsRes.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { codeBlock } from 'common-tags'; 3 | 4 | 5 | export class AiAnalyseGrammarsPrompt { 6 | public static schema = z.object({ 7 | hasGrammar: z.boolean().describe('whether the sentence has grammar'), 8 | grammarsMd: z.string().describe('explain result, must be in Chinese(简体中文), markdown format'), 9 | }); 10 | 11 | public static promptFunc = (s: string) => codeBlock` 12 | 你是一个英语老师,擅长用简洁的语言分析句子中的语法。 13 | 你的回复只需要包含语法解释,不需要包含其他内容。 14 | 用户的显示窗口比较小,所以请尽量简洁,但是不要遗漏重要的内容。 15 | 你的所有回复都会被放到 Markdown 容器中,所以请使用精美的 Markdown 格式书写,以便于用户阅读。 16 | 17 | 用中文分析下面句子包含的语法 18 | """sentence 19 | ${s} 20 | """ 21 | `; 22 | } 23 | 24 | export type AiAnalyseGrammarsRes = z.infer; 25 | 26 | -------------------------------------------------------------------------------- /src/common/types/aiRes/AiAnalyseNewPhrasesRes.ts: -------------------------------------------------------------------------------- 1 | export interface AiAnalyseNewPhrasesRes { 2 | hasPhrase: boolean; 3 | phrases: { 4 | phrase: string; 5 | meaning: string; 6 | }[]; 7 | } 8 | -------------------------------------------------------------------------------- /src/common/types/aiRes/AiAnalyseNewWordsRes.ts: -------------------------------------------------------------------------------- 1 | // const schema = z.object({ 2 | // hasNewWord: z.boolean().describe("Whether the sentence contains new words for an intermediate English speaker"), 3 | // words: z.array( 4 | // z.object({ 5 | // word: z.string().describe("The word"), 6 | // phonetic: z.string().describe("The phonetic of the word"), 7 | // meaning: z.string().describe("The meaning of the word in Chinese"), 8 | // }) 9 | // ).describe("A list of new words for an intermediate English speaker, if none, it should be an empty list"), 10 | // }); 11 | export interface AiAnalyseNewWordsRes { 12 | hasNewWord: boolean; 13 | words: { 14 | word: string; 15 | phonetic: string; 16 | meaning: string; 17 | }[]; 18 | } 19 | -------------------------------------------------------------------------------- /src/common/types/aiRes/AiFuncExplainSelectRes.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { codeBlock } from 'common-tags'; 3 | export class AiFuncExplainSelectPrompt { 4 | public static promptFunc(word: string):string { 5 | return codeBlock` 6 | 你是一个专业的在线双语词典,你的工作是帮助中文用户理解英文的 单词/短语。 7 | 请解释 "${word}" 这个 单词/短语 并用这个 单词/短语 造三个例句。 8 | ` 9 | } 10 | 11 | public static schema = z.object({ 12 | word: z.object({ 13 | word: z.string().describe('单词/短语原文'), 14 | phonetic: z.string().describe('单词/短语的音标'), 15 | meaningEn: z.string().describe('单词/短语的英文释意'), 16 | meaningZh: z.string().describe('单词/短语的中文释意'), 17 | }), 18 | // 例句 19 | examplesSentence1: z.string().describe('例句1'), 20 | examplesSentenceMeaning1: z.string().describe('例句1的中文释义'), 21 | examplesSentence2: z.string().describe('例句2'), 22 | examplesSentenceMeaning2: z.string().describe('例句2的中文释义'), 23 | examplesSentence3: z.string().describe('例句3'), 24 | examplesSentenceMeaning3: z.string().describe('例句3的中文释义'), 25 | }); 26 | } 27 | 28 | export type AiFuncExplainSelectRes = z.infer; 29 | -------------------------------------------------------------------------------- /src/common/types/aiRes/AiFuncExplainSelectWithContextRes.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { codeBlock } from 'common-tags'; 3 | export class AiFuncExplainSelectWithContextPrompt { 4 | public static promptFunc(text: string, selectedWord: string):string { 5 | return codeBlock` 6 | 你的任务是帮助中等英文水平的中文用户理解英文句子中的单词/短语。请根据句子解释单词/短语,并用这个单词/短语造三个例句。 7 | 8 | 句子:""" 9 | ${text} 10 | """ 11 | 12 | 请根据这个句子解释 "${selectedWord}" 这个 单词/短语 并用这个 单词/短语 造三个例句。 13 | ` 14 | } 15 | 16 | public static schema = z.object({ 17 | sentence: z.object({ 18 | sentence: z.string().describe('句子原文'), 19 | meaning: z.string().describe('句子的中文意思(简体中文)') 20 | }), 21 | word: z.object({ 22 | word: z.string().describe('单词/短语原文'), 23 | phonetic: z.string().describe('单词/短语的音标'), 24 | meaningEn: z.string().describe('单词/短语的英文释意'), 25 | meaningZh: z.string().describe('单词/短语的中文释意'), 26 | meaningInSentence: z.string().describe('结合句子解释这个单词/短语的意思') 27 | }), 28 | // 例句 29 | examplesSentence1: z.string().describe('例句1'), 30 | examplesSentenceMeaning1: z.string().describe('例句1的中文释义'), 31 | examplesSentence2: z.string().describe('例句2'), 32 | examplesSentenceMeaning2: z.string().describe('例句2的中文释义'), 33 | examplesSentence3: z.string().describe('例句3'), 34 | examplesSentenceMeaning3: z.string().describe('例句3的中文释义'), 35 | }); 36 | } 37 | 38 | export type AiFuncExplainSelectWithContextRes = z.infer; 39 | -------------------------------------------------------------------------------- /src/common/types/aiRes/AiFuncFormatSplit.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { codeBlock } from 'common-tags'; 3 | export class AiFuncFormatSplitPrompt { 4 | public static promptFunc(text: string):string { 5 | return codeBlock` 6 | 用户输入的视频章节分割文本可能有格式错误,导致程序无法正确解析。 7 | 格式要求: 8 | - 时间格式为:hh:mm:ss,如果用户输入小时为空,请你补全为00 9 | - 标题和时间之间用空格分隔 10 | 11 | 正确格式示例: 12 | 00:00:00 Title 13 | 00:00:10 Title 14 | 00:10:00 Title 15 | 00:20:00 Title 16 | 17 | 18 | 用户输入的文本如下: 19 | ${text} 20 | 21 | 22 | 修改用户输入的文本,使其符合格式要求。你的回复会被其他程序调用,所以请直接返回修改后的文本内容。 23 | ` 24 | } 25 | 26 | public static schema = z.object({ 27 | formatedText: z.string().describe('修正后的文本'), 28 | }) 29 | } 30 | 31 | export type AiFuncFormatSplitRes = z.infer; 32 | -------------------------------------------------------------------------------- /src/common/types/aiRes/AiFuncPolish.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { codeBlock } from 'common-tags'; 3 | export class AiFuncPolishPrompt { 4 | public static promptFunc(text: string):string { 5 | return codeBlock` 6 | Please edit the following sentences to improve clarity, conciseness, and coherence, making them match the expression of native speakers. 7 | 8 | 句子:""" 9 | ${text} 10 | """ 11 | ` 12 | } 13 | 14 | public static schema = z.object({ 15 | edit1: z.string().describe('润色后的句子1'), 16 | edit2: z.string().describe('润色后的句子2'), 17 | edit3: z.string().describe('润色后的句子3'), 18 | }).describe('Polish the sentence in three ways'); 19 | } 20 | 21 | export type AiFuncPolishRes = z.infer; 22 | -------------------------------------------------------------------------------- /src/common/types/aiRes/AiFuncTranslateWithContextRes.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { codeBlock } from 'common-tags'; 3 | export class AiFuncTranslateWithContextPrompt { 4 | public static promptFunc(sentence: string, context: string[]):string { 5 | return codeBlock` 6 | 你是一个翻译官,你会根据上下文把指定句子翻译成中文。 7 | 8 | 上下文: 9 | """ 10 | ${context.join('\n')} 11 | """ 12 | 13 | 14 | 请根据上下文把下面的句子翻译成中文。(简体中文) 15 | ${sentence} 16 | ` 17 | } 18 | 19 | public static schema = z.object({ 20 | original: z.string().describe('句子的原文'), 21 | translation: z.string().describe('句子的中文翻译,该字段语言为简体中文'), 22 | }); 23 | } 24 | 25 | export type AiFuncTranslateWithContextRes = z.infer; 26 | -------------------------------------------------------------------------------- /src/common/types/aiRes/AiMakeExampleSentencesRes.ts: -------------------------------------------------------------------------------- 1 | // const schema = z.object({ 2 | // sentences: z.array( 3 | // z.object({ 4 | // sentence: z.string().describe("The example sentence"), 5 | // meaning: z.string().describe("The meaning of the sentence in Chinese, Because it is for Chinese, who may not understand English well, so the meaning is in Chinese"), 6 | // point: z.array(z.string().describe("points you use in the sentence")), 7 | // }) 8 | // ).describe("A list of example sentences for an intermediate English speaker"), 9 | // }); 10 | 11 | export interface AiMakeExampleSentencesRes { 12 | sentences: { 13 | sentence: string; 14 | meaning: string; 15 | points: string[]; 16 | }[]; 17 | } 18 | -------------------------------------------------------------------------------- /src/common/types/aiRes/AiPhraseGroupRes.ts: -------------------------------------------------------------------------------- 1 | import { codeBlock } from 'common-tags'; 2 | import { z } from 'zod'; 3 | 4 | export class AiPhraseGroupPrompt { 5 | public static promptFunc(text: string):string { 6 | return codeBlock` 7 | 分析下面三个单引号包裹的英文句子的意群。将这个句子按照意群拆分成多个短句,每个短句是一个意群。 8 | 请在 tags 为该意群打标签,可以包括任何你认为有用的信息。 9 | ''' 10 | ${text} 11 | ''' 12 | ` 13 | } 14 | public static schema = z.object({ 15 | sentence: z.string().describe("The complete sentence from which phrase groups are derived."), 16 | phraseGroups: z.array( 17 | z.object({ 18 | original: z.string().describe("The original text of the phrase group."), 19 | translation: z.string().describe("The translation of the phrase group. in Chinese(简体中文)."), 20 | tags: z.array(z.string()).optional().describe("A list of tags to categorize the phrase group. in Chinese(简体中文)."), 21 | }) 22 | ).describe("An array of phrase groups that compose the sentence."), 23 | }); 24 | } 25 | export type AiPhraseGroupRes = z.infer; 26 | 27 | // 意群数组中的每个元素 28 | export type AiPhraseGroupElement = AiPhraseGroupRes['phraseGroups'][0]; 29 | -------------------------------------------------------------------------------- /src/common/types/aiRes/AiPunctuationResp.ts: -------------------------------------------------------------------------------- 1 | import { codeBlock } from 'common-tags'; 2 | import { z } from 'zod'; 3 | 4 | export class AiFuncPunctuationPrompt { 5 | public static promptFunc(sentence: string, srt: string) { 6 | return codeBlock` 7 | '''srt 8 | ${srt} 9 | ''' 10 | ${sentence}这行是完整的吗? 如果不是, 完整的句子是什么? 11 | `; 12 | } 13 | 14 | public static schema = z.object({ 15 | isComplete: z.boolean().describe('是完整的吗'), 16 | completeVersion: z.string().describe("完整的句子"), 17 | }); 18 | } 19 | 20 | export type AiFuncPunctuationRes = z.infer; 21 | -------------------------------------------------------------------------------- /src/common/types/chapter-result.ts: -------------------------------------------------------------------------------- 1 | export interface ChapterParseResult { 2 | timestampStart: string 3 | timestampEnd: string 4 | timestampValid: boolean, 5 | title: string 6 | original: string 7 | } 8 | -------------------------------------------------------------------------------- /src/common/types/clipMeta/ClipMetaDataV1.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | 4 | export const ClipSrtLineSchema = z.object({ 5 | index: z.number(), 6 | start: z.number(), 7 | end: z.number(), 8 | contentEn: z.string(), 9 | contentZh: z.string(), 10 | isClip: z.boolean() 11 | }); 12 | 13 | // Version 1 特定的 Schema 14 | export const MetaDataSchemaV1 = z.object({ 15 | video_name: z.string().describe('视频名'), 16 | created_at: z.number().describe('创建时间'), 17 | clip_content: z.array(ClipSrtLineSchema), 18 | clip_file: z.string(), 19 | thumbnail_file: z.string(), 20 | tags: z.array(z.string()) 21 | }); 22 | 23 | // ClipMeta Schema (不需要额外的合并,因为 MetaDataSchemaV1 已经包含了所有必要的字段) 24 | type ClipMetaV1 = z.infer; 25 | type ClipSrtLineV1 = z.infer; 26 | 27 | // 导出 schema 28 | export { 29 | ClipMetaV1, 30 | ClipSrtLineV1 31 | }; 32 | -------------------------------------------------------------------------------- /src/common/types/clipMeta/base.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | // 基础 OssObject Schema 4 | export const OssBaseSchema = z.object({ 5 | key: z.string(), 6 | baseDir: z.string(), 7 | version: z.number() 8 | }); 9 | 10 | export type OssObjectType = z.infer; 11 | -------------------------------------------------------------------------------- /src/common/types/clipMeta/index.ts: -------------------------------------------------------------------------------- 1 | import { ClipMetaV1, ClipSrtLineV1 } from '@/common/types/clipMeta/ClipMetaDataV1'; 2 | import { OssObjectType } from '@/common/types/clipMeta/base'; 3 | 4 | export type ClipMeta = ClipMetaV1; 5 | export const ClipVersion = 1; 6 | export type OssBaseMeta = OssObjectType; 7 | export type ClipSrtLine = ClipSrtLineV1; 8 | -------------------------------------------------------------------------------- /src/common/types/dl-progress.ts: -------------------------------------------------------------------------------- 1 | export interface DlProgress { 2 | name: string; 3 | progress: number; 4 | stdOut: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/common/types/msg/AiCtxMenuPolishMessage.ts: -------------------------------------------------------------------------------- 1 | import CustomMessage, { MsgType } from '@/common/types/msg/interfaces/CustomMessage'; 2 | import { codeBlock } from 'common-tags'; 3 | import { Topic } from '@/fronted/hooks/useChatPanel'; 4 | import { AiFuncPolishRes } from '@/common/types/aiRes/AiFuncPolish'; 5 | import { getDpTaskResult } from '@/fronted/hooks/useDpTaskCenter'; 6 | import { CoreMessage } from 'ai'; 7 | 8 | export default class AiCtxMenuPolishMessage implements CustomMessage { 9 | public taskId: number; 10 | private readonly topic: Topic; 11 | public origin: string; 12 | 13 | constructor(taskId: number, topic:Topic, origin: string) { 14 | this.taskId = taskId; 15 | this.origin = origin; 16 | this.topic = topic; 17 | } 18 | 19 | copy(): AiCtxMenuPolishMessage { 20 | return new AiCtxMenuPolishMessage(this.taskId, this.topic, this.origin); 21 | } 22 | 23 | msgType: MsgType = 'ai-func-polish'; 24 | 25 | async toMsg(): Promise { 26 | const resp: AiFuncPolishRes | null= await getDpTaskResult(this.taskId); 27 | // 根据以上信息编造一个假的回复 28 | const aiResp = codeBlock` 29 | 好的,这句话可以这样润色: 30 | 31 | - ${resp?.edit1} 32 | - ${resp?.edit2} 33 | - ${resp?.edit3} 34 | ` 35 | return [{ 36 | role:'user', 37 | content: `帮我用三种方式润色这句话,让它更地道` 38 | },{ 39 | role:'assistant', 40 | content: aiResp 41 | }]; 42 | } 43 | 44 | getTopic(): Topic { 45 | return this.topic; 46 | } 47 | 48 | getTaskIds(): number[] { 49 | return [this.taskId]; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/common/types/msg/AiNormalMessage.ts: -------------------------------------------------------------------------------- 1 | import CustomMessage, { MsgType } from '@/common/types/msg/interfaces/CustomMessage'; 2 | import { Topic } from '@/fronted/hooks/useChatPanel'; 3 | import { getDpTaskResult } from '@/fronted/hooks/useDpTaskCenter'; 4 | import { CoreMessage } from 'ai'; 5 | 6 | class AiNormalMessage implements CustomMessage { 7 | private readonly topic: Topic; 8 | public readonly taskId: number; 9 | 10 | constructor(topic: Topic, taskId: number) { 11 | this.topic = topic; 12 | this.taskId = taskId; 13 | } 14 | 15 | async toMsg(): Promise { 16 | const msg = await getDpTaskResult(this.taskId, true); 17 | return [{ 18 | role: 'assistant', 19 | content: msg ?? '' 20 | }]; 21 | } 22 | 23 | msgType: MsgType = 'ai-normal'; 24 | 25 | copy(): AiNormalMessage { 26 | return new AiNormalMessage(this.topic, this.taskId); 27 | } 28 | 29 | getTopic(): Topic { 30 | return this.topic; 31 | } 32 | 33 | getTaskIds(): number[] { 34 | return [this.taskId]; 35 | } 36 | } 37 | 38 | export default AiNormalMessage; 39 | -------------------------------------------------------------------------------- /src/common/types/msg/HumanNormalMessage.ts: -------------------------------------------------------------------------------- 1 | import CustomMessage, {MsgType} from "@/common/types/msg/interfaces/CustomMessage"; 2 | import { Topic } from '@/fronted/hooks/useChatPanel'; 3 | import { CoreMessage } from 'ai'; 4 | 5 | class HumanNormalMessage implements CustomMessage { 6 | public content: string; 7 | private readonly topic: Topic ; 8 | constructor(topic:Topic, content: string) { 9 | this.topic = topic; 10 | this.content = content; 11 | } 12 | 13 | async toMsg(): Promise { 14 | return [{ 15 | role: "user", 16 | content: this.content 17 | }] 18 | } 19 | 20 | 21 | copy(): HumanNormalMessage { 22 | return new HumanNormalMessage(this.topic, this.content); 23 | } 24 | 25 | msgType: MsgType = "human-normal"; 26 | 27 | getTopic(): Topic { 28 | return this.topic; 29 | } 30 | 31 | getTaskIds(): number[] { 32 | return []; 33 | } 34 | } 35 | 36 | 37 | export default HumanNormalMessage; 38 | -------------------------------------------------------------------------------- /src/common/types/msg/HumanTopicMessage.ts: -------------------------------------------------------------------------------- 1 | import CustomMessage, { MsgType } from '@/common/types/msg/interfaces/CustomMessage'; 2 | import { Topic } from '@/fronted/hooks/useChatPanel'; 3 | import { CoreMessage } from 'ai'; 4 | 5 | 6 | class HumanTopicMessage implements CustomMessage { 7 | private readonly topic: Topic; 8 | public content: string; 9 | public phraseGroupTask: number; 10 | 11 | constructor(topic: Topic, text: string, phraseGroupTask: number) { 12 | this.topic = topic; 13 | this.content = text; 14 | this.phraseGroupTask = phraseGroupTask; 15 | } 16 | 17 | async toMsg(): Promise { 18 | return [ 19 | { 20 | role: 'system', 21 | content: 'You are an English teacher, specialized in teaching English.' 22 | }, 23 | { 24 | role: 'user', 25 | content: `请帮我分析 "${this.content}"` 26 | }]; 27 | } 28 | 29 | msgType: MsgType = 'human-topic'; 30 | 31 | copy(): HumanTopicMessage { 32 | return new HumanTopicMessage(this.topic, this.content, this.phraseGroupTask); 33 | } 34 | 35 | getTopic(): Topic { 36 | return this.topic; 37 | } 38 | 39 | getTaskIds(): number[] { 40 | return [this.phraseGroupTask]; 41 | } 42 | } 43 | 44 | export default HumanTopicMessage; 45 | -------------------------------------------------------------------------------- /src/common/types/msg/interfaces/CustomMessage.ts: -------------------------------------------------------------------------------- 1 | import { Topic } from '@/fronted/hooks/useChatPanel'; 2 | import { CoreMessage } from 'ai'; 3 | 4 | export default interface CustomMessage { 5 | toMsg(): Promise; 6 | 7 | msgType: MsgType; 8 | 9 | copy(): T; 10 | 11 | getTopic(): Topic; 12 | 13 | getTaskIds(): number[]; 14 | } 15 | 16 | export type MsgType = 17 | | 'human-topic' 18 | | 'human-normal' 19 | | 'ai-welcome' 20 | | 'ai-streaming' 21 | | 'ai-normal' 22 | | 'ai-func-explain-select' 23 | | 'ai-func-explain-select-with-context' 24 | | 'ai-func-polish' 25 | -------------------------------------------------------------------------------- /src/common/types/release.ts: -------------------------------------------------------------------------------- 1 | export default interface Release { 2 | url: string; 3 | version: string; 4 | content: string; 5 | } -------------------------------------------------------------------------------- /src/common/types/store_schema.ts: -------------------------------------------------------------------------------- 1 | export const SettingKeyObj = { 2 | 'shortcut.previousSentence': 'left,a', 3 | 'shortcut.nextSentence': 'right,d', 4 | 'shortcut.repeatSentence': 'down,s', 5 | 'shortcut.playPause': 'space,up,w', 6 | 'shortcut.repeatSingleSentence': 'r', 7 | 'shortcut.autoPause': 'u', 8 | 'shortcut.toggleEnglishDisplay': 'e', 9 | 'shortcut.toggleChineseDisplay': 'c', 10 | 'shortcut.toggleWordLevelDisplay': 'l', 11 | 'shortcut.toggleBilingualDisplay': 'b', 12 | 'shortcut.nextTheme': 't', 13 | 'shortcut.adjustBeginMinus': 'z', 14 | 'shortcut.adjustBeginPlus': 'x', 15 | 'shortcut.adjustEndMinus': 'n', 16 | 'shortcut.adjustEndPlus': 'm', 17 | 'shortcut.clearAdjust': 'v', 18 | 'shortcut.nextPlaybackRate': 'p', 19 | 'shortcut.aiChat': 'slash', 20 | 'shortcut.toggleCopyMode': 'shift+y', 21 | 'shortcut.addClip': 'shift+l', 22 | 'shortcut.openControlPanel': 'shift+p', 23 | 'userSelect.playbackRateStack':'', 24 | 'apiKeys.youdao.secretId': '', 25 | 'apiKeys.youdao.secretKey': '', 26 | 'apiKeys.tencent.secretId': '', 27 | 'apiKeys.tencent.secretKey': '', 28 | 'apiKeys.openAi.key': '', 29 | 'apiKeys.openAi.endpoint': '', 30 | 'apiKeys.openAi.stream': 'on', 31 | 'model.gpt.default': 'gpt-4o-mini', 32 | 'appearance.theme': 'light', 33 | 'appearance.fontSize': 'fontSizeLarge', 34 | 'storage.path': '', 35 | 'storage.collection': 'default', 36 | } 37 | export type SettingKey = keyof typeof SettingKeyObj; 38 | -------------------------------------------------------------------------------- /src/common/types/tonvert-type.ts: -------------------------------------------------------------------------------- 1 | export interface FolderVideos { 2 | folder: string; 3 | videos: string[]; 4 | } 5 | 6 | export interface ConvertResult { 7 | progress: number; 8 | path: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/common/types/video-info.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | // VideoInfo schema 4 | const VideoInfoSchema = z.object({ 5 | filename: z.string(), 6 | duration: z.number(), 7 | size: z.number(), 8 | modifiedTime: z.number(), 9 | createdTime: z.number(), 10 | bitrate: z.number().optional(), 11 | videoCodec: z.string().optional(), 12 | audioCodec: z.string().optional(), 13 | }); 14 | 15 | // WhisperResponse schema 16 | const WhisperResponseVerifySchema = z.object({ 17 | language: z.string(), 18 | duration: z.union([z.number(), z.string()]), 19 | text: z.string(), 20 | segments: z.array(z.object({ 21 | seek: z.number(), 22 | start: z.number(), 23 | end: z.number(), 24 | text: z.string() 25 | })) 26 | }); 27 | 28 | // SplitChunk schema 29 | const SplitChunkSchema = z.object({ 30 | offset: z.number(), 31 | filePath: z.string(), 32 | response: WhisperResponseVerifySchema.optional(), 33 | }); 34 | 35 | // WhisperContext schema 36 | const WhisperContextSchema = z.object({ 37 | filePath: z.string(), 38 | folder: z.string(), 39 | state: z.enum(['init', 'processed', 'done']), 40 | videoInfo: VideoInfoSchema.nullable(), 41 | chunks: z.array(SplitChunkSchema), 42 | updatedTime: z.number(), 43 | }); 44 | 45 | // 类型推断 46 | type WhisperContext = z.infer; 47 | type VideoInfo = z.infer; 48 | type SplitChunk = z.infer; 49 | type WhisperResponse = z.infer; 50 | 51 | export { 52 | VideoInfoSchema, 53 | SplitChunkSchema, 54 | WhisperContextSchema, 55 | WhisperResponseVerifySchema, 56 | type WhisperContext, 57 | type VideoInfo, 58 | type SplitChunk, 59 | type WhisperResponse 60 | }; 61 | -------------------------------------------------------------------------------- /src/common/utils/CollUtil.ts: -------------------------------------------------------------------------------- 1 | export default class CollUtil { 2 | public static isEmpty(coll: T[] | null | undefined): coll is null | undefined | [] { 3 | return !coll || coll.length === 0; 4 | } 5 | 6 | public static isNotEmpty(coll: T[] | null | undefined): boolean { 7 | return !this.isEmpty(coll); 8 | } 9 | 10 | public static emptyIfNull(coll: T[] | null | undefined): T[] { 11 | return coll || []; 12 | } 13 | 14 | public static safeGet(coll: T[] | null | undefined, index: number): T | null { 15 | if (this.isEmpty(coll)) { 16 | return null; 17 | } 18 | if (index < 0 || index >= coll.length) { 19 | return null; 20 | } 21 | return coll[index]; 22 | } 23 | public static validGet(coll: T[], index: number): T { 24 | if (this.isEmpty(coll)) { 25 | throw new Error('coll is empty'); 26 | } 27 | let targetIndex = index; 28 | if (targetIndex < 0) { 29 | targetIndex = 0; 30 | } 31 | if (targetIndex >= coll.length) { 32 | targetIndex = coll.length - 1; 33 | } 34 | return coll[targetIndex]; 35 | } 36 | 37 | public static getFirst(coll: T[] | null | undefined): T | null { 38 | return this.safeGet(coll, 0); 39 | } 40 | 41 | static toMap(entities: E[], keyFunc: (e: E) => K): Map { 42 | const result = new Map(); 43 | for (const e of entities) { 44 | result.set(keyFunc(e), e); 45 | } 46 | return result; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/common/utils/RateLimiter.ts: -------------------------------------------------------------------------------- 1 | export type RATE_LIMIT_KEY = 2 | | 'whisper' 3 | | 'gpt' 4 | | 'tencent' 5 | | 'tts'; 6 | 7 | const RateLimitConfig: Record = { 8 | whisper: {maxRequests: 5, timeWindow: 1000}, 9 | gpt: {maxRequests: 10, timeWindow: 1000}, 10 | tencent: {maxRequests: 4, timeWindow: 1000}, 11 | tts: {maxRequests: 10, timeWindow: 1000} 12 | }; 13 | 14 | 15 | export default class RateLimiter { 16 | private static limits: Map = new Map(); 17 | 18 | public static async wait(key: RATE_LIMIT_KEY): Promise { 19 | if (!this.limits.has(key)) { 20 | this.limits.set(key, []); 21 | } 22 | const timeWindow = RateLimitConfig[key].timeWindow; 23 | const maxRequests = RateLimitConfig[key].maxRequests; 24 | 25 | const now = Date.now(); 26 | const timestamps = this.limits.get(key)!; 27 | 28 | // Remove timestamps outside the current time window 29 | while (timestamps.length && timestamps[0] <= now - timeWindow) { 30 | timestamps.shift(); 31 | } 32 | 33 | if (timestamps.length >= maxRequests) { 34 | // Calculate the wait time 35 | const waitTime = (timestamps[0] + timeWindow) - now; 36 | await new Promise(resolve => setTimeout(resolve, waitTime)); 37 | return this.wait(key); // Retry the request 38 | } 39 | 40 | // Log the request timestamp 41 | timestamps.push(now); 42 | this.limits.set(key, timestamps); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/common/utils/TransHolder.ts: -------------------------------------------------------------------------------- 1 | import { p } from './Util'; 2 | 3 | export default class TransHolder { 4 | private result: Map = new Map(); 5 | 6 | public getMapping(): Map { 7 | return this.result; 8 | } 9 | 10 | public isEmpty(): boolean { 11 | return this.result.size === 0; 12 | } 13 | 14 | public addAll(map: Map): void { 15 | map.forEach((value, key) => { 16 | this.add(p(key), value); 17 | }); 18 | } 19 | 20 | public add(key: string, value: T) { 21 | this.result.set(p(key), value); 22 | } 23 | 24 | public get(key: string): T | undefined { 25 | return this.result.get(p(key)); 26 | } 27 | 28 | public merge(other: TransHolder): TransHolder { 29 | const res = new TransHolder(); 30 | this.result.forEach((value, key) => { 31 | res.add(key, value); 32 | }); 33 | other.result.forEach((value, key) => { 34 | res.add(key, value); 35 | }); 36 | return res; 37 | } 38 | 39 | static from(mapping: Map) { 40 | const res = new TransHolder(); 41 | res.addAll(mapping); 42 | return res; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/common/utils/UndoRedo.ts: -------------------------------------------------------------------------------- 1 | class UndoRedo { 2 | private undoStack: T[] = []; 3 | private redoStack: T[] = []; 4 | 5 | public undo() { 6 | const state = this.undoStack.pop(); 7 | if (state) { 8 | this.redoStack.push(state); 9 | } 10 | return this.undoStack[this.undoStack.length - 1]; 11 | } 12 | 13 | public redo() { 14 | const state = this.redoStack.pop(); 15 | if (state) { 16 | this.undoStack.push(state); 17 | } 18 | return this.undoStack[this.undoStack.length - 1]; 19 | } 20 | 21 | public canUndo() { 22 | return this.undoStack.length > 1; 23 | } 24 | 25 | public canRedo() { 26 | return this.redoStack.length > 0; 27 | } 28 | 29 | public add(state: T) { 30 | this.undoStack.push(state); 31 | this.redoStack = []; 32 | } 33 | 34 | public update(state: T) { 35 | this.undoStack[this.undoStack.length - 1] = state; 36 | } 37 | public clear() { 38 | this.undoStack = []; 39 | this.redoStack = []; 40 | } 41 | } 42 | 43 | export default UndoRedo; 44 | -------------------------------------------------------------------------------- /src/common/utils/UrlUtil.ts: -------------------------------------------------------------------------------- 1 | import * as base32 from 'hi-base32'; 2 | import PathUtil from '@/common/utils/PathUtil'; 3 | 4 | export const DP = 'dp'; 5 | export const DP_FILE = 'dp-file'; 6 | 7 | export default class UrlUtil { 8 | public static dp(...paths: string[]) { 9 | const url = PathUtil.join(...paths); 10 | return `${DP}://${base32.encode(url)}`; 11 | } 12 | 13 | public static file(...paths: string[]) { 14 | const url = PathUtil.join(...paths); 15 | return `${DP_FILE}://${url}`; 16 | } 17 | 18 | public static joinWebUrl(...paths: string[]): string { 19 | // 移除空字符串 20 | const cleanPaths = paths.filter(path => path !== ''); 21 | 22 | // 处理第一个部分(可能包含协议) 23 | let result = cleanPaths[0] || ''; 24 | 25 | // 处理剩余部分 26 | for (let i = 1; i < cleanPaths.length; i++) { 27 | const segment = cleanPaths[i]; 28 | 29 | if (result.endsWith('/')) { 30 | result += segment.startsWith('/') ? segment.slice(1) : segment; 31 | } else { 32 | result += segment.startsWith('/') ? segment : '/' + segment; 33 | } 34 | } 35 | 36 | // 处理协议后的双斜杠 37 | result = result.replace(/^(https?:)\/+/, '$1//'); 38 | 39 | // 移除URL参数和锚点之前的多余斜杠 40 | result = result.replace(/\/+([?#])/, '$1'); 41 | 42 | // 移除中间的多余斜杠 43 | result = result.replace(/([^:]\/)\/+/g, '$1'); 44 | 45 | return result; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/common/utils/func-util.ts: -------------------------------------------------------------------------------- 1 | export default class FuncUtil { 2 | public static blank = () => { 3 | // do nothing 4 | }; 5 | } 6 | -------------------------------------------------------------------------------- /src/common/utils/srtSlice.ts: -------------------------------------------------------------------------------- 1 | import SrtUtil from '@/common/utils/SrtUtil'; 2 | 3 | /** 4 | * 将 SRT 文件切片,返回指定范围内的字幕内容。 5 | * 6 | * @param {string} srt - SRT 文件的内容。 7 | * @param {number} no - 要查找的字幕编号。 8 | * @param {number} range - 要返回的字幕范围。 9 | * @returns {string} - 指定范围内的字幕内容。 10 | */ 11 | export const srtSlice = (srt: string, no: number, range: number): string => { 12 | // Split the SRT file into an array of subtitles 13 | const subtitles = srt.split(/\n\s*\n/).map(subtitle => subtitle.trim()); 14 | 15 | // Find the index of the subtitle with the given number 16 | const index = subtitles.findIndex(subtitle => subtitle.startsWith(no.toString())); 17 | 18 | // Calculate the start and end indices for the range of subtitles 19 | const start = Math.max(0, index - Math.floor(range)); 20 | const end = Math.min(subtitles.length, index + Math.ceil(range)); 21 | 22 | // Slice the array to get the range of subtitles and join them back into a string 23 | return subtitles.slice(start, end).join('\n\n'); 24 | }; 25 | /** 26 | * 获取指定编号的字幕内容。 27 | * 28 | * @param {string} srt - SRT 文件的内容。 29 | * @param {number} no - 要查找的字幕编号。 30 | * @returns {string | undefined} - 返回指定编号的字幕内容,如果未找到则返回 undefined。 31 | */ 32 | export const getSubtitleContent = (srt: string, no: number): string | undefined => { 33 | // Parse the SRT string into an array of subtitle objects 34 | const subtitles = SrtUtil.parseSrt(srt); 35 | 36 | // Find the subtitle object with the given number 37 | const subtitle = subtitles.find(subtitle => subtitle.index === no); 38 | 39 | // Return the content of the found subtitle 40 | return subtitle ? subtitle.contentEn : undefined; 41 | }; 42 | -------------------------------------------------------------------------------- /src/common/utils/str-util.ts: -------------------------------------------------------------------------------- 1 | export default class StrUtil { 2 | // Type predicate: Returns true if `str` is null, undefined, or only whitespace 3 | public static isBlank(str: string | undefined | null): str is undefined | null | '' { 4 | return str === undefined || str === null || str.trim() === ''; 5 | } 6 | 7 | // Type predicate: Returns true if `str` is a non-blank string 8 | public static isNotBlank(str: string | undefined | null): str is string { 9 | return !StrUtil.isBlank(str); 10 | } 11 | 12 | public static allBlank(...strs: (string | undefined | null)[]): boolean { 13 | return strs.every(this.isBlank); 14 | } 15 | 16 | public static ifBlank(str: string | undefined | null, defaultStr: string): string { 17 | return StrUtil.isBlank(str) ? defaultStr : str!; 18 | } 19 | 20 | /** 21 | * Checks if at least one string is non-blank. 22 | * 23 | * @param {...(string | undefined | null)[]} strs - Strings to check 24 | * @returns {boolean} - True if any string is non-blank 25 | */ 26 | public static hasNonBlank(...strs: (string | undefined | null)[]): boolean { 27 | return strs.some(StrUtil.isNotBlank); 28 | } 29 | 30 | public static hasBlank(...strs: (string | undefined | null)[]): boolean { 31 | return strs.some(StrUtil.isBlank); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/fronted/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import {cn} from "@/fronted/lib/utils"; 2 | import React, { useEffect } from 'react'; 3 | 4 | export interface ButtonParam { 5 | onClick?: () => void; 6 | title?: string; 7 | className?: string; 8 | children?: React.ReactNode; 9 | } 10 | const Button = ({ children, onClick, title, className }: ButtonParam) => { 11 | const [clicked, setClicked] = React.useState(false); 12 | useEffect(() => { 13 | if (clicked) { 14 | setTimeout(() => { 15 | setClicked(false); 16 | }, 1000); 17 | } 18 | }, [clicked]); 19 | return ( 20 |
    { 22 | setClicked(true); 23 | onClick?.(); 24 | }} 25 | className={cn( 26 | 'flex gap-2 rounded-lg hover:bg-gray-100 px-2 py-1 w-full text-base justify-start items-center h-10', 27 | className 28 | )} 29 | > 30 | {children && React.cloneElement(children as React.ReactElement, { 31 | className: cn('text-gray-500 w-8 h-8 transition-all duration-500', 32 | clicked && 'text-green-500 scale-105'), 33 | })} 34 |
    36 | {title} 37 |
    38 |
    39 | ); 40 | }; 41 | Button.defaultProps = { 42 | checked: false, 43 | onChange: (checked: boolean) => { 44 | // 45 | }, 46 | title: 'Toggle me', 47 | className: '', 48 | }; 49 | 50 | export default Button; 51 | -------------------------------------------------------------------------------- /src/fronted/components/Eb.tsx: -------------------------------------------------------------------------------- 1 | import { ErrorBoundary } from 'react-error-boundary'; 2 | import FallBack from '@/fronted/components/FallBack'; 3 | 4 | const Eb = ({children}:{ 5 | children?: React.ReactNode 6 | }) => { 7 | return ( 8 | 9 | {children} 10 | 11 | ) 12 | } 13 | export default Eb; 14 | -------------------------------------------------------------------------------- /src/fronted/components/FallBack.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const FallBack = () => { 4 | return ( 5 |
    6 |
    7 | Oh no! 8 | Something went wrong. 9 |
    10 |
    11 | ) 12 | } 13 | 14 | export default FallBack; 15 | -------------------------------------------------------------------------------- /src/fronted/components/Notification.tsx: -------------------------------------------------------------------------------- 1 | import usePlayerToaster from '../hooks/usePlayerToaster'; 2 | 3 | const Notification = () => { 4 | const text = usePlayerToaster((state) => state.text); 5 | const type = usePlayerToaster((state) => state.type); 6 | 7 | return ( 8 | <> 9 | {type !== 'none' ? ( 10 |
    11 |
    16 | {text} 17 |
    18 |
    19 | ) : ( 20 | <> 21 | )} 22 | 23 | ); 24 | }; 25 | export default Notification; 26 | -------------------------------------------------------------------------------- /src/fronted/components/PlaySpeedToaster.tsx: -------------------------------------------------------------------------------- 1 | import {cn} from "@/fronted/lib/utils"; 2 | import {useEffect, useRef, useState} from "react"; 3 | 4 | const PlaySpeedToaster = ({speed, className}: { 5 | speed: number; 6 | className?: string; 7 | 8 | }) => { 9 | const localSpeed = useRef(speed); 10 | const [show, setShow] = useState(false); 11 | 12 | useEffect(() => { 13 | if (localSpeed.current !== speed) { 14 | localSpeed.current = speed; 15 | setShow(true); 16 | const timer = setTimeout(() => { 17 | setShow(false); 18 | }, 1000); 19 | return () => { 20 | clearTimeout(timer); 21 | }; 22 | } 23 | }, [speed]); 24 | return ( 25 | show ?
    26 | Speed: {speed.toFixed(2)} x 27 |
    : <> 28 | ) 29 | } 30 | 31 | export default PlaySpeedToaster; 32 | -------------------------------------------------------------------------------- /src/fronted/components/PlayerToaster.tsx: -------------------------------------------------------------------------------- 1 | import {cn} from "@/fronted/lib/utils"; 2 | import {useEffect, useRef, useState} from "react"; 3 | import usePlayerToaster from '@/fronted/hooks/usePlayerToaster'; 4 | 5 | const PlayerToaster = ({ className}: { 6 | className?: string; 7 | 8 | }) => { 9 | const type = usePlayerToaster(s=>s.type); 10 | const text = usePlayerToaster(s=>s.text); 11 | 12 | // 等宽字体 13 | return ( 14 | type!== 'none' ?
    15 | {text} 16 |
    : <> 17 | ) 18 | } 19 | 20 | export default PlayerToaster; 21 | -------------------------------------------------------------------------------- /src/fronted/components/Separtor.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import {cn} from "@/fronted/lib/utils"; 5 | 6 | export interface SeparatorProps { 7 | orientation?: 'horizontal' | 'vertical'; 8 | className?: string; 9 | } 10 | 11 | const Separator = ({ orientation, className }: SeparatorProps) => { 12 | if (orientation === 'horizontal') { 13 | return ( 14 |
    15 |
    16 |
    17 | ); 18 | } 19 | return ( 20 |
    21 |
    22 |
    23 | ); 24 | }; 25 | 26 | Separator.defaultProps = { 27 | orientation: 'horizontal', 28 | className: '', 29 | }; 30 | 31 | export default Separator; 32 | -------------------------------------------------------------------------------- /src/fronted/components/Tag.tsx: -------------------------------------------------------------------------------- 1 | // src/components/ui/Tag.tsx 2 | 3 | import React from 'react'; 4 | 5 | interface TagProps { 6 | children: React.ReactNode; 7 | onContextMenu?: (e: React.MouseEvent) => void; 8 | className?: string; 9 | } 10 | 11 | const Tag: React.FC = ({ children, onContextMenu, className }) => { 12 | return ( 13 |
    17 | {children} 18 |
    19 | ); 20 | }; 21 | 22 | export default Tag; 23 | -------------------------------------------------------------------------------- /src/fronted/components/TitleBar/TitleBar.tsx: -------------------------------------------------------------------------------- 1 | import TitleBarWindows from './TitleBarWindows'; 2 | import TitleBarMac from './TitleBarMac'; 3 | import useSystem from '../../hooks/useSystem'; 4 | 5 | export interface TitleBarProps { 6 | className?: string; 7 | maximizable?: boolean; 8 | } 9 | 10 | const TitleBar = ({ className, maximizable }: TitleBarProps) => { 11 | const isWindows = useSystem((s) => s.isWindows); 12 | 13 | return ( 14 | <> 15 | {isWindows ? ( 16 | 20 | ) : ( 21 | 22 | )} 23 | 24 | ); 25 | }; 26 | 27 | TitleBar.defaultProps = { 28 | className: '', 29 | maximizable: true, 30 | }; 31 | export default TitleBar; 32 | -------------------------------------------------------------------------------- /src/fronted/components/TitleBar/TitleBarWindows.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sourceRoot":"","sources":["TitleBarWindows.scss"],"names":[],"mappings":"AAuBQ;EACI,kBAxBoB;;AA0BpB;EACI,kBA1BgB;;AA6BxB;EACI,kBA1BoB;;AA4BpB;EACI,kBA5BgB;;AA+BxB;EACI,kBA5BoB;;AA8BpB;EACI,kBA9BgB;;AAoCxB;EACI;;AAMA;EACI;;;AAMhB;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA,kBAtDY;EAuDZ;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAIA;EACI,kBApFoB;EAqFpB;EACA;;AAEJ;EACI;;AAEJ;EACI;;AAEJ;EACI,kBA9FoB;;AAmGxB;EACI,kBAhGoB;EAiGpB;EACA;;AAGJ;EACI,kBArGoB;;AA0GxB;EACI,kBAvGoB;EAwGpB;EACA;;AAEJ;EACI,kBA9GoB;EA+GpB;EACA;EACA;;AAEJ;EACI,kBAjHoB;;AAmHxB;EACI,kBAtHoB","file":"TitleBarWindows.css"} -------------------------------------------------------------------------------- /src/fronted/components/TitleBar/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidSpoon/DashPlayer/4499055708c86a52390a9861ebde88f5561f68e7/src/fronted/components/TitleBar/index.ts -------------------------------------------------------------------------------- /src/fronted/components/bg/AboutBg.tsx: -------------------------------------------------------------------------------- 1 | import {cn} from "@/fronted/lib/utils"; 2 | import './AboutBg.css'; 3 | 4 | const AboutBg = ({ className }: { className?: string }) => { 5 | return
    ; 6 | }; 7 | 8 | AboutBg.defaultProps = { 9 | className: '', 10 | }; 11 | 12 | export default AboutBg; 13 | -------------------------------------------------------------------------------- /src/fronted/components/bg/Background.tsx: -------------------------------------------------------------------------------- 1 | import {cn} from "@/fronted/lib/utils"; 2 | 3 | const Background = ({ className }: { className?: string }) => { 4 | return
    ; 5 | }; 6 | 7 | Background.defaultProps = { 8 | className: '', 9 | }; 10 | 11 | export default Background; 12 | -------------------------------------------------------------------------------- /src/fronted/components/chat/Playable.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/fronted/lib/utils'; 2 | import { PiSpeakerSimpleHigh } from 'react-icons/pi'; 3 | import { getTtsUrl, playAudioUrl } from '@/common/utils/AudioPlayer'; 4 | import { useState } from 'react'; 5 | import { Loader, Volume2 } from 'lucide-react'; 6 | 7 | export interface PlayableProps { 8 | className?: string; 9 | children?: string; 10 | } 11 | 12 | const Playable = ({ className, children }: PlayableProps) => { 13 | const [loading, setLoading] = useState(false); 14 | return ( 15 | { 17 | const selectedText = window.getSelection().toString(); 18 | if (selectedText.length === 0) { 19 | setLoading(true); 20 | const str = children || ''; 21 | console.log('ttsStr', str); 22 | const ttsUrl = await getTtsUrl(str); 23 | setLoading(false); 24 | console.log('ttsUrl', ttsUrl); 25 | await playAudioUrl(ttsUrl); 26 | } 27 | }} 28 | className={cn(' cursor-pointer hover:underline', className)}> 29 | {children} 30 | 31 | {loading ? : 32 | } 33 | 34 | ); 35 | }; 36 | 37 | Playable.defaultProps = { 38 | className: '', 39 | children: '' 40 | }; 41 | 42 | export default Playable; 43 | -------------------------------------------------------------------------------- /src/fronted/components/chat/markdown.tsx: -------------------------------------------------------------------------------- 1 | import {FC, memo} from 'react' 2 | import ReactMarkdown, {Options} from 'react-markdown' 3 | import remarkGfm from "remark-gfm"; 4 | import remarkMath from "remark-math"; 5 | import React from 'react' 6 | export const MemoizedReactMarkdown: FC = memo( 7 | ReactMarkdown, 8 | (prevProps, nextProps) => 9 | prevProps.children === nextProps.children && 10 | prevProps.className === nextProps.className 11 | ) 12 | const Md: FC<{ children: string }> = ({children}) => { 13 | return 17 | {children} 18 | 19 | } 20 | 21 | export default Md; 22 | -------------------------------------------------------------------------------- /src/fronted/components/chat/msg/AiNormalMsg.tsx: -------------------------------------------------------------------------------- 1 | import {cn} from "@/fronted/lib/utils"; 2 | import {IconOpenAI} from "@/fronted/components/chat/icons"; 3 | import Md from '@/fronted/components/chat/markdown'; 4 | import AiNormalMessage from "@/common/types/msg/AiNormalMessage"; 5 | import MsgDelete from '@/fronted/components/chat/msg/MsgDelete'; 6 | import useDpTaskViewer from '@/fronted/hooks/useDpTaskViewer'; 7 | 8 | export function AiNormalMsg({msg}: { msg: AiNormalMessage }) { 9 | 10 | const {detail: content}= useDpTaskViewer(msg.taskId, true); 11 | 12 | return ( 13 |
    14 | 15 |
    17 | 18 |
    19 |
    20 | 21 | {content??''} 22 | 23 |
    24 |
    25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/fronted/components/chat/msg/HumanNormalMsg.tsx: -------------------------------------------------------------------------------- 1 | import {IconUser} from "@/fronted/components/chat/icons"; 2 | import Md from "@/fronted/components/chat/markdown"; 3 | import HumanNormalMessage from "@/common/types/msg/HumanNormalMessage"; 4 | import MsgDelete from '@/fronted/components/chat/msg/MsgDelete'; 5 | 6 | export default function HumanNormalMsg({msg}: { msg: HumanNormalMessage }) { 7 | return ( 8 |
    9 | 10 |
    12 | 13 |
    14 |
    15 | 16 | {msg.content} 17 | 18 |
    19 |
    20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/fronted/components/chat/msg/MsgDelete.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@/fronted/components/ui/button'; 2 | import { Trash2 } from 'lucide-react'; 3 | import useChatPanel from '@/fronted/hooks/useChatPanel'; 4 | import CustomMessage from '@/common/types/msg/interfaces/CustomMessage'; 5 | 6 | const MsgDelete = ({ msg }: { msg: CustomMessage }) => { 7 | const deleteMessage = useChatPanel(s => s.deleteMessage).bind(null, msg); 8 | return ( 9 | 14 | 15 | ); 16 | }; 17 | 18 | export default MsgDelete; 19 | -------------------------------------------------------------------------------- /src/fronted/components/chat/spinner.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | export const spinner = ( 4 | 14 | 15 | 16 | ) 17 | -------------------------------------------------------------------------------- /src/fronted/components/playerSubtitle/PlayerSubtitlePanel.tsx: -------------------------------------------------------------------------------- 1 | import {cn} from "@/fronted/lib/utils"; 2 | import PlayerSubtitle from "@/fronted/components/playerSubtitle/PlayerSubtitle"; 3 | import PlayerSubtitleControlPanel from "@/fronted/components/playerSubtitle/PlayerSubtitleControlPannel"; 4 | import usePlayerController from "@/fronted/hooks/usePlayerController"; 5 | import {useShallow} from "zustand/react/shallow"; 6 | 7 | 8 | const PlayerSubtitlePanel = () => { 9 | const { 10 | playing, 11 | play, 12 | pause, 13 | seekTo 14 | 15 | } = usePlayerController( 16 | useShallow((state) => ({ 17 | playing: state.playing, 18 | play: state.play, 19 | pause: state.pause, 20 | seekTo: state.seekTo, 21 | })) 22 | ); 23 | return ( 24 |
    25 | 26 | { 28 | seekTo({time}); 29 | }} 30 | onPause={() => { 31 | pause(); 32 | }} 33 | onPlay={() => { 34 | play(); 35 | }} 36 | playing={playing} 37 | /> 38 |
    39 | ) 40 | } 41 | 42 | export default PlayerSubtitlePanel; 43 | -------------------------------------------------------------------------------- /src/fronted/components/query/StringQuery.tsx: -------------------------------------------------------------------------------- 1 | import { Search } from 'lucide-react'; 2 | import { Input } from '@/fronted/components/ui/input'; 3 | import React from 'react'; 4 | import { Button } from '@/fronted/components/ui/button'; 5 | 6 | const StringQuery = ({ 7 | query, setQuery, onKeywordRangeChange 8 | }: { 9 | query?: string, 10 | setQuery?: (query: string) => void, 11 | onKeywordRangeChange?: (keywordRange: 'clip' | 'context') => void 12 | } 13 | ) => { 14 | const [keywordRange, setKeywordRange] = React.useState<'clip' | 'context'>('clip'); 15 | return ( 16 |
    17 | 18 | setQuery?.(e.target.value)} 24 | /> 25 | 34 |
    35 | ); 36 | }; 37 | 38 | export default StringQuery; 39 | -------------------------------------------------------------------------------- /src/fronted/components/setting/FooterWrapper.tsx: -------------------------------------------------------------------------------- 1 | export interface FooterWrapperProps { 2 | children: React.ReactNode; 3 | } 4 | const FooterWrapper = ({ children }: FooterWrapperProps) => { 5 | return ( 6 |
    7 | {children} 8 |
    9 | ); 10 | }; 11 | 12 | export default FooterWrapper; 13 | -------------------------------------------------------------------------------- /src/fronted/components/setting/Header.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | 3 | export interface HeaderProps { 4 | title: string; 5 | description?: string | undefined | ReactElement; 6 | } 7 | 8 | const Header = ({ title, description }: HeaderProps) => { 9 | return ( 10 |
    11 |

    {title}

    12 | {description} 13 |
    14 | ); 15 | }; 16 | 17 | Header.defaultProps = { 18 | description: undefined 19 | }; 20 | 21 | export default Header; 22 | -------------------------------------------------------------------------------- /src/fronted/components/setting/ItemWrapper.tsx: -------------------------------------------------------------------------------- 1 | export interface ItemWapperProps { 2 | children: React.ReactNode; 3 | } 4 | const ItemWrapper = ({ children }: ItemWapperProps) => { 5 | return ( 6 |
    7 | {children} 8 |
    9 | ); 10 | }; 11 | 12 | export default ItemWrapper; 13 | -------------------------------------------------------------------------------- /src/fronted/components/setting/SettingInput.tsx: -------------------------------------------------------------------------------- 1 | import {cn} from "@/fronted/lib/utils"; 2 | import {Input} from "@/fronted/components/ui/input"; 3 | import {Label} from "@/fronted/components/ui/label"; 4 | import * as React from "react"; 5 | 6 | export interface SettingInputProps { 7 | title: string; 8 | placeHolder?: string; 9 | value: string; 10 | setValue: (value: string) => void; 11 | type?: string; 12 | inputWidth?: string; 13 | description?: string; 14 | className?: string; 15 | } 16 | 17 | const SettingInput = ({ 18 | title, 19 | description, 20 | placeHolder, 21 | value, 22 | setValue, 23 | type, 24 | inputWidth, 25 | className, 26 | }: SettingInputProps) => { 27 | return ( 28 |
    29 | 30 | setValue(event.target.value)} 35 | placeholder={placeHolder}/> 36 |

    41 | {description} 42 |

    43 |
    44 | ); 45 | }; 46 | 47 | SettingInput.defaultProps = { 48 | placeHolder: '', 49 | type: 'text', 50 | inputWidth: 'w-96', 51 | description: '', 52 | className: '', 53 | }; 54 | export default SettingInput; 55 | -------------------------------------------------------------------------------- /src/fronted/components/setting/SliderInput.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { cn } from '@/fronted/lib/utils'; 3 | import {Slider} from "@/fronted/components/ui/slider"; 4 | 5 | export interface SliderInputProps { 6 | title: string; 7 | values: string[]; 8 | defaultValue: string; 9 | setValue: (value: string) => void; 10 | inputWidth?: string; 11 | } 12 | const SliderInput = ({ 13 | title, 14 | values, 15 | defaultValue, 16 | setValue, 17 | inputWidth, 18 | }: SliderInputProps) => { 19 | const [localValue, setLocalValue] = useState(defaultValue); 20 | useEffect(() => { 21 | setLocalValue(defaultValue); 22 | }, [defaultValue]); 23 | return ( 24 |
    25 |
    {title} :
    26 | { 33 | setLocalValue(values[value[0]]); 34 | }} 35 | onValueCommit={(value) => { 36 | setValue(values[value[0]]); 37 | }} 38 | /> 39 |
    {localValue}
    40 |
    41 | ); 42 | }; 43 | 44 | SliderInput.defaultProps = { 45 | inputWidth: 'w-44', 46 | }; 47 | export default SliderInput; 48 | -------------------------------------------------------------------------------- /src/fronted/components/setting/Title.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | 3 | export interface HeaderProps { 4 | title: string; 5 | description?: string | undefined | ReactElement; 6 | } 7 | 8 | const Title = ({ title, description }: HeaderProps) => { 9 | return ( 10 |
    11 |

    {title}

    12 | {description} 13 |
    14 | ); 15 | }; 16 | 17 | Title.defaultProps = { 18 | description: undefined 19 | }; 20 | 21 | export default Title; 22 | -------------------------------------------------------------------------------- /src/fronted/components/setting/index.ts: -------------------------------------------------------------------------------- 1 | import FooterWrapper from './FooterWrapper'; 2 | import Header from './Header'; 3 | import ItemWrapper from './ItemWrapper'; 4 | import SettingInput from './SettingInput'; 5 | import SliderInput from './SliderInput'; 6 | import Title from './Title'; 7 | 8 | export { 9 | FooterWrapper, 10 | Header, 11 | ItemWrapper, 12 | SettingInput, 13 | SliderInput, 14 | Title, 15 | } 16 | -------------------------------------------------------------------------------- /src/fronted/components/short-cut/GlobalShortCut.tsx: -------------------------------------------------------------------------------- 1 | import useSetting from '../../hooks/useSetting'; 2 | import {useHotkeys} from "react-hotkeys-hook"; 3 | 4 | const process = (values: string) => values 5 | .split(',') 6 | .map((k) => k.replaceAll(' ', '')) 7 | .filter((k) => k !== '') 8 | export default function GlobalShortCut() { 9 | 10 | const setting = useSetting((s) => s.setting); 11 | const setSetting = useSetting((s) => s.setSetting); 12 | useHotkeys(process(setting('shortcut.nextTheme')), () => { 13 | setSetting('appearance.theme', setting('appearance.theme') === 'dark' ? 'light' : 'dark'); 14 | }); 15 | return <>; 16 | } 17 | -------------------------------------------------------------------------------- /src/fronted/components/toasts/ModeSwitchToast.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@/fronted/components/ui/button'; 2 | import React from 'react'; 3 | 4 | interface ModeSwitchToastProps { 5 | mode: 'podcast' | 'video'; 6 | onCancel: () => void; 7 | } 8 | 9 | const icons = { 10 | podcast: '🎙️', 11 | video: '📺' 12 | } as const; 13 | 14 | export function ModeSwitchToast({ mode, onCancel }: ModeSwitchToastProps) { 15 | return ( 16 |
    17 | 18 | {icons[mode]} 19 | {mode === 'podcast' ? 'Podcast Mode' : 'Video Mode'} Enabled 20 | 21 | 28 |
    29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/fronted/components/ui/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" 2 | 3 | const AspectRatio = AspectRatioPrimitive.Root 4 | 5 | export { AspectRatio } 6 | -------------------------------------------------------------------------------- /src/fronted/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/fronted/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
    33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /src/fronted/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox" 3 | import { Check } from "lucide-react" 4 | 5 | import { cn } from "@/fronted/lib/utils" 6 | 7 | const Checkbox = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >(({ className, ...props }, ref) => ( 11 | 19 | 22 | 23 | 24 | 25 | )) 26 | Checkbox.displayName = CheckboxPrimitive.Root.displayName 27 | 28 | export { Checkbox } 29 | -------------------------------------------------------------------------------- /src/fronted/components/ui/hover-card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card" 3 | 4 | import { cn } from "@/fronted/lib/utils" 5 | 6 | const HoverCard = HoverCardPrimitive.Root 7 | 8 | const HoverCardTrigger = HoverCardPrimitive.Trigger 9 | 10 | const HoverCardContent = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 14 | 24 | )) 25 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName 26 | 27 | export { HoverCard, HoverCardTrigger, HoverCardContent } 28 | -------------------------------------------------------------------------------- /src/fronted/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/fronted/lib/utils" 4 | 5 | export type InputProps = React.InputHTMLAttributes 6 | 7 | const Input = React.forwardRef( 8 | ({ className, type, ...props }, ref) => { 9 | return ( 10 | 19 | ) 20 | } 21 | ) 22 | Input.displayName = "Input" 23 | 24 | export { Input } 25 | -------------------------------------------------------------------------------- /src/fronted/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/fronted/lib/utils" 6 | 7 | const labelVariants = cva( 8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 9 | ) 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | VariantProps 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )) 22 | Label.displayName = LabelPrimitive.Root.displayName 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /src/fronted/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as PopoverPrimitive from "@radix-ui/react-popover" 3 | 4 | import { cn } from "@/fronted/lib/utils" 5 | 6 | const Popover = PopoverPrimitive.Root 7 | 8 | const PopoverTrigger = PopoverPrimitive.Trigger 9 | 10 | const PopoverContent = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 14 | 15 | 25 | 26 | )) 27 | PopoverContent.displayName = PopoverPrimitive.Content.displayName 28 | 29 | export { Popover, PopoverTrigger, PopoverContent } 30 | -------------------------------------------------------------------------------- /src/fronted/components/ui/progress.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as ProgressPrimitive from "@radix-ui/react-progress" 3 | 4 | import { cn } from "@/fronted/lib/utils" 5 | 6 | const Progress = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, value, ...props }, ref) => ( 10 | 18 | 22 | 23 | )) 24 | Progress.displayName = ProgressPrimitive.Root.displayName 25 | 26 | export { Progress } 27 | -------------------------------------------------------------------------------- /src/fronted/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" 3 | 4 | import { cn } from "@/fronted/lib/utils" 5 | 6 | const ScrollArea = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, children, ...props }, ref) => ( 10 | 15 | 16 | {children} 17 | 18 | 19 | 20 | 21 | )) 22 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName 23 | 24 | const ScrollBar = React.forwardRef< 25 | React.ElementRef, 26 | React.ComponentPropsWithoutRef 27 | >(({ className, orientation = "vertical", ...props }, ref) => ( 28 | 41 | 42 | 43 | )) 44 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName 45 | 46 | export { ScrollArea, ScrollBar } 47 | -------------------------------------------------------------------------------- /src/fronted/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/fronted/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
    12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /src/fronted/components/ui/slider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as SliderPrimitive from "@radix-ui/react-slider" 3 | 4 | import { cn } from "@/fronted/lib/utils" 5 | 6 | const Slider = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 18 | 19 | 20 | 21 | 22 | 23 | )) 24 | Slider.displayName = SliderPrimitive.Root.displayName 25 | 26 | export { Slider } 27 | -------------------------------------------------------------------------------- /src/fronted/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from "next-themes" 2 | import { Toaster as Sonner } from "sonner" 3 | 4 | type ToasterProps = React.ComponentProps 5 | 6 | const Toaster = ({ ...props }: ToasterProps) => { 7 | const { theme = "system" } = useTheme() 8 | 9 | return ( 10 | 26 | ) 27 | } 28 | 29 | export { Toaster } 30 | -------------------------------------------------------------------------------- /src/fronted/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as SwitchPrimitives from "@radix-ui/react-switch" 3 | 4 | import { cn } from "@/fronted/lib/utils" 5 | 6 | const Switch = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 18 | 23 | 24 | )) 25 | Switch.displayName = SwitchPrimitives.Root.displayName 26 | 27 | export { Switch } 28 | -------------------------------------------------------------------------------- /src/fronted/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/fronted/lib/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |