├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── actions │ └── build-package │ │ └── action.yml └── workflows │ ├── build.yml │ ├── stable-release.yml │ └── testing-release.yml ├── .gitignore ├── FFmpeg ├── avcodec-61.dll ├── avdevice-61.dll ├── avfilter-10.dll ├── avformat-61.dll ├── avutil-59.dll ├── postproc-58.dll ├── swresample-5.dll └── swscale-8.dll ├── FlyleafLib ├── AssemblyInfo.cs ├── Controls │ ├── IHostPlayer.cs │ └── WPF │ │ ├── Converters.cs │ │ ├── FlyleafHost.cs │ │ ├── PlayerDebug.xaml │ │ ├── PlayerDebug.xaml.cs │ │ ├── RelayCommand.cs │ │ └── RelayCommandSimple.cs ├── Engine │ ├── Config.cs │ ├── Engine.Audio.cs │ ├── Engine.FFmpeg.cs │ ├── Engine.Plugins.cs │ ├── Engine.Video.cs │ ├── Engine.cs │ ├── Globals.cs │ ├── Language.cs │ ├── TesseractModel.cs │ ├── WhisperConfig.cs │ ├── WhisperCppModel.cs │ └── WhisperLanguage.cs ├── FlyleafLib.csproj ├── MediaFramework │ ├── MediaContext │ │ ├── DecoderContext.Open.cs │ │ ├── DecoderContext.cs │ │ └── Downloader.cs │ ├── MediaDecoder │ │ ├── AudioDecoder.Filters.cs │ │ ├── AudioDecoder.cs │ │ ├── DataDecoder.cs │ │ ├── DecoderBase.cs │ │ ├── SubtitlesDecoder.cs │ │ └── VideoDecoder.cs │ ├── MediaDemuxer │ │ ├── CustomIOContext.cs │ │ ├── Demuxer.cs │ │ ├── DemuxerInput.cs │ │ └── Interrupter.cs │ ├── MediaDevice │ │ ├── AudioDevice.cs │ │ ├── AudioDeviceStream.cs │ │ ├── DeviceBase.cs │ │ ├── DeviceStreamBase.cs │ │ ├── VideoDevice.cs │ │ └── VideoDeviceStream.cs │ ├── MediaFrame │ │ ├── AudioFrame.cs │ │ ├── DataFrame.cs │ │ ├── FrameBase.cs │ │ ├── SubtitlesFrame.cs │ │ └── VideoFrame.cs │ ├── MediaPlaylist │ │ ├── M3UPlaylist.cs │ │ ├── PLSPlaylist.cs │ │ ├── Playlist.cs │ │ ├── PlaylistItem.cs │ │ └── Session.cs │ ├── MediaProgram │ │ └── Program.cs │ ├── MediaRemuxer │ │ └── Remuxer.cs │ ├── MediaRenderer │ │ ├── Renderer.Device.cs │ │ ├── Renderer.PixelShader.cs │ │ ├── Renderer.Present.cs │ │ ├── Renderer.PresentOffline.cs │ │ ├── Renderer.SwapChain.cs │ │ ├── Renderer.VideoProcessor.cs │ │ ├── Renderer.cs │ │ └── ShaderCompiler.cs │ ├── MediaStream │ │ ├── AudioStream.cs │ │ ├── DataStream.cs │ │ ├── ExternalAudioStream.cs │ │ ├── ExternalStream.cs │ │ ├── ExternalSubtitlesStream.cs │ │ ├── ExternalVideoStream.cs │ │ ├── StreamBase.cs │ │ ├── SubtitlesStream.cs │ │ └── VideoStream.cs │ └── RunThreadBase.cs ├── MediaPlayer │ ├── Activity.cs │ ├── Audio.cs │ ├── Commands.cs │ ├── Data.cs │ ├── Player.Extra.cs │ ├── Player.Keys.cs │ ├── Player.Open.cs │ ├── Player.Playback.cs │ ├── Player.Screamers.cs │ ├── Player.cs │ ├── Subtitles.cs │ ├── SubtitlesASR.cs │ ├── SubtitlesManager.cs │ ├── SubtitlesOCR.cs │ ├── Translation │ │ ├── Services │ │ │ ├── DeepLTranslateService.cs │ │ │ ├── DeepLXTranslateService.cs │ │ │ ├── GoogleV1TranslateService.cs │ │ │ ├── ITranslateService.cs │ │ │ ├── ITranslateSettings.cs │ │ │ ├── OpenAIBaseTranslateService.cs │ │ │ └── TranslateServiceFactory.cs │ │ ├── SubtitlesTranslator.cs │ │ ├── TranslateChatConfig.cs │ │ └── TranslateLanguage.cs │ └── Video.cs ├── Plugins │ ├── OpenDefault.cs │ ├── OpenSubtitles.cs │ ├── PluginBase.cs │ ├── PluginHandler.cs │ └── StreamSuggester.cs ├── Themes │ └── Generic.xaml └── Utils │ ├── Disposable.cs │ ├── Logger.cs │ ├── NativeMethods.cs │ ├── ObservableDictionary.cs │ ├── SubtitleTextUtil.cs │ ├── TextEncodings.cs │ ├── Utils.cs │ └── ZOrderHandler.cs ├── FlyleafLibTests ├── FlyleafLibTests.csproj ├── MediaPlayer │ └── SubtitlesManagerTest.cs ├── Utils │ └── SubtitleTextUtilTests.cs └── xunit.runner.json ├── LICENSE ├── LLPlayer-screenshot.jpg ├── LLPlayer.png ├── LLPlayer.slnx ├── LLPlayer ├── App.xaml ├── App.xaml.cs ├── AssemblyInfo.cs ├── Assets │ ├── completion.mp3 │ └── kennedy.wav ├── Controls │ ├── AlignableWrapPanel.cs │ ├── FlyleafBar.xaml │ ├── FlyleafBar.xaml.cs │ ├── NonTopmostPopup.cs │ ├── OutlinedTextBlock.cs │ ├── SelectableSubtitleText.xaml │ ├── SelectableSubtitleText.xaml.cs │ ├── SelectableTextBox.cs │ ├── Settings │ │ ├── Controls │ │ │ ├── ColorPicker.xaml │ │ │ └── ColorPicker.xaml.cs │ │ ├── SettingsAbout.xaml │ │ ├── SettingsAbout.xaml.cs │ │ ├── SettingsAudio.xaml │ │ ├── SettingsAudio.xaml.cs │ │ ├── SettingsKeys.xaml │ │ ├── SettingsKeys.xaml.cs │ │ ├── SettingsKeysOffset.xaml │ │ ├── SettingsKeysOffset.xaml.cs │ │ ├── SettingsMouse.xaml │ │ ├── SettingsMouse.xaml.cs │ │ ├── SettingsPlayer.xaml │ │ ├── SettingsPlayer.xaml.cs │ │ ├── SettingsPlugins.xaml │ │ ├── SettingsPlugins.xaml.cs │ │ ├── SettingsSubtitles.xaml │ │ ├── SettingsSubtitles.xaml.cs │ │ ├── SettingsSubtitlesASR.xaml │ │ ├── SettingsSubtitlesASR.xaml.cs │ │ ├── SettingsSubtitlesAction.xaml │ │ ├── SettingsSubtitlesAction.xaml.cs │ │ ├── SettingsSubtitlesOCR.xaml │ │ ├── SettingsSubtitlesOCR.xaml.cs │ │ ├── SettingsSubtitlesPS.xaml │ │ ├── SettingsSubtitlesPS.xaml.cs │ │ ├── SettingsSubtitlesTrans.xaml │ │ ├── SettingsSubtitlesTrans.xaml.cs │ │ ├── SettingsThemes.xaml │ │ ├── SettingsThemes.xaml.cs │ │ ├── SettingsVideo.xaml │ │ ├── SettingsVideo.xaml.cs │ │ └── Trans │ │ │ ├── OpenAIBaseTranslateControl.xaml │ │ │ └── OpenAIBaseTranslateControl.xaml.cs │ ├── SubtitlesControl.xaml │ ├── SubtitlesControl.xaml.cs │ ├── WordClickedEventArgs.cs │ ├── WordPopup.xaml │ └── WordPopup.xaml.cs ├── Converters │ ├── FlyleafConverters.cs │ ├── GeneralConverters.cs │ └── SubtitleConverters.cs ├── Extensions │ ├── Bindable.cs │ ├── ColorHexJsonConverter.cs │ ├── DataGridRowOrderBehavior.cs │ ├── ExtendedDialogService.cs │ ├── FileHelper.cs │ ├── FocusBehavior.cs │ ├── Guards.cs │ ├── HyperLinkHelper.cs │ ├── JsonInterfaceConcreteConverter.cs │ ├── MyDialogWindow.xaml │ ├── MyDialogWindow.xaml.cs │ ├── ScrollParentWhenAtMax.cs │ ├── StringExtensions.cs │ ├── TextBoxHelper.cs │ ├── TextBoxMiscHelper.cs │ ├── UIHelper.cs │ └── WindowsClipboard.cs ├── GlobalUsings.cs ├── LLPlayer.csproj ├── LLPlayer.ico ├── Properties │ ├── PublishProfiles │ │ └── FolderProfile.pubxml │ └── launchSettings.json ├── Resources │ ├── Converters.xaml │ ├── Images │ │ ├── pause.png │ │ └── play.png │ ├── MaterialDesignMy.xaml │ ├── MaterialDesignMy.xaml.cs │ ├── PopupMenu.xaml │ ├── PopupMenu.xaml.cs │ ├── Slider.xaml │ ├── Validators.xaml │ └── Validators.xaml.cs ├── Services │ ├── AppActions.cs │ ├── AppConfig.cs │ ├── ErrorDialogHelper.cs │ ├── FlyleafLoader.cs │ ├── FlyleafManager.cs │ ├── OpenSubtitlesProvider.cs │ ├── PDICSender.cs │ ├── SrtExporter.cs │ └── WhisperCppModelLoader.cs ├── Themes │ ├── Generic.xaml │ └── SelectableTextBox.xaml ├── ViewModels │ ├── CheatSheetDialogVM.cs │ ├── ErrorDialogVM.cs │ ├── FlyleafOverlayVM.cs │ ├── MainWindowVM.cs │ ├── SelectLanguageDialogVM.cs │ ├── SettingsDialogVM.cs │ ├── SubtitlesDownloaderDialogVM.cs │ ├── SubtitlesExportDialogVM.cs │ ├── SubtitlesSidebarVM.cs │ ├── TesseractDownloadDialogVM.cs │ ├── WhisperEngineDownloadDialogVM.cs │ └── WhisperModelDownloadDialogVM.cs ├── Views │ ├── CheatSheetDialog.xaml │ ├── CheatSheetDialog.xaml.cs │ ├── ErrorDialog.xaml │ ├── ErrorDialog.xaml.cs │ ├── FlyleafOverlay.xaml │ ├── FlyleafOverlay.xaml.cs │ ├── MainWindow.xaml │ ├── MainWindow.xaml.cs │ ├── SelectLanguageDialog.xaml │ ├── SelectLanguageDialog.xaml.cs │ ├── SettingsDialog.xaml │ ├── SettingsDialog.xaml.cs │ ├── SubtitlesDownloaderDialog.xaml │ ├── SubtitlesDownloaderDialog.xaml.cs │ ├── SubtitlesExportDialog.xaml │ ├── SubtitlesExportDialog.xaml.cs │ ├── SubtitlesSidebar.xaml │ ├── SubtitlesSidebar.xaml.cs │ ├── TesseractDownloadDialog.xaml │ ├── TesseractDownloadDialog.xaml.cs │ ├── WhisperEngineDownloadDialog.xaml │ ├── WhisperEngineDownloadDialog.xaml.cs │ ├── WhisperModelDownloadDialog.xaml │ └── WhisperModelDownloadDialog.xaml.cs └── lib │ ├── 7z.dll │ └── license.7z.txt ├── Plugins └── YoutubeDL │ ├── Libs │ └── yt-dlp.exe_here │ ├── Properties │ └── PublishProfiles │ │ └── FolderProfile.pubxml │ ├── YoutubeDL.cs │ ├── YoutubeDL.csproj │ └── YoutubeDLJson.cs ├── README.md └── WpfColorFontDialog ├── AvailableColors.cs ├── ColorFontChooser.xaml ├── ColorFontChooser.xaml.cs ├── ColorFontDialog.xaml ├── ColorFontDialog.xaml.cs ├── ColorPicker.xaml ├── ColorPicker.xaml.cs ├── ColorPickerViewModel.cs ├── FontColor.cs ├── FontInfo.cs ├── FontSizeListBoxItemToDoubleConverter.cs ├── FontValueConverter.cs ├── I18NUtil.cs ├── I18n ├── en-US.xaml └── zh-CN.xaml ├── LICENSE ├── WpfColorFontDialog.csproj └── resources └── colorfont_icon.png /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | insert_final_newline = true 5 | trim_trailing_whitespace = true 6 | 7 | [*.{cs,xaml}] 8 | # In Visual Studio 2022 and Rider (2025), even if utf-8 is specified, new files will have bom. so set this to avoid confusion 9 | charset = utf-8-bom 10 | indent_style = space 11 | indent_size = 4 12 | # to distinguish 13 | tab_width = 8 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Environment (please complete the following information):** 27 | 28 | The version can be obtained from the `About` section of the settings. 29 | 30 | - OS: [e.g. Windows 10, 11] 31 | - CPU Architecture: [e.g. x64, x32, ARM64] 32 | - LLPlayer version: [e.g. v0.0.1] 33 | - LLPlayer commit version: [e.g. 86f063df2c56a198b204664951a6147ee739f1b7] 34 | 35 | If the bug relates to `Whisper` / `ASR`, please complete the following. 36 | 37 | - GPU: [e.g. NVIDIA RTX 4060] 38 | - Whisper Hardware Options: [e.g. Cuda -> Cpu -> CpuNoAvx] 39 | - [Microsoft Visual C++ Redistributable Version >=2022](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170#latest-microsoft-visual-c-redistributable-version) Installed: [e.g. True] 40 | 41 | If the bug relates to the specific media file, please complete the following. 42 | 43 | - local or online video: [e.g. local] 44 | - [MediaInfo](https://mediaarea.net/MediaInfo) results (if local): 45 | 46 | ``` 47 | ``` 48 | 49 | - URL (if online): [e.g. https://www.youtube.com/watch?v=aaaa] 50 | 51 | **Additional context** 52 | Add any other context about the problem here. 53 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/actions/build-package/action.yml: -------------------------------------------------------------------------------- 1 | name: "Build & Package" 2 | description: "Builds the solution, clean, archive with 7z" 3 | inputs: 4 | archive-name: 5 | description: 'archive name including extension' 6 | required: true 7 | runs: 8 | using: "composite" 9 | steps: 10 | - name: Setup .NET 11 | uses: actions/setup-dotnet@v4 12 | with: 13 | dotnet-version: 9.0.x 14 | 15 | - name: Restore dependencies 16 | shell: pwsh 17 | run: dotnet restore 18 | 19 | - name: Build App 20 | shell: pwsh 21 | run: | 22 | dotnet publish .\LLPlayer\LLPlayer.csproj ` 23 | --configuration Release ` 24 | --runtime win-x64 ` 25 | --self-contained false ` 26 | -p:PublishSingleFile=true ` 27 | -p:PublishReadyToRun=true ` 28 | --output ${{ runner.temp }}\publish 29 | 30 | - name: Clean build 31 | shell: pwsh 32 | run: | 33 | $pub = "${{ runner.temp }}\publish" 34 | 35 | # Clean-up whisper.net 36 | $pathsToRemove = @( 37 | "$pub\runtimes\noavx\linux-x64" 38 | "$pub\runtimes\noavx\win-x86" 39 | "$pub\runtimes\openvino\linux-x64" 40 | "$pub\runtimes\vulkan\linux-x64" 41 | "$pub\runtimes\win-arm64" 42 | "$pub\runtimes\win-x86" 43 | ) 44 | Remove-Item -Recurse $pathsToRemove 45 | 46 | # Clean-up TesseractOCR 47 | Remove-Item -Recurse "$pub\x86" 48 | 49 | # Copy FFmpeg 50 | Copy-Item .\FFmpeg -Destination $pub -Recurse 51 | 52 | - name: Build Plugin (YoutubeDL) 53 | shell: pwsh 54 | run: | 55 | dotnet publish .\Plugins\YoutubeDL\YoutubeDL.csproj ` 56 | --configuration Release ` 57 | --runtime win-x64 ` 58 | --framework net9.0-windows10.0.18362.0 ` 59 | --self-contained false ` 60 | -p:PublishSingleFile=false ` 61 | -p:PublishReadyToRun=true ` 62 | --output ${{ runner.temp }}\publish-YoutubeDL 63 | 64 | - name: Copy Plugin DLLs 65 | shell: pwsh 66 | run: | 67 | $pub = "${{ runner.temp }}\publish" 68 | $pubY = "${{ runner.temp }}\publish-YoutubeDL" 69 | New-Item -ItemType Directory -Path "$pub\Plugins\YoutubeDL" 70 | Copy-Item "$pubY\YoutubeDL.dll","$pubY\YoutubeDL.pdb" -Destination "$pub\Plugins\YoutubeDL" 71 | 72 | - name: Get latest yt-dlp release 73 | id: fetch-yt 74 | uses: pozetroninc/github-action-get-latest-release@master 75 | with: 76 | repository: yt-dlp/yt-dlp 77 | excludes: prerelease,draft 78 | 79 | - name: Download yt-dlp.exe 80 | shell: pwsh 81 | run: | 82 | $pub = "${{ runner.temp }}\publish" 83 | $ver = "${{ steps.fetch-yt.outputs.release }}" 84 | $url = "https://github.com/yt-dlp/yt-dlp/releases/download/$ver/yt-dlp.exe" 85 | $outDir = "$pub\Plugins\YoutubeDL" 86 | New-Item -Path "$outDir\yt-dlp.exe_here" -ItemType File 87 | Invoke-WebRequest -Uri "$url" -OutFile "$outDir\yt-dlp.exe" 88 | 89 | - name: Archive with 7-Zip 90 | id: archive 91 | shell: pwsh 92 | run: | 93 | $pub = "${{ runner.temp }}\publish" 94 | $out = "${{ inputs.archive-name }}" 95 | & "C:\Program Files\7-Zip\7z.exe" a -t7z -mx=8 -mmt=4 "$out" "$pub\*" 96 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: windows-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Setup .NET 17 | uses: actions/setup-dotnet@v4 18 | with: 19 | dotnet-version: 9.0.x 20 | 21 | - name: Restore dependencies 22 | run: dotnet restore 23 | 24 | - name: Build App 25 | run: dotnet build --no-restore -warnaserror .\LLPlayer 26 | 27 | - name: Build Plugin (YoutubeDL) 28 | run: dotnet build --no-restore -warnaserror .\Plugins\YoutubeDL 29 | 30 | - name: Test 31 | run: dotnet test --no-restore .\FlyleafLibTests 32 | -------------------------------------------------------------------------------- /.github/workflows/stable-release.yml: -------------------------------------------------------------------------------- 1 | name: Stable Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | env: 12 | ARCHIVE_NAME: LLPlayer-${{ github.ref_name }}-x64.7z 13 | 14 | jobs: 15 | release: 16 | runs-on: windows-latest 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | with: 22 | ref: ${{ github.sha }} 23 | 24 | - name: Build & Package 25 | uses: ./.github/actions/build-package 26 | with: 27 | archive-name: ${{ env.ARCHIVE_NAME }} 28 | 29 | - name: Create or update GitHub Draft Release & Upload Asset 30 | uses: softprops/action-gh-release@v2 31 | with: 32 | tag_name: ${{ github.ref_name }} 33 | name: "${{ github.ref_name }}" 34 | draft: true # created as draft, later published manually 35 | prerelease: false 36 | files: ${{ env.ARCHIVE_NAME }} 37 | -------------------------------------------------------------------------------- /.github/workflows/testing-release.yml: -------------------------------------------------------------------------------- 1 | name: Testing Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | commit: 7 | description: 'Build Commit Hash or ref' 8 | required: true 9 | 10 | permissions: 11 | contents: write 12 | 13 | jobs: 14 | release: 15 | runs-on: windows-latest 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | with: 21 | ref: ${{ github.event.inputs.commit }} 22 | 23 | - name: Get short commit hash 24 | id: short-hash 25 | run: | 26 | $short = git rev-parse --short ${{ github.event.inputs.commit }} 27 | "sha=$short" >> $env:GITHUB_OUTPUT 28 | 29 | - name: Get latest stable release tag 30 | id: latest-tag 31 | uses: actions/github-script@v7 32 | with: 33 | github-token: ${{ secrets.GITHUB_TOKEN }} 34 | script: | 35 | const latest = await github.rest.repos.getLatestRelease({ 36 | owner: context.repo.owner, 37 | repo: context.repo.repo 38 | }); 39 | return latest.data.tag_name; 40 | 41 | - name: Set archive name 42 | id: archive-name 43 | run: | 44 | $tag = ${{ steps.latest-tag.outputs.result }} 45 | $hash = "${{ steps.short-hash.outputs.sha }}" 46 | 47 | "name=LLPlayer-testing-$tag-$hash.7z" >> $env:GITHUB_OUTPUT 48 | 49 | - name: Build & Package 50 | uses: ./.github/actions/build-package 51 | with: 52 | archive-name: ${{ steps.archive-name.outputs.name }} 53 | 54 | - name: Upload Testing Asset (overwrite) 55 | run: | 56 | gh release upload v0.0.1 ${{ steps.archive-name.outputs.name }} --clobber 57 | env: 58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 59 | -------------------------------------------------------------------------------- /FFmpeg/avcodec-61.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umlx5h/LLPlayer/878511b033609f4f5e7f57f92ddf8bde10f607c8/FFmpeg/avcodec-61.dll -------------------------------------------------------------------------------- /FFmpeg/avdevice-61.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umlx5h/LLPlayer/878511b033609f4f5e7f57f92ddf8bde10f607c8/FFmpeg/avdevice-61.dll -------------------------------------------------------------------------------- /FFmpeg/avfilter-10.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umlx5h/LLPlayer/878511b033609f4f5e7f57f92ddf8bde10f607c8/FFmpeg/avfilter-10.dll -------------------------------------------------------------------------------- /FFmpeg/avformat-61.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umlx5h/LLPlayer/878511b033609f4f5e7f57f92ddf8bde10f607c8/FFmpeg/avformat-61.dll -------------------------------------------------------------------------------- /FFmpeg/avutil-59.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umlx5h/LLPlayer/878511b033609f4f5e7f57f92ddf8bde10f607c8/FFmpeg/avutil-59.dll -------------------------------------------------------------------------------- /FFmpeg/postproc-58.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umlx5h/LLPlayer/878511b033609f4f5e7f57f92ddf8bde10f607c8/FFmpeg/postproc-58.dll -------------------------------------------------------------------------------- /FFmpeg/swresample-5.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umlx5h/LLPlayer/878511b033609f4f5e7f57f92ddf8bde10f607c8/FFmpeg/swresample-5.dll -------------------------------------------------------------------------------- /FFmpeg/swscale-8.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umlx5h/LLPlayer/878511b033609f4f5e7f57f92ddf8bde10f607c8/FFmpeg/swscale-8.dll -------------------------------------------------------------------------------- /FlyleafLib/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | [assembly: ThemeInfo( 4 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 5 | //(used if a resource is not found in the page, 6 | // or application resource dictionaries) 7 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 8 | //(used if a resource is not found in the page, 9 | // app, or any theme specific resource dictionaries) 10 | )] 11 | -------------------------------------------------------------------------------- /FlyleafLib/Controls/IHostPlayer.cs: -------------------------------------------------------------------------------- 1 | namespace FlyleafLib.Controls; 2 | 3 | public interface IHostPlayer 4 | { 5 | bool Player_CanHideCursor(); 6 | bool Player_GetFullScreen(); 7 | void Player_SetFullScreen(bool value); 8 | void Player_Disposed(); 9 | } 10 | -------------------------------------------------------------------------------- /FlyleafLib/Controls/WPF/Converters.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Windows.Data; 3 | 4 | namespace FlyleafLib.Controls.WPF; 5 | 6 | [ValueConversion(typeof(long), typeof(TimeSpan))] 7 | public class TicksToTimeSpanConverter : IValueConverter 8 | { 9 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) => new TimeSpan((long)value); 10 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => ((TimeSpan)value).Ticks; 11 | } 12 | 13 | [ValueConversion(typeof(long), typeof(string))] 14 | public class TicksToTimeConverter : IValueConverter 15 | { 16 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) => new TimeSpan((long)value).ToString(@"hh\:mm\:ss"); 17 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException(); 18 | } 19 | 20 | [ValueConversion(typeof(long), typeof(double))] 21 | public class TicksToSecondsConverter : IValueConverter 22 | { 23 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) => (long)value / 10000000.0; 24 | 25 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => (long)((double)value * 10000000); 26 | } 27 | 28 | [ValueConversion(typeof(long), typeof(int))] 29 | public class TicksToMilliSecondsConverter : IValueConverter 30 | { 31 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) => (int)((long)value / 10000); 32 | 33 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => long.Parse(value.ToString()) * 10000; 34 | } 35 | 36 | [ValueConversion(typeof(AspectRatio), typeof(string))] 37 | public class StringToRationalConverter : IValueConverter 38 | { 39 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) => value.ToString(); 40 | 41 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => new AspectRatio(value.ToString()); 42 | } 43 | -------------------------------------------------------------------------------- /FlyleafLib/Controls/WPF/PlayerDebug.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Controls; 3 | using System.Windows.Media; 4 | 5 | using FlyleafLib.MediaPlayer; 6 | 7 | namespace FlyleafLib.Controls.WPF; 8 | 9 | public partial class PlayerDebug : UserControl 10 | { 11 | public Player Player 12 | { 13 | get => (Player)GetValue(PlayerProperty); 14 | set => SetValue(PlayerProperty, value); 15 | } 16 | 17 | public static readonly DependencyProperty PlayerProperty = 18 | DependencyProperty.Register("Player", typeof(Player), typeof(PlayerDebug), new PropertyMetadata(null)); 19 | 20 | public Brush BoxColor 21 | { 22 | get => (Brush)GetValue(BoxColorProperty); 23 | set => SetValue(BoxColorProperty, value); 24 | } 25 | 26 | public static readonly DependencyProperty BoxColorProperty = 27 | DependencyProperty.Register("BoxColor", typeof(Brush), typeof(PlayerDebug), new PropertyMetadata(new SolidColorBrush((Color)ColorConverter.ConvertFromString("#D0000000")))); 28 | 29 | public Brush HeaderColor 30 | { 31 | get => (Brush)GetValue(HeaderColorProperty); 32 | set => SetValue(HeaderColorProperty, value); 33 | } 34 | 35 | public static readonly DependencyProperty HeaderColorProperty = 36 | DependencyProperty.Register("HeaderColor", typeof(Brush), typeof(PlayerDebug), new PropertyMetadata(new SolidColorBrush(Colors.LightSalmon))); 37 | 38 | public Brush InfoColor 39 | { 40 | get => (Brush)GetValue(InfoColorProperty); 41 | set => SetValue(InfoColorProperty, value); 42 | } 43 | 44 | public static readonly DependencyProperty InfoColorProperty = 45 | DependencyProperty.Register("InfoColor", typeof(Brush), typeof(PlayerDebug), new PropertyMetadata(new SolidColorBrush(Colors.LightSteelBlue))); 46 | 47 | public Brush ValueColor 48 | { 49 | get => (Brush)GetValue(ValueColorProperty); 50 | set => SetValue(ValueColorProperty, value); 51 | } 52 | 53 | public static readonly DependencyProperty ValueColorProperty = 54 | DependencyProperty.Register("ValueColor", typeof(Brush), typeof(PlayerDebug), new PropertyMetadata(new SolidColorBrush(Colors.White))); 55 | 56 | public PlayerDebug() => InitializeComponent(); 57 | } 58 | -------------------------------------------------------------------------------- /FlyleafLib/Controls/WPF/RelayCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows.Input; 3 | 4 | namespace FlyleafLib.Controls.WPF; 5 | 6 | public class RelayCommand : ICommand 7 | { 8 | private Action execute; 9 | 10 | private Predicate canExecute; 11 | 12 | private event EventHandler CanExecuteChangedInternal; 13 | 14 | public RelayCommand(Action execute) : this(execute, DefaultCanExecute) { } 15 | 16 | public RelayCommand(Action execute, Predicate canExecute) 17 | { 18 | this.execute = execute ?? throw new ArgumentNullException("execute"); 19 | this.canExecute = canExecute ?? throw new ArgumentNullException("canExecute"); 20 | } 21 | 22 | public event EventHandler CanExecuteChanged 23 | { 24 | add 25 | { 26 | CommandManager.RequerySuggested += value; 27 | CanExecuteChangedInternal += value; 28 | } 29 | 30 | remove 31 | { 32 | CommandManager.RequerySuggested -= value; 33 | CanExecuteChangedInternal -= value; 34 | } 35 | } 36 | 37 | private static bool DefaultCanExecute(object parameter) => true; 38 | public bool CanExecute(object parameter) => canExecute != null && canExecute(parameter); 39 | 40 | public void Execute(object parameter) => execute(parameter); 41 | 42 | public void OnCanExecuteChanged() 43 | { 44 | var handler = CanExecuteChangedInternal; 45 | handler?.Invoke(this, EventArgs.Empty); 46 | //CommandManager.InvalidateRequerySuggested(); 47 | } 48 | 49 | public void Destroy() 50 | { 51 | canExecute = _ => false; 52 | execute = _ => { return; }; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /FlyleafLib/Controls/WPF/RelayCommandSimple.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows.Input; 3 | 4 | namespace FlyleafLib.Controls.WPF; 5 | 6 | public class RelayCommandSimple : ICommand 7 | { 8 | public event EventHandler CanExecuteChanged { add { } remove { } } 9 | Action execute; 10 | 11 | public RelayCommandSimple(Action execute) => this.execute = execute; 12 | public bool CanExecute(object parameter) => true; 13 | public void Execute(object parameter) => execute(); 14 | } 15 | -------------------------------------------------------------------------------- /FlyleafLib/Engine/Engine.FFmpeg.cs: -------------------------------------------------------------------------------- 1 | namespace FlyleafLib; 2 | 3 | public class FFmpegEngine 4 | { 5 | public string Folder { get; private set; } 6 | public string Version { get; private set; } 7 | 8 | const int AV_LOG_BUFFER_SIZE = 5 * 1024; 9 | internal AVRational AV_TIMEBASE_Q; 10 | 11 | internal FFmpegEngine() 12 | { 13 | try 14 | { 15 | Engine.Log.Info($"Loading FFmpeg libraries from '{Engine.Config.FFmpegPath}'"); 16 | Folder = Utils.GetFolderPath(Engine.Config.FFmpegPath); 17 | LoadLibraries(Folder, Engine.Config.FFmpegLoadProfile); 18 | 19 | uint ver = avformat_version(); 20 | Version = $"{ver >> 16}.{(ver >> 8) & 255}.{ver & 255}"; 21 | 22 | SetLogLevel(); 23 | AV_TIMEBASE_Q = av_get_time_base_q(); 24 | Engine.Log.Info($"FFmpeg Loaded (Profile: {Engine.Config.FFmpegLoadProfile}, Location: {Folder}, FmtVer: {Version})"); 25 | } catch (Exception e) 26 | { 27 | Engine.Log.Error($"Loading FFmpeg libraries '{Engine.Config.FFmpegPath}' failed\r\n{e.Message}\r\n{e.StackTrace}"); 28 | throw new Exception($"Loading FFmpeg libraries '{Engine.Config.FFmpegPath}' failed"); 29 | } 30 | } 31 | 32 | internal static void SetLogLevel() 33 | { 34 | if (Engine.Config.FFmpegLogLevel != Flyleaf.FFmpeg.LogLevel.Quiet) 35 | { 36 | av_log_set_level(Engine.Config.FFmpegLogLevel); 37 | av_log_set_callback(LogFFmpeg); 38 | } 39 | else 40 | { 41 | av_log_set_level(Flyleaf.FFmpeg.LogLevel.Quiet); 42 | av_log_set_callback(null); 43 | } 44 | } 45 | 46 | internal unsafe static av_log_set_callback_callback LogFFmpeg = (p0, level, format, vl) => 47 | { 48 | if (level > av_log_get_level()) 49 | return; 50 | 51 | byte* buffer = stackalloc byte[AV_LOG_BUFFER_SIZE]; 52 | int printPrefix = 1; 53 | av_log_format_line2(p0, level, format, vl, buffer, AV_LOG_BUFFER_SIZE, &printPrefix); 54 | string line = Utils.BytePtrToStringUTF8(buffer); 55 | 56 | Logger.Output($"FFmpeg|{level,-7}|{line.Trim()}"); 57 | }; 58 | 59 | internal unsafe static string ErrorCodeToMsg(int error) 60 | { 61 | byte* buffer = stackalloc byte[AV_LOG_BUFFER_SIZE]; 62 | av_strerror(error, buffer, AV_LOG_BUFFER_SIZE); 63 | return Utils.BytePtrToStringUTF8(buffer); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /FlyleafLib/Engine/Engine.Plugins.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Reflection; 5 | 6 | using FlyleafLib.Plugins; 7 | 8 | namespace FlyleafLib; 9 | 10 | public class PluginsEngine 11 | { 12 | public Dictionary 13 | Types { get; private set; } = new Dictionary(); 14 | 15 | public string Folder { get; private set; } 16 | 17 | private Type pluginBaseType = typeof(PluginBase); 18 | 19 | internal PluginsEngine() 20 | { 21 | Folder = string.IsNullOrEmpty(Engine.Config.PluginsPath) ? null : Utils.GetFolderPath(Engine.Config.PluginsPath); 22 | 23 | LoadAssemblies(); 24 | } 25 | 26 | internal void LoadAssemblies() 27 | { 28 | // Load FlyleafLib's Embedded Plugins 29 | LoadPlugin(Assembly.GetExecutingAssembly()); 30 | 31 | // Load External Plugins Folder 32 | if (Folder != null && Directory.Exists(Folder)) 33 | { 34 | string[] dirs = Directory.GetDirectories(Folder); 35 | 36 | foreach(string dir in dirs) 37 | foreach(string file in Directory.GetFiles(dir, "*.dll")) 38 | LoadPlugin(Assembly.LoadFrom(Path.GetFullPath(file))); 39 | } 40 | else 41 | { 42 | Engine.Log.Info($"[PluginHandler] No external plugins found"); 43 | } 44 | } 45 | 46 | /// 47 | /// Manually load plugins 48 | /// 49 | /// The assembly to search for plugins 50 | public void LoadPlugin(Assembly assembly) 51 | { 52 | try 53 | { 54 | var types = assembly.GetTypes(); 55 | 56 | foreach (var type in types) 57 | { 58 | if (pluginBaseType.IsAssignableFrom(type) && type.IsClass && !type.IsAbstract) 59 | { 60 | // Force static constructors to execute (For early load, will be useful with c# 8.0 and static properties for interfaces eg. DefaultOptions) 61 | // System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(type.TypeHandle); 62 | 63 | if (!Types.ContainsKey(type.Name)) 64 | { 65 | Types.Add(type.Name, new PluginType() { Name = type.Name, Type = type, Version = assembly.GetName().Version}); 66 | Engine.Log.Info($"Plugin loaded ({type.Name} - {assembly.GetName().Version})"); 67 | } 68 | else 69 | Engine.Log.Info($"Plugin already exists ({type.Name} - {assembly.GetName().Version})"); 70 | } 71 | } 72 | } 73 | catch (Exception e) { Engine.Log.Error($"[PluginHandler] [Error] Failed to load assembly ({e.Message} {Utils.GetRecInnerException(e)})"); } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /FlyleafLib/Engine/WhisperCppModel.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Text.Json.Serialization; 3 | using Whisper.net.Ggml; 4 | 5 | namespace FlyleafLib; 6 | 7 | #nullable enable 8 | 9 | public class WhisperCppModel : NotifyPropertyChanged, IEquatable 10 | { 11 | public GgmlType Model { get; set; } 12 | 13 | [JsonIgnore] 14 | public long Size 15 | { 16 | get; 17 | set 18 | { 19 | if (Set(ref field, value)) 20 | { 21 | Raise(nameof(Downloaded)); 22 | } 23 | } 24 | } 25 | 26 | [JsonIgnore] 27 | public string ModelFileName 28 | { 29 | get 30 | { 31 | string modelName = Model.ToString().ToLower(); 32 | return $"ggml-{modelName}.bin"; 33 | } 34 | } 35 | 36 | [JsonIgnore] 37 | public string ModelFilePath => Path.Combine(WhisperConfig.ModelsDirectory, ModelFileName); 38 | 39 | [JsonIgnore] 40 | public bool Downloaded => Size > 0; 41 | 42 | public override string ToString() => Model.ToString(); 43 | 44 | public bool Equals(WhisperCppModel? other) 45 | { 46 | if (other is null) return false; 47 | if (ReferenceEquals(this, other)) return true; 48 | return Model == other.Model; 49 | } 50 | 51 | public override bool Equals(object? obj) => obj is WhisperCppModel o && Equals(o); 52 | 53 | public override int GetHashCode() 54 | { 55 | return (int)Model; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /FlyleafLib/FlyleafLib.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0-windows10.0.18362.0 5 | preview 6 | true 7 | true 8 | 9 | Media Player .NET Library for WinUI 3/WPF/WinForms (based on FFmpeg/DirectX) 10 | 3.8.4 11 | SuRGeoNix 12 | SuRGeoNix © 2025 13 | GPL-3.0-or-later 14 | https://github.com/SuRGeoNix/Flyleaf 15 | flyleaf flyleaflib video audio media player engine framework download extract ffmpeg vortice directx 16 | true 17 | snupkg 18 | 19 | 20 | 21 | true 22 | 23 | 24 | 25 | true 26 | 27 | 28 | 29 | 6 30 | 31 | 32 | 33 | 6 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /FlyleafLib/MediaFramework/MediaDemuxer/CustomIOContext.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace FlyleafLib.MediaFramework.MediaDemuxer; 4 | 5 | public unsafe class CustomIOContext 6 | { 7 | AVIOContext* avioCtx; 8 | public Stream stream; 9 | Demuxer demuxer; 10 | 11 | public CustomIOContext(Demuxer demuxer) 12 | { 13 | this.demuxer = demuxer; 14 | } 15 | 16 | public void Initialize(Stream stream) 17 | { 18 | this.stream = stream; 19 | //this.stream.Seek(0, SeekOrigin.Begin); 20 | 21 | ioread = IORead; 22 | ioseek = IOSeek; 23 | avioCtx = avio_alloc_context((byte*)av_malloc((nuint)demuxer.Config.IOStreamBufferSize), demuxer.Config.IOStreamBufferSize, 0, null, ioread, null, ioseek); 24 | demuxer.FormatContext->pb = avioCtx; 25 | demuxer.FormatContext->flags |= FmtFlags2.CustomIo; 26 | } 27 | 28 | public void Dispose() 29 | { 30 | if (avioCtx != null) 31 | { 32 | av_free(avioCtx->buffer); 33 | fixed (AVIOContext** ptr = &avioCtx) avio_context_free(ptr); 34 | } 35 | avioCtx = null; 36 | stream = null; 37 | ioread = null; 38 | ioseek = null; 39 | } 40 | 41 | avio_alloc_context_read_packet ioread; 42 | avio_alloc_context_seek ioseek; 43 | 44 | int IORead(void* opaque, byte* buffer, int bufferSize) 45 | { 46 | int ret; 47 | 48 | if (demuxer.Interrupter.ShouldInterrupt(null) != 0) return AVERROR_EXIT; 49 | 50 | ret = demuxer.CustomIOContext.stream.Read(new Span(buffer, bufferSize)); 51 | 52 | if (ret > 0) 53 | return ret; 54 | 55 | if (ret == 0) 56 | return AVERROR_EOF; 57 | 58 | demuxer.Log.Warn("CustomIOContext Interrupted"); 59 | 60 | return AVERROR_EXIT; 61 | } 62 | 63 | long IOSeek(void* opaque, long offset, IOSeekFlags whence) 64 | { 65 | //System.Diagnostics.Debug.WriteLine($"** S | {decCtx.demuxer.fmtCtx->pb->pos} - {decCtx.demuxer.ioStream.Position}"); 66 | 67 | return whence == IOSeekFlags.Size 68 | ? demuxer.CustomIOContext.stream.Length 69 | : demuxer.CustomIOContext.stream.Seek(offset, (SeekOrigin) whence); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /FlyleafLib/MediaFramework/MediaDemuxer/DemuxerInput.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | 4 | namespace FlyleafLib.MediaFramework.MediaDemuxer; 5 | 6 | public class DemuxerInput : NotifyPropertyChanged 7 | { 8 | /// 9 | /// Url provided as a demuxer input 10 | /// 11 | public string Url { get => _Url; set => _Url = Utils.FixFileUrl(value); } 12 | string _Url; 13 | 14 | /// 15 | /// Fallback url provided as a demuxer input 16 | /// 17 | public string UrlFallback { get => _UrlFallback; set => _UrlFallback = Utils.FixFileUrl(value); } 18 | string _UrlFallback; 19 | 20 | /// 21 | /// IOStream provided as a demuxer input 22 | /// 23 | public Stream IOStream { get; set; } 24 | 25 | public Dictionary 26 | HTTPHeaders { get; set; } 27 | 28 | public string UserAgent { get; set; } 29 | 30 | public string Referrer { get; set; } 31 | } 32 | -------------------------------------------------------------------------------- /FlyleafLib/MediaFramework/MediaDemuxer/Interrupter.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | using static FlyleafLib.Logger; 4 | 5 | namespace FlyleafLib.MediaFramework.MediaDemuxer; 6 | 7 | public unsafe class Interrupter 8 | { 9 | public int ForceInterrupt { get; set; } 10 | public Requester Requester { get; private set; } 11 | public int Interrupted { get; private set; } 12 | public bool Timedout { get; private set; } 13 | 14 | Demuxer demuxer; 15 | Stopwatch sw = new(); 16 | internal AVIOInterruptCB_callback interruptClbk; 17 | long curTimeoutMs; 18 | 19 | internal int ShouldInterrupt(void* opaque) 20 | { 21 | if (demuxer.Status == Status.Stopping) 22 | { 23 | if (CanDebug) demuxer.Log.Debug($"{Requester} Interrupt (Stopping) !!!"); 24 | 25 | return Interrupted = 1; 26 | } 27 | 28 | if (demuxer.Config.AllowTimeouts && sw.ElapsedMilliseconds > curTimeoutMs) 29 | { 30 | if (Timedout) 31 | return Interrupted = 1; 32 | 33 | if (CanWarn) demuxer.Log.Warn($"{Requester} Timeout !!!! {sw.ElapsedMilliseconds} ms"); 34 | 35 | Timedout = true; 36 | Interrupted = 1; 37 | demuxer.OnTimedOut(); 38 | 39 | return Interrupted; 40 | } 41 | 42 | if (Requester == Requester.Close) 43 | return 0; 44 | 45 | if (ForceInterrupt != 0 && demuxer.allowReadInterrupts) 46 | { 47 | if (CanTrace) demuxer.Log.Trace($"{Requester} Interrupt !!!"); 48 | 49 | return Interrupted = 1; 50 | } 51 | 52 | return Interrupted = 0; 53 | } 54 | 55 | public Interrupter(Demuxer demuxer) 56 | { 57 | this.demuxer = demuxer; 58 | interruptClbk = ShouldInterrupt; 59 | } 60 | 61 | public void ReadRequest() 62 | { 63 | Requester = Requester.Read; 64 | 65 | if (!demuxer.Config.AllowTimeouts) 66 | return; 67 | 68 | Timedout = false; 69 | curTimeoutMs= demuxer.IsLive ? demuxer.Config.readLiveTimeoutMs: demuxer.Config.readTimeoutMs; 70 | sw.Restart(); 71 | } 72 | 73 | public void SeekRequest() 74 | { 75 | Requester = Requester.Seek; 76 | 77 | if (!demuxer.Config.AllowTimeouts) 78 | return; 79 | 80 | Timedout = false; 81 | curTimeoutMs= demuxer.Config.seekTimeoutMs; 82 | sw.Restart(); 83 | } 84 | 85 | public void OpenRequest() 86 | { 87 | Requester = Requester.Open; 88 | 89 | if (!demuxer.Config.AllowTimeouts) 90 | return; 91 | 92 | Timedout = false; 93 | curTimeoutMs= demuxer.Config.openTimeoutMs; 94 | sw.Restart(); 95 | } 96 | 97 | public void CloseRequest() 98 | { 99 | Requester = Requester.Close; 100 | 101 | if (!demuxer.Config.AllowTimeouts) 102 | return; 103 | 104 | Timedout = false; 105 | curTimeoutMs= demuxer.Config.closeTimeoutMs; 106 | sw.Restart(); 107 | } 108 | } 109 | 110 | public enum Requester 111 | { 112 | Close, 113 | Open, 114 | Read, 115 | Seek 116 | } 117 | -------------------------------------------------------------------------------- /FlyleafLib/MediaFramework/MediaDevice/AudioDevice.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Vortice.MediaFoundation; 3 | 4 | namespace FlyleafLib.MediaFramework.MediaDevice; 5 | 6 | public class AudioDevice : DeviceBase 7 | { 8 | public AudioDevice(string friendlyName, string symbolicLink) : base(friendlyName, symbolicLink) 9 | => Url = $"fmt://dshow?audio={FriendlyName}"; 10 | 11 | public static void RefreshDevices() 12 | { 13 | Utils.UIInvokeIfRequired(() => 14 | { 15 | Engine.Audio.CapDevices.Clear(); 16 | 17 | var devices = MediaFactory.MFEnumAudioDeviceSources(); 18 | foreach (var device in devices) 19 | try { Engine.Audio.CapDevices.Add(new AudioDevice(device.FriendlyName, device.SymbolicLink)); } catch(Exception) { } 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /FlyleafLib/MediaFramework/MediaDevice/AudioDeviceStream.cs: -------------------------------------------------------------------------------- 1 | namespace FlyleafLib.MediaFramework.MediaDevice; 2 | 3 | public class AudioDeviceStream : DeviceStreamBase 4 | { 5 | public AudioDeviceStream(string deviceName) : base(deviceName) { } 6 | } 7 | -------------------------------------------------------------------------------- /FlyleafLib/MediaFramework/MediaDevice/DeviceBase.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace FlyleafLib.MediaFramework.MediaDevice; 4 | 5 | public class DeviceBase :DeviceBase 6 | { 7 | public DeviceBase(string friendlyName, string symbolicLink) : base(friendlyName, symbolicLink) 8 | { 9 | } 10 | } 11 | 12 | public class DeviceBase 13 | where T: DeviceStreamBase 14 | { 15 | public string FriendlyName { get; } 16 | public string SymbolicLink { get; } 17 | public IList 18 | Streams { get; protected set; } 19 | public string Url { get; protected set; } // default Url 20 | 21 | public DeviceBase(string friendlyName, string symbolicLink) 22 | { 23 | FriendlyName = friendlyName; 24 | SymbolicLink = symbolicLink; 25 | 26 | Engine.Log.Debug($"[{(this is AudioDevice ? "Audio" : "Video")}Device] {friendlyName}"); 27 | } 28 | 29 | public override string ToString() => FriendlyName; 30 | } 31 | -------------------------------------------------------------------------------- /FlyleafLib/MediaFramework/MediaDevice/DeviceStreamBase.cs: -------------------------------------------------------------------------------- 1 | namespace FlyleafLib.MediaFramework.MediaDevice; 2 | 3 | public class DeviceStreamBase 4 | { 5 | public string DeviceFriendlyName { get; } 6 | public string Url { get; protected set; } 7 | 8 | public DeviceStreamBase(string deviceFriendlyName) => DeviceFriendlyName = deviceFriendlyName; 9 | } 10 | -------------------------------------------------------------------------------- /FlyleafLib/MediaFramework/MediaDevice/VideoDevice.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | using Vortice.MediaFoundation; 5 | 6 | namespace FlyleafLib.MediaFramework.MediaDevice; 7 | 8 | public class VideoDevice : DeviceBase 9 | { 10 | public VideoDevice(string friendlyName, string symbolicLink) : base(friendlyName, symbolicLink) 11 | { 12 | Streams = VideoDeviceStream.GetVideoFormatsForVideoDevice(friendlyName, symbolicLink); 13 | Url = Streams.Where(f => f.SubType.Contains("MJPG") && f.FrameRate >= 30).OrderByDescending(f => f.FrameSizeHeight).FirstOrDefault()?.Url; 14 | } 15 | 16 | public static void RefreshDevices() 17 | { 18 | Utils.UIInvokeIfRequired(() => 19 | { 20 | Engine.Video.CapDevices.Clear(); 21 | 22 | var devices = MediaFactory.MFEnumVideoDeviceSources(); 23 | foreach (var device in devices) 24 | try { Engine.Video.CapDevices.Add(new VideoDevice(device.FriendlyName, device.SymbolicLink)); } catch(Exception) { } 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /FlyleafLib/MediaFramework/MediaFrame/AudioFrame.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FlyleafLib.MediaFramework.MediaFrame; 4 | 5 | public class AudioFrame : FrameBase 6 | { 7 | public IntPtr dataPtr; 8 | public int dataLen; 9 | } 10 | -------------------------------------------------------------------------------- /FlyleafLib/MediaFramework/MediaFrame/DataFrame.cs: -------------------------------------------------------------------------------- 1 | namespace FlyleafLib.MediaFramework.MediaFrame; 2 | 3 | public class DataFrame : FrameBase 4 | { 5 | public AVCodecID DataCodecId; 6 | public byte[] Data; 7 | } 8 | -------------------------------------------------------------------------------- /FlyleafLib/MediaFramework/MediaFrame/FrameBase.cs: -------------------------------------------------------------------------------- 1 | namespace FlyleafLib.MediaFramework.MediaFrame; 2 | 3 | public unsafe class FrameBase 4 | { 5 | public long timestamp; 6 | //public long pts; 7 | } 8 | -------------------------------------------------------------------------------- /FlyleafLib/MediaFramework/MediaFrame/VideoFrame.cs: -------------------------------------------------------------------------------- 1 | using Vortice.Direct3D11; 2 | 3 | using ID3D11Texture2D = Vortice.Direct3D11.ID3D11Texture2D; 4 | 5 | namespace FlyleafLib.MediaFramework.MediaFrame; 6 | 7 | public unsafe class VideoFrame : FrameBase 8 | { 9 | public ID3D11Texture2D[] textures; // Planes 10 | public ID3D11ShaderResourceView[] srvs; // Views 11 | 12 | // Zero-Copy 13 | public AVFrame* avFrame; // Lets ffmpeg to know that we still need it 14 | } 15 | -------------------------------------------------------------------------------- /FlyleafLib/MediaFramework/MediaPlaylist/PLSPlaylist.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Runtime.InteropServices; 3 | using System.Text; 4 | 5 | namespace FlyleafLib.MediaFramework.MediaPlaylist; 6 | 7 | public class PLSPlaylist 8 | { 9 | [DllImport("kernel32")] 10 | private static extern long WritePrivateProfileString(string name, string key, string val, string filePath); 11 | [DllImport("kernel32")] 12 | private static extern int GetPrivateProfileString(string section, string key, string def, StringBuilder retVal, int size, string filePath); 13 | 14 | public string path; 15 | 16 | public static List Parse(string filename) 17 | { 18 | List items = new(); 19 | string res; 20 | int entries = 1000; 21 | 22 | if ((res = GetINIAttribute("playlist", "NumberOfEntries", filename)) != null) 23 | entries = int.Parse(res); 24 | 25 | for (int i=1; i<=entries; i++) 26 | { 27 | if ((res = GetINIAttribute("playlist", $"File{i}", filename)) == null) 28 | break; 29 | 30 | PLSPlaylistItem item = new() { Url = res }; 31 | 32 | if ((res = GetINIAttribute("playlist", $"Title{i}", filename)) != null) 33 | item.Title = res; 34 | 35 | if ((res = GetINIAttribute("playlist", $"Length{i}", filename)) != null) 36 | item.Duration = int.Parse(res); 37 | 38 | items.Add(item); 39 | } 40 | 41 | return items; 42 | } 43 | 44 | public static string GetINIAttribute(string name, string key, string path) 45 | { 46 | StringBuilder sb = new(255); 47 | return GetPrivateProfileString(name, key, "", sb, 255, path) > 0 48 | ? sb.ToString() : null; 49 | } 50 | } 51 | 52 | public class PLSPlaylistItem 53 | { 54 | public int Duration { get; set; } 55 | public string Title { get; set; } 56 | public string Url { get; set; } 57 | } 58 | -------------------------------------------------------------------------------- /FlyleafLib/MediaFramework/MediaPlaylist/Session.cs: -------------------------------------------------------------------------------- 1 | namespace FlyleafLib.MediaFramework.MediaPlaylist; 2 | 3 | public class Session 4 | { 5 | public string Url { get; set; } 6 | public int PlaylistItem { get; set; } = -1; 7 | 8 | public int ExternalAudioStream { get; set; } = -1; 9 | public int ExternalVideoStream { get; set; } = -1; 10 | public string ExternalSubtitlesUrl { get; set; } 11 | 12 | public int AudioStream { get; set; } = -1; 13 | public int VideoStream { get; set; } = -1; 14 | public int SubtitlesStream { get; set; } = -1; 15 | 16 | public long CurTime { get; set; } 17 | 18 | public long AudioDelay { get; set; } 19 | public long SubtitlesDelay { get; set; } 20 | 21 | internal bool isReopen; // temp fix for opening existing playlist item as a new session (should not re-initialize - is like switch) 22 | 23 | //public SavedSession() { } 24 | //public SavedSession(int extVideoStream, int videoStream, int extAudioStream, int audioStream, int extSubtitlesStream, int subtitlesStream, long curTime, long audioDelay, long subtitlesDelay) 25 | //{ 26 | // Update(extVideoStream, videoStream, extAudioStream, audioStream, extSubtitlesStream, subtitlesStream, curTime, audioDelay, subtitlesDelay); 27 | //} 28 | //public void Update(int extVideoStream, int videoStream, int extAudioStream, int audioStream, int extSubtitlesStream, int subtitlesStream, long curTime, long audioDelay, long subtitlesDelay) 29 | //{ 30 | // ExternalVideoStream = extVideoStream; VideoStream = videoStream; 31 | // ExternalAudioStream = extAudioStream; AudioStream = audioStream; 32 | // ExternalSubtitlesStream = extSubtitlesStream; SubtitlesStream = subtitlesStream; 33 | // CurTime = curTime; AudioDelay = audioDelay; SubtitlesDelay = subtitlesDelay; 34 | //} 35 | } 36 | -------------------------------------------------------------------------------- /FlyleafLib/MediaFramework/MediaProgram/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | using FlyleafLib.MediaFramework.MediaDemuxer; 5 | using FlyleafLib.MediaFramework.MediaStream; 6 | 7 | namespace FlyleafLib.MediaFramework.MediaProgram; 8 | 9 | public class Program 10 | { 11 | public int ProgramNumber { get; internal set; } 12 | 13 | public int ProgramId { get; internal set; } 14 | 15 | public IReadOnlyDictionary 16 | Metadata { get; internal set; } 17 | 18 | public IReadOnlyList 19 | Streams { get; internal set; } 20 | 21 | public string Name => Metadata.ContainsKey("name") ? Metadata["name"] : string.Empty; 22 | 23 | public unsafe Program(AVProgram* program, Demuxer demuxer) 24 | { 25 | ProgramNumber = program->program_num; 26 | ProgramId = program->id; 27 | 28 | // Load stream info 29 | List streams = new(3); 30 | for(int s = 0; snb_stream_indexes; s++) 31 | { 32 | uint streamIndex = program->stream_index[s]; 33 | StreamBase stream = null; 34 | stream = demuxer.AudioStreams.FirstOrDefault(it=>it.StreamIndex == streamIndex); 35 | 36 | if (stream == null) 37 | { 38 | stream = demuxer.VideoStreams.FirstOrDefault(it => it.StreamIndex == streamIndex); 39 | stream ??= demuxer.SubtitlesStreamsAll.FirstOrDefault(it => it.StreamIndex == streamIndex); 40 | } 41 | if (stream!=null) 42 | { 43 | streams.Add(stream); 44 | } 45 | } 46 | Streams = streams; 47 | 48 | // Load metadata 49 | Dictionary metadata = new(); 50 | AVDictionaryEntry* b = null; 51 | while (true) 52 | { 53 | b = av_dict_get(program->metadata, "", b, DictReadFlags.IgnoreSuffix); 54 | if (b == null) break; 55 | metadata.Add(Utils.BytePtrToStringUTF8(b->key), Utils.BytePtrToStringUTF8(b->value)); 56 | } 57 | Metadata = metadata; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /FlyleafLib/MediaFramework/MediaStream/AudioStream.cs: -------------------------------------------------------------------------------- 1 | using FlyleafLib.MediaFramework.MediaDemuxer; 2 | 3 | namespace FlyleafLib.MediaFramework.MediaStream; 4 | 5 | public unsafe class AudioStream : StreamBase 6 | { 7 | public int Bits { get; set; } 8 | public int Channels { get; set; } 9 | public ulong ChannelLayout { get; set; } 10 | public string ChannelLayoutStr { get; set; } 11 | public AVSampleFormat SampleFormat { get; set; } 12 | public string SampleFormatStr { get; set; } 13 | public int SampleRate { get; set; } 14 | public AVCodecID CodecIDOrig { get; set; } 15 | 16 | public override string GetDump() 17 | => $"[{Type} #{StreamIndex}-{Language.IdSubLanguage}{(Title != null ? "(" + Title + ")" : "")}] {Codec} {SampleFormatStr}@{Bits} {SampleRate / 1000}KHz {ChannelLayoutStr} | [BR: {BitRate}] | {Utils.TicksToTime((long)(AVStream->start_time * Timebase))}/{Utils.TicksToTime((long)(AVStream->duration * Timebase))} | {Utils.TicksToTime(StartTime)}/{Utils.TicksToTime(Duration)}"; 18 | 19 | public AudioStream() { } 20 | public AudioStream(Demuxer demuxer, AVStream* st) : base(demuxer, st) => Refresh(); 21 | 22 | public override void Refresh() 23 | { 24 | base.Refresh(); 25 | 26 | SampleFormat = (AVSampleFormat) Enum.ToObject(typeof(AVSampleFormat), AVStream->codecpar->format); 27 | SampleFormatStr = av_get_sample_fmt_name(SampleFormat); 28 | SampleRate = AVStream->codecpar->sample_rate; 29 | 30 | if (AVStream->codecpar->ch_layout.order == AVChannelOrder.Unspec) 31 | av_channel_layout_default(&AVStream->codecpar->ch_layout, AVStream->codecpar->ch_layout.nb_channels); 32 | 33 | ChannelLayout = AVStream->codecpar->ch_layout.u.mask; 34 | Channels = AVStream->codecpar->ch_layout.nb_channels; 35 | Bits = AVStream->codecpar->bits_per_coded_sample; 36 | 37 | // https://trac.ffmpeg.org/ticket/7321 38 | CodecIDOrig = CodecID; 39 | if (CodecID == AVCodecID.Mp2 && (SampleFormat == AVSampleFormat.Fltp || SampleFormat == AVSampleFormat.Flt)) 40 | CodecID = AVCodecID.Mp3; // OR? st->codecpar->format = (int) AVSampleFormat.AV_SAMPLE_FMT_S16P; 41 | 42 | byte[] buf = new byte[50]; 43 | fixed (byte* bufPtr = buf) 44 | { 45 | av_channel_layout_describe(&AVStream->codecpar->ch_layout, bufPtr, (nuint)buf.Length); 46 | ChannelLayoutStr = Utils.BytePtrToStringUTF8(bufPtr); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /FlyleafLib/MediaFramework/MediaStream/DataStream.cs: -------------------------------------------------------------------------------- 1 | using FlyleafLib.MediaFramework.MediaDemuxer; 2 | 3 | namespace FlyleafLib.MediaFramework.MediaStream; 4 | 5 | public unsafe class DataStream : StreamBase 6 | { 7 | 8 | public DataStream() { } 9 | public DataStream(Demuxer demuxer, AVStream* st) : base(demuxer, st) 10 | { 11 | Demuxer = demuxer; 12 | AVStream = st; 13 | Refresh(); 14 | } 15 | 16 | public override void Refresh() 17 | { 18 | base.Refresh(); 19 | } 20 | 21 | public override string GetDump() 22 | => $"[{Type} #{StreamIndex}] {CodecID}"; 23 | } 24 | -------------------------------------------------------------------------------- /FlyleafLib/MediaFramework/MediaStream/ExternalAudioStream.cs: -------------------------------------------------------------------------------- 1 | namespace FlyleafLib.MediaFramework.MediaStream; 2 | 3 | public class ExternalAudioStream : ExternalStream 4 | { 5 | public int SampleRate { get; set; } 6 | public string ChannelLayout { get; set; } 7 | public Language Language { get; set; } 8 | 9 | public bool HasVideo { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /FlyleafLib/MediaFramework/MediaStream/ExternalStream.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | using FlyleafLib.MediaFramework.MediaDemuxer; 4 | using FlyleafLib.MediaFramework.MediaPlaylist; 5 | using FlyleafLib.MediaPlayer; 6 | 7 | namespace FlyleafLib.MediaFramework.MediaStream; 8 | 9 | public class ExternalStream : DemuxerInput 10 | { 11 | public string PluginName { get; set; } 12 | public PlaylistItem 13 | PlaylistItem { get; set; } 14 | public int Index { get; set; } = -1; // if we need it (already used to compare same type streams) we need to ensure we fix it in case of removing an item 15 | public string Protocol { get; set; } 16 | public string Codec { get; set; } 17 | public long BitRate { get; set; } 18 | public Dictionary 19 | Tag { get; set; } = new Dictionary(); 20 | public void AddTag(object tag, string pluginName) 21 | { 22 | if (Tag.ContainsKey(pluginName)) 23 | Tag[pluginName] = tag; 24 | else 25 | Tag.Add(pluginName, tag); 26 | } 27 | public object GetTag(string pluginName) 28 | => Tag.ContainsKey(pluginName) ? Tag[pluginName] : null; 29 | 30 | /// 31 | /// Whether the item is currently enabled or not 32 | /// 33 | public bool Enabled 34 | { 35 | get => _Enabled; 36 | set 37 | { 38 | Utils.UI(() => 39 | { 40 | if (Set(ref _Enabled, value) && value) 41 | { 42 | OpenedCounter++; 43 | } 44 | 45 | Raise(nameof(EnabledPrimarySubtitle)); 46 | Raise(nameof(EnabledSecondarySubtitle)); 47 | Raise(nameof(SubtitlesStream.SelectedSubMethods)); 48 | }); 49 | } 50 | } 51 | bool _Enabled; 52 | 53 | /// 54 | /// Times this item has been used/opened 55 | /// 56 | public int OpenedCounter { get; set; } 57 | 58 | public MediaType 59 | Type => this is ExternalAudioStream ? MediaType.Audio : this is ExternalVideoStream ? MediaType.Video : MediaType.Subs; 60 | 61 | #region Subtitles 62 | // TODO: L: Used for subtitle streams only, but defined in the base class 63 | public bool EnabledPrimarySubtitle => Enabled && this.GetSubEnabled(0); 64 | public bool EnabledSecondarySubtitle => Enabled && this.GetSubEnabled(1); 65 | #endregion 66 | } 67 | -------------------------------------------------------------------------------- /FlyleafLib/MediaFramework/MediaStream/ExternalSubtitlesStream.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | 3 | namespace FlyleafLib.MediaFramework.MediaStream; 4 | 5 | public class ExternalSubtitlesStream : ExternalStream, ISubtitlesStream 6 | { 7 | public SelectedSubMethod[] SelectedSubMethods 8 | { 9 | get 10 | { 11 | var methods = (SelectSubMethod[])Enum.GetValues(typeof(SelectSubMethod)); 12 | 13 | if (!IsBitmap) 14 | { 15 | // delete OCR if text sub 16 | methods = methods.Where(m => m != SelectSubMethod.OCR).ToArray(); 17 | } 18 | 19 | return methods. 20 | Select(m => new SelectedSubMethod(this, m)).ToArray(); 21 | } 22 | } 23 | 24 | public bool IsBitmap { get; set; } 25 | public bool ManualDownloaded{ get; set; } 26 | public bool Automatic { get; set; } 27 | public bool Downloaded { get; set; } 28 | public Language Language { get; set; } = Language.Unknown; 29 | public bool LanguageDetected{ get; set; } 30 | // TODO: Add confidence rating (maybe result is for other movie/episode) | Add Weight calculated based on rating/downloaded/confidence (and lang?) which can be used from suggesters 31 | public string Title { get; set; } 32 | 33 | public string DisplayMember => 34 | $"({Language}){(ManualDownloaded ? " (DL)" : "")}{(Automatic ? " (Auto)" : "")} {Utils.TruncateString(Title, 50)} ({(IsBitmap ? "BMP" : "TXT")})"; 35 | } 36 | -------------------------------------------------------------------------------- /FlyleafLib/MediaFramework/MediaStream/ExternalVideoStream.cs: -------------------------------------------------------------------------------- 1 | namespace FlyleafLib.MediaFramework.MediaStream; 2 | 3 | public class ExternalVideoStream : ExternalStream 4 | { 5 | public double FPS { get; set; } 6 | public int Height { get; set; } 7 | public int Width { get; set; } 8 | 9 | public bool HasAudio { get; set; } 10 | 11 | public string DisplayMember => 12 | $"{Width}x{Height} @{Math.Round(FPS, 2, MidpointRounding.AwayFromZero)} ({Codec}) [{Protocol}]{(HasAudio ? "" : " [NA]")}"; 13 | } 14 | -------------------------------------------------------------------------------- /FlyleafLib/MediaFramework/MediaStream/SubtitlesStream.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Linq; 3 | using FlyleafLib.MediaFramework.MediaDemuxer; 4 | using FlyleafLib.MediaPlayer; 5 | 6 | namespace FlyleafLib.MediaFramework.MediaStream; 7 | 8 | public enum SelectSubMethod 9 | { 10 | Original, 11 | OCR 12 | } 13 | 14 | // Both external and internal subtitles implement this interface and use it with PopMenu. 15 | public interface ISubtitlesStream 16 | { 17 | public bool EnabledPrimarySubtitle { get; } 18 | public bool EnabledSecondarySubtitle { get; } 19 | } 20 | 21 | public class SelectedSubMethod 22 | { 23 | private readonly ISubtitlesStream _stream; 24 | 25 | public SelectedSubMethod(ISubtitlesStream stream, SelectSubMethod method) 26 | { 27 | _stream = stream; 28 | SelectSubMethod = method; 29 | } 30 | 31 | public SelectSubMethod SelectSubMethod { get; } 32 | 33 | public bool IsPrimaryEnabled => _stream.EnabledPrimarySubtitle 34 | && SelectSubMethod == SubtitlesSelectedHelper.PrimaryMethod; 35 | 36 | public bool IsSecondaryEnabled => _stream.EnabledSecondarySubtitle 37 | && SelectSubMethod == SubtitlesSelectedHelper.SecondaryMethod; 38 | } 39 | 40 | public unsafe class SubtitlesStream : StreamBase, ISubtitlesStream 41 | { 42 | public bool IsBitmap { get; set; } 43 | 44 | public SelectedSubMethod[] SelectedSubMethods { 45 | get 46 | { 47 | var methods = (SelectSubMethod[])Enum.GetValues(typeof(SelectSubMethod)); 48 | if (!IsBitmap) 49 | { 50 | // delete OCR if text sub 51 | methods = methods.Where(m => m != SelectSubMethod.OCR).ToArray(); 52 | } 53 | 54 | return methods. 55 | Select(m => new SelectedSubMethod(this, m)).ToArray(); 56 | } 57 | } 58 | 59 | public string DisplayMember => $"[#{StreamIndex}] {Language} ({(IsBitmap ? "BMP" : "TXT")})"; 60 | 61 | public override string GetDump() 62 | => $"[{Type} #{StreamIndex}-{Language.IdSubLanguage}{(Title != null ? "(" + Title + ")" : "")}] {Codec} | [BR: {BitRate}] | {Utils.TicksToTime((long)(AVStream->start_time * Timebase))}/{Utils.TicksToTime((long)(AVStream->duration * Timebase))} | {Utils.TicksToTime(StartTime)}/{Utils.TicksToTime(Duration)}"; 63 | 64 | public SubtitlesStream() { } 65 | public SubtitlesStream(Demuxer demuxer, AVStream* st) : base(demuxer, st) => Refresh(); 66 | 67 | public override void Refresh() 68 | { 69 | base.Refresh(); 70 | 71 | var codecDescr = avcodec_descriptor_get(CodecID); 72 | IsBitmap = codecDescr != null && (codecDescr->props & CodecPropFlags.BitmapSub) != 0; 73 | 74 | if (Demuxer.FormatContext->nb_streams == 1) // External Streams (mainly for .sub will have as start time the first subs timestamp) 75 | StartTime = 0; 76 | } 77 | 78 | public void ExternalStreamAdded() 79 | { 80 | // VobSub (parse .idx data to extradata - based on .sub url) 81 | if (CodecID == AVCodecID.DvdSubtitle && ExternalStream != null && ExternalStream.Url.EndsWith(".sub", StringComparison.OrdinalIgnoreCase)) 82 | { 83 | var idxFile = ExternalStream.Url.Substring(0, ExternalStream.Url.Length - 3) + "idx"; 84 | if (File.Exists(idxFile)) 85 | { 86 | var bytes = File.ReadAllBytes(idxFile); 87 | AVStream->codecpar->extradata = (byte*)av_malloc((nuint)bytes.Length); 88 | AVStream->codecpar->extradata_size = bytes.Length; 89 | Span src = new(bytes); 90 | Span dst = new(AVStream->codecpar->extradata, bytes.Length); 91 | src.CopyTo(dst); 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /FlyleafLib/MediaPlayer/Data.cs: -------------------------------------------------------------------------------- 1 | using FlyleafLib.MediaFramework.MediaContext; 2 | using FlyleafLib.MediaFramework.MediaStream; 3 | using System; 4 | using System.Collections.ObjectModel; 5 | 6 | namespace FlyleafLib.MediaPlayer; 7 | public class Data : NotifyPropertyChanged 8 | { 9 | /// 10 | /// Embedded Streams 11 | /// 12 | public ObservableCollection 13 | Streams => decoder?.DataDemuxer.DataStreams; 14 | 15 | /// 16 | /// Whether the input has data and it is configured 17 | /// 18 | public bool IsOpened { get => isOpened; internal set => Set(ref _IsOpened, value); } 19 | internal bool _IsOpened, isOpened; 20 | 21 | Action uiAction; 22 | Player player; 23 | DecoderContext decoder => player.decoder; 24 | Config Config => player.Config; 25 | 26 | public Data(Player player) 27 | { 28 | this.player = player; 29 | uiAction = () => 30 | { 31 | IsOpened = IsOpened; 32 | }; 33 | } 34 | internal void Reset() 35 | { 36 | isOpened = false; 37 | 38 | player.UIAdd(uiAction); 39 | } 40 | internal void Refresh() 41 | { 42 | if (decoder.DataStream == null) 43 | { Reset(); return; } 44 | 45 | isOpened = !decoder.DataDecoder.Disposed; 46 | 47 | player.UIAdd(uiAction); 48 | } 49 | internal void Enable() 50 | { 51 | if (!player.CanPlay) 52 | return; 53 | 54 | decoder.OpenSuggestedData(); 55 | player.ReSync(decoder.DataStream, (int)(player.CurTime / 10000), true); 56 | 57 | Refresh(); 58 | player.UIAll(); 59 | } 60 | internal void Disable() 61 | { 62 | if (!IsOpened) 63 | return; 64 | 65 | decoder.CloseData(); 66 | 67 | player.dFrame = null; 68 | Reset(); 69 | player.UIAll(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /FlyleafLib/MediaPlayer/Translation/Services/DeepLTranslateService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using DeepL; 4 | using DeepL.Model; 5 | 6 | namespace FlyleafLib.MediaPlayer.Translation.Services; 7 | 8 | #nullable enable 9 | 10 | public class DeepLTranslateService : ITranslateService 11 | { 12 | private string? _srcLang; 13 | private string? _targetLang; 14 | private readonly Translator _translator; 15 | private readonly DeepLTranslateSettings _settings; 16 | 17 | public DeepLTranslateService(DeepLTranslateSettings settings) 18 | { 19 | if (string.IsNullOrWhiteSpace(settings.ApiKey)) 20 | { 21 | throw new TranslationConfigException( 22 | $"API Key for {ServiceType} is not configured."); 23 | } 24 | 25 | _settings = settings; 26 | _translator = new Translator(_settings.ApiKey, new TranslatorOptions() 27 | { 28 | OverallConnectionTimeout = TimeSpan.FromMilliseconds(settings.TimeoutMs) 29 | }); 30 | } 31 | 32 | public TranslateServiceType ServiceType => TranslateServiceType.DeepL; 33 | 34 | public void Dispose() 35 | { 36 | _translator.Dispose(); 37 | } 38 | 39 | public void Initialize(Language src, TargetLanguage target) 40 | { 41 | (TranslateLanguage srcLang, _) = this.TryGetLanguage(src, target); 42 | 43 | _srcLang = ToSourceCode(srcLang.ISO6391); 44 | _targetLang = ToTargetCode(target); 45 | } 46 | 47 | internal static string ToSourceCode(string iso6391) 48 | { 49 | // ref: https://developers.deepl.com/docs/resources/supported-languages 50 | 51 | // Just capitalize ISO6391. 52 | return iso6391.ToUpper(); 53 | } 54 | 55 | internal static string ToTargetCode(TargetLanguage target) 56 | { 57 | return target switch 58 | { 59 | TargetLanguage.EnglishAmerican => "EN-US", 60 | TargetLanguage.EnglishBritish => "EN-GB", 61 | TargetLanguage.Portuguese => "PT-PT", 62 | TargetLanguage.PortugueseBrazilian => "PT-BR", 63 | TargetLanguage.ChineseSimplified => "ZH-HANS", 64 | TargetLanguage.ChineseTraditional => "ZH-HANT", 65 | _ => target.ToISO6391().ToUpper() 66 | }; 67 | } 68 | 69 | public async Task TranslateAsync(string text, CancellationToken token) 70 | { 71 | if (_srcLang == null || _targetLang == null) 72 | throw new InvalidOperationException("must be initialized"); 73 | 74 | try 75 | { 76 | TextResult result = await _translator.TranslateTextAsync(text, _srcLang, _targetLang, 77 | new TextTranslateOptions 78 | { 79 | Formality = Formality.Default, 80 | }, token).ConfigureAwait(false); 81 | 82 | return result.Text; 83 | } 84 | catch (OperationCanceledException) 85 | { 86 | throw; 87 | } 88 | catch (Exception ex) 89 | { 90 | // Timeout: DeepL.ConnectionException 91 | throw new TranslationException($"Cannot request to {ServiceType}: {ex.Message}", ex); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /FlyleafLib/MediaPlayer/Translation/Services/TranslateServiceFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace FlyleafLib.MediaPlayer.Translation.Services; 4 | 5 | public class TranslateServiceFactory 6 | { 7 | private readonly Config.SubtitlesConfig _config; 8 | 9 | public TranslateServiceFactory(Config.SubtitlesConfig config) 10 | { 11 | _config = config; 12 | } 13 | 14 | /// 15 | /// GetService 16 | /// 17 | /// 18 | /// 19 | /// 20 | /// 21 | /// 22 | public ITranslateService GetService(TranslateServiceType serviceType, bool wordMode) 23 | { 24 | switch (serviceType) 25 | { 26 | case TranslateServiceType.GoogleV1: 27 | return new GoogleV1TranslateService((GoogleV1TranslateSettings)_config.TranslateServiceSettings.GetValueOrDefault(serviceType, new GoogleV1TranslateSettings())); 28 | 29 | case TranslateServiceType.DeepL: 30 | return new DeepLTranslateService((DeepLTranslateSettings)_config.TranslateServiceSettings.GetValueOrDefault(serviceType, new DeepLTranslateSettings())); 31 | 32 | case TranslateServiceType.DeepLX: 33 | return new DeepLXTranslateService((DeepLXTranslateSettings)_config.TranslateServiceSettings.GetValueOrDefault(serviceType, new DeepLXTranslateSettings())); 34 | 35 | case TranslateServiceType.Ollama: 36 | return new OpenAIBaseTranslateService((OllamaTranslateSettings)_config.TranslateServiceSettings.GetValueOrDefault(serviceType, new OllamaTranslateSettings()), _config.TranslateChatConfig, wordMode); 37 | 38 | case TranslateServiceType.LMStudio: 39 | return new OpenAIBaseTranslateService((LMStudioTranslateSettings)_config.TranslateServiceSettings.GetValueOrDefault(serviceType, new LMStudioTranslateSettings()), _config.TranslateChatConfig, wordMode); 40 | 41 | case TranslateServiceType.KoboldCpp: 42 | return new OpenAIBaseTranslateService((KoboldCppTranslateSettings)_config.TranslateServiceSettings.GetValueOrDefault(serviceType, new KoboldCppTranslateSettings()), _config.TranslateChatConfig, wordMode); 43 | 44 | case TranslateServiceType.OpenAI: 45 | return new OpenAIBaseTranslateService((OpenAITranslateSettings)_config.TranslateServiceSettings.GetValueOrDefault(serviceType, new OpenAITranslateSettings()), _config.TranslateChatConfig, wordMode); 46 | 47 | case TranslateServiceType.OpenAILike: 48 | return new OpenAIBaseTranslateService((OpenAILikeTranslateSettings)_config.TranslateServiceSettings.GetValueOrDefault(serviceType, new OpenAILikeTranslateSettings()), _config.TranslateChatConfig, wordMode); 49 | 50 | case TranslateServiceType.Claude: 51 | return new OpenAIBaseTranslateService((ClaudeTranslateSettings)_config.TranslateServiceSettings.GetValueOrDefault(serviceType, new ClaudeTranslateSettings()), _config.TranslateChatConfig, wordMode); 52 | 53 | case TranslateServiceType.LiteLLM: 54 | return new OpenAIBaseTranslateService((LiteLLMTranslateSettings)_config.TranslateServiceSettings.GetValueOrDefault(serviceType, new LiteLLMTranslateSettings()), _config.TranslateChatConfig, wordMode); 55 | } 56 | 57 | throw new InvalidOperationException($"Translate service {serviceType} does not exist."); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /FlyleafLib/MediaPlayer/Translation/TranslateChatConfig.cs: -------------------------------------------------------------------------------- 1 | namespace FlyleafLib.MediaPlayer.Translation; 2 | 3 | public class TranslateChatConfig : NotifyPropertyChanged 4 | { 5 | public const string DefaultPromptOneByOne = 6 | """ 7 | You are a professional subtitle translator, translate from {source_lang} to {target_lang}, keep sentences in {target_lang} as they are, censoring of translations is prohibited, return only the translated text without the sent text or notes or comments or anything: 8 | 9 | {source_text} 10 | """; 11 | 12 | public const string DefaultPromptKeepContext = 13 | """ 14 | You are a professional subtitle translator. 15 | I will send the text of the subtitles of the video one at a time. 16 | Please translate the text while taking into account the context of the previous text. 17 | 18 | Translate from {source_lang} to {target_lang}. 19 | Return only the translated text without the sent text or notes or comments or anything. 20 | Keep sentences in {target_lang} as they are. 21 | Censoring of translations is prohibited. 22 | """; 23 | 24 | public string PromptOneByOne { get; set => Set(ref field, value); } = DefaultPromptOneByOne.ReplaceLineEndings("\n"); 25 | 26 | public string PromptKeepContext { get; set => Set(ref field, value); } = DefaultPromptKeepContext.ReplaceLineEndings("\n"); 27 | 28 | public ChatTranslateMethod TranslateMethod { get; set => Set(ref field, value); } = ChatTranslateMethod.KeepContext; 29 | 30 | public int SubtitleContextCount { get; set => Set(ref field, value); } = 6; 31 | 32 | public ChatContextRetainPolicy ContextRetainPolicy { get; set => Set(ref field, value); } = ChatContextRetainPolicy.Reset; 33 | 34 | public bool IncludeTargetLangRegion { get; set => Set(ref field, value); } = true; 35 | } 36 | 37 | public enum ChatTranslateMethod 38 | { 39 | KeepContext, 40 | OneByOne 41 | } 42 | 43 | public enum ChatContextRetainPolicy 44 | { 45 | Reset, 46 | KeepSize 47 | } 48 | -------------------------------------------------------------------------------- /FlyleafLib/Themes/Generic.xaml: -------------------------------------------------------------------------------- 1 |  6 | 7 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /FlyleafLib/Utils/Disposable.cs: -------------------------------------------------------------------------------- 1 | namespace FlyleafLib; 2 | 3 | /// 4 | /// Anonymous Disposal Pattern 5 | /// 6 | public class Disposable : IDisposable 7 | { 8 | public static Disposable Create(Action onDispose) => new(onDispose); 9 | 10 | public static Disposable Empty { get; } = new(null); 11 | 12 | Action _onDispose; 13 | Disposable(Action onDispose) => _onDispose = onDispose; 14 | 15 | public void Dispose() 16 | { 17 | _onDispose?.Invoke(); 18 | _onDispose = null; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /FlyleafLib/Utils/ObservableDictionary.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.Specialized; 3 | using System.ComponentModel; 4 | using System.Linq; 5 | 6 | namespace FlyleafLib; 7 | 8 | public static partial class Utils 9 | { 10 | public class ObservableDictionary : Dictionary, INotifyPropertyChanged, INotifyCollectionChanged 11 | { 12 | public event PropertyChangedEventHandler PropertyChanged; 13 | public event NotifyCollectionChangedEventHandler CollectionChanged; 14 | 15 | public new TVal this[TKey key] 16 | { 17 | get => base[key]; 18 | 19 | set 20 | { 21 | if (ContainsKey(key) && base[key].Equals(value)) return; 22 | 23 | if (CollectionChanged != null) 24 | { 25 | KeyValuePair oldItem = new(key, base[key]); 26 | KeyValuePair newItem = new(key, value); 27 | base[key] = value; 28 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(key.ToString())); 29 | CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, newItem, oldItem, this.ToList().IndexOf(newItem))); 30 | } 31 | else 32 | { 33 | base[key] = value; 34 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(key.ToString())); 35 | } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /FlyleafLibTests/FlyleafLibTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0-windows10.0.18362.0 5 | enable 6 | enable 7 | Exe 8 | FlyleafLib 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /FlyleafLibTests/Utils/SubtitleTextUtilTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | 3 | namespace FlyleafLib; 4 | 5 | public class SubtitleTextUtilTests 6 | { 7 | [Theory] 8 | [InlineData("", "")] 9 | [InlineData(" ", " ")] // Assume trim is done beforehand 10 | [InlineData("Hello", "Hello")] 11 | [InlineData("Hello\nWorld", "Hello World")] 12 | [InlineData("Hello\r\nWorld", "Hello World")] 13 | [InlineData("Hello\n\nWorld", "Hello World")] 14 | [InlineData("Hello\r\n\r\nWorld", "Hello World")] 15 | [InlineData("Hello\n \nWorld", "Hello World")] 16 | 17 | [InlineData("- Hello\n- How are you?", "- Hello\n- How are you?")] 18 | [InlineData("- Hello\n - How are you?", "- Hello - How are you?")] 19 | [InlineData("- Hello\r- How are you?", "- Hello\r- How are you?")] 20 | [InlineData("- Hello\n\n- How are you?", "- Hello\n\n- How are you?")] 21 | [InlineData("- Hello\r\n- How are you?", "- Hello\r\n- How are you?")] 22 | [InlineData("- Hello\nWorld", "- Hello World")] 23 | [InlineData("- こんにちは\n- 世界", "- こんにちは\n- 世界")] 24 | [InlineData("- こんにちは\n世界", "- こんにちは 世界")] 25 | 26 | [InlineData("こんにちは\n世界", "こんにちは 世界")] 27 | [InlineData("🙂\n🙃", "🙂 🙃")] 28 | [InlineData("Hello\nWorld", "Hello World")] 29 | 30 | [InlineData("- Hello\n- Good\nbye", "- Hello\n- Good bye")] 31 | [InlineData("- Hello\nWorld\n- Good\nbye", "- Hello World\n- Good bye")] 32 | 33 | [InlineData("Hello\n- Good\n- bye", "Hello - Good - bye")] 34 | [InlineData(" -Hello\n- Good\n- bye", " -Hello - Good - bye")] 35 | 36 | [InlineData("- Hello\n- aa-bb-cc dd", "- Hello\n- aa-bb-cc dd")] 37 | [InlineData("- Hello\naa-bb-cc dd", "- Hello aa-bb-cc dd")] 38 | 39 | [InlineData("- Hello\n- Goodbye", "- Hello\n- Goodbye")] // hyphen 40 | [InlineData("– Hello\n– Goodbye", "– Hello\n– Goodbye")] // en dash 41 | [InlineData("- Hello\n– Goodbye", "- Hello – Goodbye")] // hyphen + en dash 42 | 43 | public void FlattenUnlessAllDash_ShouldReturnExpected(string input, string expected) 44 | { 45 | string result = SubtitleTextUtil.FlattenText(input); 46 | result.Should().Be(expected); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /FlyleafLibTests/xunit.runner.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json" 3 | } 4 | -------------------------------------------------------------------------------- /LLPlayer-screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umlx5h/LLPlayer/878511b033609f4f5e7f57f92ddf8bde10f607c8/LLPlayer-screenshot.jpg -------------------------------------------------------------------------------- /LLPlayer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umlx5h/LLPlayer/878511b033609f4f5e7f57f92ddf8bde10f607c8/LLPlayer.png -------------------------------------------------------------------------------- /LLPlayer.slnx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /LLPlayer/App.xaml: -------------------------------------------------------------------------------- 1 |  8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /LLPlayer/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | [assembly: ThemeInfo( 4 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 5 | //(used if a resource is not found in the page, 6 | // or application resource dictionaries) 7 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 8 | //(used if a resource is not found in the page, 9 | // app, or any theme specific resource dictionaries) 10 | )] 11 | -------------------------------------------------------------------------------- /LLPlayer/Assets/completion.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umlx5h/LLPlayer/878511b033609f4f5e7f57f92ddf8bde10f607c8/LLPlayer/Assets/completion.mp3 -------------------------------------------------------------------------------- /LLPlayer/Assets/kennedy.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umlx5h/LLPlayer/878511b033609f4f5e7f57f92ddf8bde10f607c8/LLPlayer/Assets/kennedy.wav -------------------------------------------------------------------------------- /LLPlayer/Controls/SelectableSubtitleText.xaml: -------------------------------------------------------------------------------- 1 |  12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /LLPlayer/Controls/Settings/Controls/ColorPicker.xaml: -------------------------------------------------------------------------------- 1 |  11 | 12 | 13 | 17 | 23 | 24 | 30 | 31 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 60 | 61 | 65 | 62 | 63 | 64 | 65 | 73 | 74 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /LLPlayer/Extensions/Bindable.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace LLPlayer.Extensions; 5 | 6 | public class Bindable : INotifyPropertyChanged 7 | { 8 | public event PropertyChangedEventHandler? PropertyChanged; 9 | 10 | protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) 11 | { 12 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 13 | } 14 | 15 | protected bool Set(ref T field, T value, [CallerMemberName] string? propertyName = null) 16 | { 17 | if (EqualityComparer.Default.Equals(field, value)) return false; 18 | field = value; 19 | OnPropertyChanged(propertyName); 20 | return true; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /LLPlayer/Extensions/ColorHexJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | using System.Windows.Media; 5 | 6 | namespace LLPlayer.Extensions; 7 | 8 | // Convert Color object to HEX string 9 | public class ColorHexJsonConverter : JsonConverter 10 | { 11 | public override void Write(Utf8JsonWriter writer, Color value, JsonSerializerOptions options) 12 | { 13 | string hex = $"#{value.A:X2}{value.R:X2}{value.G:X2}{value.B:X2}"; 14 | writer.WriteStringValue(hex); 15 | } 16 | 17 | public override Color Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 18 | { 19 | string? hex = null; 20 | try 21 | { 22 | hex = reader.GetString(); 23 | 24 | if (string.IsNullOrWhiteSpace(hex)) 25 | { 26 | throw new JsonException("Color value is null or empty."); 27 | } 28 | 29 | if (!hex.StartsWith("#") || (hex.Length != 7 && hex.Length != 9)) 30 | { 31 | throw new JsonException($"Invalid color format: {hex}"); 32 | } 33 | byte a = 255; 34 | 35 | int start = 1; 36 | 37 | if (hex.Length == 9) 38 | { 39 | a = byte.Parse(hex.Substring(1, 2), NumberStyles.HexNumber); 40 | start = 3; 41 | } 42 | 43 | byte r = byte.Parse(hex.Substring(start, 2), NumberStyles.HexNumber); 44 | byte g = byte.Parse(hex.Substring(start + 2, 2), NumberStyles.HexNumber); 45 | byte b = byte.Parse(hex.Substring(start + 4, 2), NumberStyles.HexNumber); 46 | 47 | return Color.FromArgb(a, r, g, b); 48 | } 49 | catch (Exception ex) 50 | { 51 | throw new JsonException($"Error parsing color value: {hex}", ex); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /LLPlayer/Extensions/ExtendedDialogService.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | namespace LLPlayer.Extensions; 4 | 5 | /// 6 | /// Customize DialogService's Show(), which sets the Owner of the Window and sets it to always-on-top. 7 | /// ref: https://stackoverflow.com/questions/64420093/prism-idialogservice-show-non-modal-dialog-acts-as-modal 8 | /// 9 | public class ExtendedDialogService(IContainerExtension containerExtension) : DialogService(containerExtension) 10 | { 11 | private bool _isOrphan; 12 | 13 | protected override void ConfigureDialogWindowContent(string dialogName, IDialogWindow window, IDialogParameters parameters) 14 | { 15 | base.ConfigureDialogWindowContent(dialogName, window, parameters); 16 | 17 | if (parameters != null && 18 | parameters.ContainsKey(MyKnownDialogParameters.OrphanWindow)) 19 | { 20 | _isOrphan = true; 21 | } 22 | } 23 | 24 | protected override void ShowDialogWindow(IDialogWindow dialogWindow, bool isModal) 25 | { 26 | base.ShowDialogWindow(dialogWindow, isModal); 27 | 28 | if (_isOrphan) 29 | { 30 | // Show and then clear Owner to place the window based on the parent window and then make it an orphan window. 31 | _isOrphan = false; 32 | dialogWindow.Owner = null; 33 | } 34 | } 35 | } 36 | 37 | // ref: https://github.com/PrismLibrary/Prism/blob/master/src/Wpf/Prism.Wpf/Dialogs/IDialogServiceCompatExtensions.cs 38 | public static class ExtendedDialogServiceExtensions 39 | { 40 | /// 41 | /// Shows a non-modal and singleton dialog. 42 | /// 43 | /// The DialogService 44 | /// The name of the dialog to show. 45 | /// Whether to set owner to window 46 | public static void ShowSingleton(this IDialogService dialogService, string name, bool orphan) 47 | { 48 | ShowSingleton(dialogService, name, null!, orphan); 49 | } 50 | 51 | /// 52 | /// Shows a non-modal and singleton dialog. 53 | /// 54 | /// The DialogService 55 | /// The name of the dialog to show. 56 | /// The action to perform when the dialog is closed. 57 | /// Whether to set owner to window 58 | public static void ShowSingleton(this IDialogService dialogService, string name, Action callback, bool orphan) 59 | { 60 | var parameters = EnsureShowNonModalParameter(null); 61 | 62 | var windows = Application.Current.Windows.OfType(); 63 | if (windows.Any()) 64 | { 65 | var curWindow = windows.FirstOrDefault(w => w.Content.GetType().Name == name); 66 | if (curWindow != null && curWindow is Window win) 67 | { 68 | // If minimized, it will not be displayed after Activate, so set it back to Normal in advance. 69 | if (win.WindowState == WindowState.Minimized) 70 | { 71 | win.WindowState = WindowState.Normal; 72 | } 73 | // TODO: L: Notify to ViewModel to update query 74 | win.Activate(); 75 | return; 76 | } 77 | } 78 | 79 | if (orphan) 80 | { 81 | parameters.Add(MyKnownDialogParameters.OrphanWindow, true); 82 | } 83 | 84 | dialogService.Show(name, parameters, callback); 85 | } 86 | 87 | private static IDialogParameters EnsureShowNonModalParameter(IDialogParameters? parameters) 88 | { 89 | parameters ??= new DialogParameters(); 90 | 91 | if (!parameters.ContainsKey(KnownDialogParameters.ShowNonModal)) 92 | parameters.Add(KnownDialogParameters.ShowNonModal, true); 93 | 94 | return parameters; 95 | } 96 | } 97 | 98 | public static class MyKnownDialogParameters 99 | { 100 | public const string OrphanWindow = "orphanWindow"; 101 | } 102 | -------------------------------------------------------------------------------- /LLPlayer/Extensions/FileHelper.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using static FlyleafLib.Utils; 3 | 4 | namespace LLPlayer.Extensions; 5 | 6 | public static class FileHelper 7 | { 8 | /// 9 | /// Retrieves the next and previous file from the specified file path. 10 | /// Select files with the same extension in the same folder, sorted in natural alphabetical order. 11 | /// Returns null if the next or previous file does not exist. 12 | /// 13 | /// 14 | /// 15 | /// 16 | /// 17 | /// 18 | public static (string? prev, string? next) GetNextAndPreviousFile(string filePath) 19 | { 20 | if (!File.Exists(filePath)) 21 | { 22 | throw new FileNotFoundException("file does not exist", filePath); 23 | } 24 | 25 | string? directory = Path.GetDirectoryName(filePath); 26 | string? extension = Path.GetExtension(filePath); 27 | 28 | if (string.IsNullOrEmpty(directory) || string.IsNullOrEmpty(extension)) 29 | { 30 | throw new InvalidOperationException($"filePath is invalid: {filePath}"); 31 | } 32 | 33 | // Get files with the same extension, ignoring case 34 | List foundFiles = Directory.GetFiles(directory, $"*{extension}") 35 | .Where(f => string.Equals(Path.GetExtension(f), extension, StringComparison.OrdinalIgnoreCase)) 36 | .OrderBy(f => Path.GetFileName(f), new NaturalStringComparer()) 37 | .ToList(); 38 | 39 | if (foundFiles.Count == 0) 40 | { 41 | throw new InvalidOperationException($"same extension file does not exist: {filePath}"); 42 | } 43 | 44 | int currentIndex = foundFiles.FindIndex(f => string.Equals(f, filePath, StringComparison.OrdinalIgnoreCase)); 45 | if (currentIndex == -1) 46 | { 47 | throw new InvalidOperationException($"current file does not exist: {filePath}"); 48 | } 49 | 50 | string? next = (currentIndex < foundFiles.Count - 1) ? foundFiles[currentIndex + 1] : null; 51 | string? prev = (currentIndex > 0) ? foundFiles[currentIndex - 1] : null; 52 | 53 | return (prev, next); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /LLPlayer/Extensions/FocusBehavior.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Controls; 3 | using System.Windows.Threading; 4 | 5 | namespace LLPlayer.Extensions; 6 | 7 | public static class FocusBehavior 8 | { 9 | public static readonly DependencyProperty IsFocusedProperty = 10 | DependencyProperty.RegisterAttached( 11 | "IsFocused", 12 | typeof(bool), 13 | typeof(FocusBehavior), 14 | new UIPropertyMetadata(false, OnIsFocusedChanged)); 15 | 16 | public static bool GetIsFocused(DependencyObject obj) => 17 | (bool)obj.GetValue(IsFocusedProperty); 18 | 19 | public static void SetIsFocused(DependencyObject obj, bool value) => 20 | obj.SetValue(IsFocusedProperty, value); 21 | 22 | private static void OnIsFocusedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 23 | { 24 | if (!(d is UIElement element) || !(e.NewValue is bool isFocused) || !isFocused) 25 | return; 26 | 27 | // Set focus to element 28 | element.Dispatcher.BeginInvoke(() => 29 | { 30 | element.Focus(); 31 | if (element is TextBox tb) 32 | { 33 | // if TextBox, then select text 34 | tb.SelectAll(); 35 | //tb.CaretIndex = tb.Text.Length; 36 | } 37 | }, DispatcherPriority.Input); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LLPlayer/Extensions/Guards.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace LLPlayer.Extensions; 5 | 6 | public static class Guards 7 | { 8 | /// Throws an immediately. 9 | /// error message 10 | /// 11 | public static void Fail(string? message = null) 12 | { 13 | throw new InvalidOperationException(message); 14 | } 15 | 16 | /// Throws an if is null. 17 | /// The reference type variable to validate as non-null. 18 | /// The name of the variable with which corresponds. 19 | /// 20 | public static void ThrowIfNull([NotNull] object? variable, [CallerArgumentExpression(nameof(variable))] string? variableName = null) 21 | { 22 | if (variable is null) 23 | { 24 | throw new InvalidOperationException(variableName); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LLPlayer/Extensions/HyperLinkHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Windows; 3 | using System.Windows.Documents; 4 | using System.Windows.Navigation; 5 | 6 | namespace LLPlayer.Extensions; 7 | 8 | public static class HyperlinkHelper 9 | { 10 | public static readonly DependencyProperty OpenInBrowserProperty = 11 | DependencyProperty.RegisterAttached( 12 | "OpenInBrowser", 13 | typeof(bool), 14 | typeof(HyperlinkHelper), 15 | new PropertyMetadata(false, OnOpenInBrowserChanged)); 16 | 17 | public static bool GetOpenInBrowser(DependencyObject obj) 18 | { 19 | return (bool)obj.GetValue(OpenInBrowserProperty); 20 | } 21 | 22 | public static void SetOpenInBrowser(DependencyObject obj, bool value) 23 | { 24 | obj.SetValue(OpenInBrowserProperty, value); 25 | } 26 | 27 | private static void OnOpenInBrowserChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 28 | { 29 | if (d is Hyperlink hyperlink) 30 | { 31 | bool newValue = (bool)e.NewValue; 32 | if (newValue) 33 | { 34 | hyperlink.RequestNavigate += OnRequestNavigate; 35 | } 36 | else 37 | { 38 | hyperlink.RequestNavigate -= OnRequestNavigate; 39 | } 40 | } 41 | } 42 | 43 | private static void OnRequestNavigate(object sender, RequestNavigateEventArgs e) 44 | { 45 | OpenUrlInBrowser(e.Uri.AbsoluteUri); 46 | e.Handled = true; 47 | } 48 | 49 | public static void OpenUrlInBrowser(string url) 50 | { 51 | Process.Start(new ProcessStartInfo 52 | { 53 | FileName = url, 54 | UseShellExecute = true 55 | }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /LLPlayer/Extensions/JsonInterfaceConcreteConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace LLPlayer.Extensions; 5 | 6 | /// 7 | /// JsonConverter to serialize and deserialize interfaces with concrete types using a mapping between interfaces and concrete types 8 | /// 9 | /// 10 | public class JsonInterfaceConcreteConverter : JsonConverter 11 | { 12 | private const string TypeKey = "TypeName"; 13 | private readonly Dictionary _typeMapping; 14 | 15 | public JsonInterfaceConcreteConverter(Dictionary typeMapping) 16 | { 17 | _typeMapping = typeMapping; 18 | } 19 | 20 | public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 21 | { 22 | using JsonDocument jsonDoc = JsonDocument.ParseValue(ref reader); 23 | 24 | if (!jsonDoc.RootElement.TryGetProperty(TypeKey, out JsonElement typeProperty)) 25 | { 26 | throw new JsonException("Type discriminator not found."); 27 | } 28 | 29 | string? typeDiscriminator = typeProperty.GetString(); 30 | if (typeDiscriminator == null || !_typeMapping.TryGetValue(typeDiscriminator, out Type? targetType)) 31 | { 32 | throw new JsonException($"Unknown type discriminator: {typeDiscriminator}"); 33 | } 34 | 35 | // If a specific type is specified as the second argument, it is deserialized with that type 36 | return (T)JsonSerializer.Deserialize(jsonDoc.RootElement, targetType, options)!; 37 | } 38 | 39 | public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) 40 | { 41 | Type type = value!.GetType(); 42 | string typeDiscriminator = type.Name; // Use type name as discriminator 43 | 44 | // Serialize with concrete types, not interfaces 45 | string json = JsonSerializer.Serialize(value, type, options); 46 | using JsonDocument jsonDoc = JsonDocument.Parse(json); 47 | 48 | writer.WriteStartObject(); 49 | // Save concrete type name 50 | writer.WriteString(TypeKey, typeDiscriminator); 51 | 52 | // Does this work even if it's nested? 53 | foreach (JsonProperty property in jsonDoc.RootElement.EnumerateObject()) 54 | { 55 | property.WriteTo(writer); 56 | } 57 | 58 | writer.WriteEndObject(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /LLPlayer/Extensions/MyDialogWindow.xaml: -------------------------------------------------------------------------------- 1 |  13 | 14 | -------------------------------------------------------------------------------- /LLPlayer/Extensions/MyDialogWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using LLPlayer.Views; 3 | 4 | namespace LLPlayer.Extensions; 5 | 6 | public partial class MyDialogWindow : Window, IDialogWindow 7 | { 8 | public IDialogResult? Result { get; set; } 9 | 10 | public MyDialogWindow() 11 | { 12 | InitializeComponent(); 13 | 14 | MainWindow.SetTitleBarDarkMode(this); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /LLPlayer/Extensions/ScrollParentWhenAtMax.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Controls; 3 | using System.Windows.Input; 4 | using System.Windows.Media; 5 | using Microsoft.Xaml.Behaviors; 6 | 7 | namespace LLPlayer.Extensions; 8 | 9 | /// 10 | /// Behavior to disable scrolling in ListView 11 | /// ref: https://stackoverflow.com/questions/1585462/bubbling-scroll-events-from-a-listview-to-its-parent 12 | /// 13 | public class ScrollParentWhenAtMax : Behavior 14 | { 15 | protected override void OnAttached() 16 | { 17 | base.OnAttached(); 18 | AssociatedObject.PreviewMouseWheel += PreviewMouseWheel; 19 | } 20 | 21 | protected override void OnDetaching() 22 | { 23 | AssociatedObject.PreviewMouseWheel -= PreviewMouseWheel; 24 | base.OnDetaching(); 25 | } 26 | 27 | private void PreviewMouseWheel(object sender, MouseWheelEventArgs e) 28 | { 29 | var scrollViewer = GetVisualChild(AssociatedObject); 30 | var scrollPos = scrollViewer!.ContentVerticalOffset; 31 | if ((scrollPos == scrollViewer.ScrollableHeight && e.Delta < 0) 32 | || (scrollPos == 0 && e.Delta > 0)) 33 | { 34 | e.Handled = true; 35 | MouseWheelEventArgs e2 = new(e.MouseDevice, e.Timestamp, e.Delta); 36 | e2.RoutedEvent = UIElement.MouseWheelEvent; 37 | AssociatedObject.RaiseEvent(e2); 38 | } 39 | } 40 | 41 | private static T? GetVisualChild(DependencyObject parent) where T : Visual 42 | { 43 | T? child = null; 44 | 45 | int numVisuals = VisualTreeHelper.GetChildrenCount(parent); 46 | for (int i = 0; i < numVisuals; i++) 47 | { 48 | Visual v = (Visual)VisualTreeHelper.GetChild(parent, i); 49 | child = v as T; 50 | if (child == null) 51 | { 52 | child = GetVisualChild(v); 53 | } 54 | 55 | if (child != null) 56 | { 57 | break; 58 | } 59 | } 60 | 61 | return child; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /LLPlayer/Extensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace LLPlayer.Extensions; 4 | 5 | public static class StringExtensions 6 | { 7 | /// 8 | /// Split for various types of newline codes 9 | /// 10 | /// 11 | /// 12 | public static IEnumerable SplitToLines(this string? input) 13 | { 14 | if (input == null) 15 | { 16 | yield break; 17 | } 18 | 19 | using StringReader reader = new(input); 20 | 21 | string? line; 22 | while ((line = reader.ReadLine()) != null) 23 | { 24 | yield return line; 25 | } 26 | } 27 | 28 | /// 29 | /// Convert only the first character to lower case 30 | /// 31 | /// 32 | /// 33 | public static string ToLowerFirstChar(this string input) 34 | { 35 | if (string.IsNullOrEmpty(input)) 36 | return input; 37 | 38 | return char.ToLower(input[0]) + input.Substring(1); 39 | } 40 | 41 | /// 42 | /// Convert only the first character to upper case 43 | /// 44 | /// 45 | /// 46 | public static string ToUpperFirstChar(this string input) 47 | { 48 | if (string.IsNullOrEmpty(input)) 49 | return input; 50 | 51 | return char.ToUpper(input[0]) + input.Substring(1); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /LLPlayer/Extensions/TextBoxMiscHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using System.Windows; 3 | using System.Windows.Controls; 4 | using System.Windows.Input; 5 | 6 | namespace LLPlayer.Extensions; 7 | 8 | public static class TextBoxMiscHelper 9 | { 10 | public static bool GetIsHexValidationEnabled(DependencyObject obj) 11 | { 12 | return (bool)obj.GetValue(IsHexValidationEnabledProperty); 13 | } 14 | 15 | public static void SetIsHexValidationEnabled(DependencyObject obj, bool value) 16 | { 17 | obj.SetValue(IsHexValidationEnabledProperty, value); 18 | } 19 | 20 | public static readonly DependencyProperty IsHexValidationEnabledProperty = 21 | DependencyProperty.RegisterAttached( 22 | "IsHexValidationEnabled", 23 | typeof(bool), 24 | typeof(TextBoxMiscHelper), 25 | new UIPropertyMetadata(false, OnIsHexValidationEnabledChanged)); 26 | 27 | private static void OnIsHexValidationEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 28 | { 29 | if (d is TextBox textBox) 30 | { 31 | if ((bool)e.NewValue) 32 | { 33 | textBox.PreviewTextInput += OnPreviewTextInput; 34 | } 35 | else 36 | { 37 | textBox.PreviewTextInput -= OnPreviewTextInput; 38 | } 39 | } 40 | } 41 | 42 | private static void OnPreviewTextInput(object sender, TextCompositionEventArgs e) 43 | { 44 | e.Handled = !Regex.IsMatch(e.Text, "^[0-9a-f]+$", RegexOptions.IgnoreCase); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /LLPlayer/Extensions/UIHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Media; 3 | 4 | namespace LLPlayer.Extensions; 5 | 6 | public static class UIHelper 7 | { 8 | // ref: https://www.infragistics.com/community/blogs/b/blagunas/posts/find-the-parent-control-of-a-specific-type-in-wpf-and-silverlight 9 | public static T? FindParent(DependencyObject child) where T : DependencyObject 10 | { 11 | //get parent item 12 | DependencyObject? parentObject = VisualTreeHelper.GetParent(child); 13 | 14 | //we've reached the end of the tree 15 | if (parentObject == null) 16 | return null; 17 | 18 | //check if the parent matches the type we're looking for 19 | if (parentObject is T parent) 20 | return parent; 21 | 22 | return FindParent(parentObject); 23 | } 24 | 25 | /// 26 | /// Traverses the visual tree upward from the current element to determine if an element with the specified name exists.j 27 | /// 28 | /// Element to start with (current element) 29 | /// Name of the element 30 | /// True if the element with the specified name exists, false otherwise. 31 | public static bool FindParentWithName(DependencyObject? element, string name) 32 | { 33 | if (element == null) 34 | { 35 | return false; 36 | } 37 | 38 | DependencyObject? current = element; 39 | 40 | while (current != null) 41 | { 42 | if (current is FrameworkElement fe && fe.Name == name) 43 | { 44 | return true; 45 | } 46 | 47 | current = VisualTreeHelper.GetParent(current); 48 | } 49 | 50 | return false; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /LLPlayer/Extensions/WindowsClipboard.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace LLPlayer.Extensions; 5 | 6 | // ref: https://stackoverflow.com/questions/44205260/net-core-copy-to-clipboard 7 | public static class WindowsClipboard 8 | { 9 | public static void SetText(string text) 10 | { 11 | OpenClipboard(); 12 | 13 | EmptyClipboard(); 14 | IntPtr hGlobal = 0; 15 | try 16 | { 17 | int bytes = (text.Length + 1) * 2; 18 | hGlobal = Marshal.AllocHGlobal(bytes); 19 | 20 | if (hGlobal == 0) 21 | { 22 | ThrowWin32(); 23 | } 24 | 25 | IntPtr target = GlobalLock(hGlobal); 26 | 27 | if (target == 0) 28 | { 29 | ThrowWin32(); 30 | } 31 | 32 | try 33 | { 34 | Marshal.Copy(text.ToCharArray(), 0, target, text.Length); 35 | } 36 | finally 37 | { 38 | GlobalUnlock(target); 39 | } 40 | 41 | if (SetClipboardData(cfUnicodeText, hGlobal) == 0) 42 | { 43 | ThrowWin32(); 44 | } 45 | 46 | hGlobal = 0; 47 | } 48 | finally 49 | { 50 | if (hGlobal != 0) 51 | { 52 | Marshal.FreeHGlobal(hGlobal); 53 | } 54 | 55 | CloseClipboard(); 56 | } 57 | } 58 | 59 | public static void OpenClipboard() 60 | { 61 | int num = 10; 62 | while (true) 63 | { 64 | if (OpenClipboard(0)) 65 | { 66 | break; 67 | } 68 | 69 | if (--num == 0) 70 | { 71 | ThrowWin32(); 72 | } 73 | 74 | Thread.Sleep(20); 75 | } 76 | } 77 | 78 | const uint cfUnicodeText = 13; 79 | 80 | static void ThrowWin32() 81 | { 82 | throw new Win32Exception(Marshal.GetLastWin32Error()); 83 | } 84 | 85 | [DllImport("kernel32.dll", SetLastError = true)] 86 | static extern IntPtr GlobalLock(IntPtr hMem); 87 | 88 | [DllImport("kernel32.dll", SetLastError = true)] 89 | [return: MarshalAs(UnmanagedType.Bool)] 90 | static extern bool GlobalUnlock(IntPtr hMem); 91 | 92 | [DllImport("user32.dll", SetLastError = true)] 93 | [return: MarshalAs(UnmanagedType.Bool)] 94 | static extern bool OpenClipboard(IntPtr hWndNewOwner); 95 | 96 | [DllImport("user32.dll", SetLastError = true)] 97 | [return: MarshalAs(UnmanagedType.Bool)] 98 | static extern bool CloseClipboard(); 99 | 100 | [DllImport("user32.dll", SetLastError = true)] 101 | static extern IntPtr SetClipboardData(uint uFormat, IntPtr data); 102 | 103 | [DllImport("user32.dll")] 104 | static extern bool EmptyClipboard(); 105 | } 106 | -------------------------------------------------------------------------------- /LLPlayer/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | // For some reason, if these are not imported, it cannot be built with anything other than portable for target runtime. 2 | // Prism import errors occur, so work around this. 3 | global using Prism; 4 | global using Prism.Commands; 5 | global using Prism.DryIoc; 6 | global using Prism.Dialogs; 7 | global using Prism.Ioc; 8 | -------------------------------------------------------------------------------- /LLPlayer/LLPlayer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | WinExe 5 | net9.0-windows10.0.18362.0 6 | enable 7 | preview 8 | enable 9 | true 10 | umlx5h 11 | umlx5h © 2025 12 | GPL-3.0-or-later 13 | LLPlayer.ico 14 | The media player for language learning. 15 | https://llplayer.com 16 | 0.2.2 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | PreserveNewest 40 | 41 | 42 | PreserveNewest 43 | 44 | 45 | PreserveNewest 46 | 47 | 48 | PreserveNewest 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /LLPlayer/LLPlayer.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umlx5h/LLPlayer/878511b033609f4f5e7f57f92ddf8bde10f607c8/LLPlayer/LLPlayer.ico -------------------------------------------------------------------------------- /LLPlayer/Properties/PublishProfiles/FolderProfile.pubxml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Release 6 | Any CPU 7 | bin\Release\net9.0-windows10.0.18362.0\publish\win-x64\ 8 | FileSystem 9 | <_TargetId>Folder 10 | net9.0-windows10.0.18362.0 11 | win-x64 12 | false 13 | true 14 | true 15 | 16 | 17 | -------------------------------------------------------------------------------- /LLPlayer/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "LLPlayer": { 4 | "commandName": "Project" 5 | }, 6 | "LLPlayer native": { 7 | "commandName": "Project", 8 | "nativeDebugging": true 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /LLPlayer/Resources/Converters.xaml: -------------------------------------------------------------------------------- 1 |  5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /LLPlayer/Resources/Images/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umlx5h/LLPlayer/878511b033609f4f5e7f57f92ddf8bde10f607c8/LLPlayer/Resources/Images/pause.png -------------------------------------------------------------------------------- /LLPlayer/Resources/Images/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umlx5h/LLPlayer/878511b033609f4f5e7f57f92ddf8bde10f607c8/LLPlayer/Resources/Images/play.png -------------------------------------------------------------------------------- /LLPlayer/Resources/MaterialDesignMy.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Controls; 3 | using System.Windows.Input; 4 | 5 | namespace LLPlayer.Resources; 6 | 7 | public partial class MaterialDesignMy : ResourceDictionary 8 | { 9 | public MaterialDesignMy() 10 | { 11 | InitializeComponent(); 12 | } 13 | 14 | /// 15 | /// Do not close the menu when right-clicking or CTRL+left-clicking on a context menu 16 | /// This is achieved by dynamically setting StaysOpenOnClick 17 | /// 18 | /// 19 | /// 20 | private void MenuItem_PreviewMouseDown(object sender, MouseButtonEventArgs e) 21 | { 22 | if (sender is MenuItem menuItem) 23 | { 24 | if ((Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) || 25 | Mouse.RightButton == MouseButtonState.Pressed) 26 | { 27 | menuItem.StaysOpenOnClick = true; 28 | } 29 | else if (menuItem.StaysOpenOnClick) 30 | { 31 | menuItem.StaysOpenOnClick = false; 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LLPlayer/Resources/PopupMenu.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Controls; 3 | using LLPlayer.ViewModels; 4 | 5 | namespace LLPlayer.Resources; 6 | 7 | public partial class PopupMenu : ResourceDictionary 8 | { 9 | public PopupMenu() 10 | { 11 | InitializeComponent(); 12 | } 13 | 14 | private void PopUpMenu_OnOpened(object sender, RoutedEventArgs e) 15 | { 16 | // TODO: L: should validate that the clipboard content is a video file? 17 | bool canPaste = !string.IsNullOrEmpty(Clipboard.GetText()); 18 | MenuPasteUrl.IsEnabled = canPaste; 19 | 20 | // Don't hide the seek bar while displaying the context menu 21 | if (sender is ContextMenu menu && menu.DataContext is FlyleafOverlayVM vm) 22 | { 23 | vm.FL.Player.Activity.IsEnabled = false; 24 | } 25 | } 26 | 27 | private void PopUpMenu_OnClosed(object sender, RoutedEventArgs e) 28 | { 29 | if (sender is ContextMenu menu && menu.DataContext is FlyleafOverlayVM vm) 30 | { 31 | vm.FL.Player.Activity.IsEnabled = true; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LLPlayer/Resources/Validators.xaml: -------------------------------------------------------------------------------- 1 |  5 | 6 | 7 | -------------------------------------------------------------------------------- /LLPlayer/Resources/Validators.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Text.RegularExpressions; 3 | using System.Windows; 4 | using System.Windows.Controls; 5 | 6 | namespace LLPlayer.Resources; 7 | 8 | public partial class Validators : ResourceDictionary 9 | { 10 | public Validators() 11 | { 12 | InitializeComponent(); 13 | } 14 | } 15 | 16 | public class ColorHexRule : ValidationRule 17 | { 18 | public override ValidationResult Validate(object? value, CultureInfo cultureInfo) 19 | { 20 | if (value != null && Regex.IsMatch(value.ToString() ?? string.Empty, "^[0-9a-f]{6}$", RegexOptions.IgnoreCase)) 21 | { 22 | return new ValidationResult(true, null); 23 | } 24 | 25 | return new ValidationResult(false, "Invalid"); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LLPlayer/Services/ErrorDialogHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using FlyleafLib.MediaPlayer; 3 | using LLPlayer.Views; 4 | 5 | namespace LLPlayer.Services; 6 | 7 | public static class ErrorDialogHelper 8 | { 9 | public static void ShowKnownErrorPopup(string message, string errorType) 10 | { 11 | var dialogService = ((App)Application.Current).Container.Resolve(); 12 | 13 | DialogParameters p = new() 14 | { 15 | { "type", "known" }, 16 | { "message", message }, 17 | { "errorType", errorType } 18 | }; 19 | 20 | dialogService.ShowDialog(nameof(ErrorDialog), p); 21 | } 22 | 23 | public static void ShowKnownErrorPopup(string message, KnownErrorType errorType) 24 | { 25 | ShowKnownErrorPopup(message, errorType.ToString()); 26 | } 27 | 28 | public static void ShowUnknownErrorPopup(string message, string errorType, Exception? ex = null) 29 | { 30 | var dialogService = ((App)Application.Current).Container.Resolve(); 31 | 32 | DialogParameters p = new() 33 | { 34 | { "type", "unknown" }, 35 | { "message", message }, 36 | { "errorType", errorType }, 37 | }; 38 | 39 | if (ex != null) 40 | { 41 | p.Add("exception", ex); 42 | } 43 | 44 | dialogService.ShowDialog(nameof(ErrorDialog), p); 45 | } 46 | 47 | public static void ShowUnknownErrorPopup(string message, UnknownErrorType errorType, Exception? ex = null) 48 | { 49 | ShowUnknownErrorPopup(message, errorType.ToString(), ex); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /LLPlayer/Services/FlyleafManager.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Windows; 3 | using FlyleafLib; 4 | using FlyleafLib.Controls.WPF; 5 | using FlyleafLib.MediaPlayer; 6 | 7 | namespace LLPlayer.Services; 8 | 9 | public class FlyleafManager 10 | { 11 | public Player Player { get; } 12 | public Config PlayerConfig => Player.Config; 13 | public FlyleafHost? FlyleafHost => Player.Host as FlyleafHost; 14 | public AppConfig Config { get; } 15 | public AppActions Action { get; } 16 | 17 | public AudioEngine AudioEngine => Engine.Audio; 18 | public EngineConfig ConfigEngine => Engine.Config; 19 | 20 | public FlyleafManager(Player player, IDialogService dialogService) 21 | { 22 | Player = player; 23 | 24 | // Load app configuration at this time 25 | Config = LoadAppConfig(); 26 | Action = new AppActions(Player, Config, dialogService); 27 | } 28 | 29 | private AppConfig LoadAppConfig() 30 | { 31 | AppConfig? config = null; 32 | 33 | if (File.Exists(App.AppConfigPath)) 34 | { 35 | try 36 | { 37 | config = AppConfig.Load(App.AppConfigPath); 38 | 39 | if (config.Version != App.Version) 40 | { 41 | config.Version = App.Version; 42 | config.Save(App.AppConfigPath); 43 | } 44 | } 45 | catch (Exception ex) 46 | { 47 | MessageBox.Show($"Cannot load AppConfig from {Path.GetFileName(App.AppConfigPath)}, Please review the settings or delete the config file. Error details are recorded in {Path.GetFileName(App.CrashLogPath)}."); 48 | try 49 | { 50 | File.WriteAllText(App.CrashLogPath, "AppConfig Loading Error: " + ex); 51 | } 52 | catch 53 | { 54 | // ignored 55 | } 56 | 57 | Application.Current.Shutdown(); 58 | } 59 | } 60 | 61 | if (config == null) 62 | { 63 | config = new AppConfig(); 64 | } 65 | config.Initialize(this); 66 | 67 | return config; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /LLPlayer/Services/PDICSender.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.IO; 3 | using System.Text; 4 | 5 | namespace LLPlayer.Services; 6 | 7 | public class PipeClient : IDisposable 8 | { 9 | private Process _proc; 10 | 11 | public PipeClient(string pipePath) 12 | { 13 | _proc = new Process 14 | { 15 | StartInfo = new ProcessStartInfo() 16 | { 17 | RedirectStandardOutput = true, 18 | RedirectStandardInput = true, 19 | RedirectStandardError = true, 20 | FileName = pipePath, 21 | CreateNoWindow = true, 22 | } 23 | }; 24 | _proc.Start(); 25 | } 26 | 27 | public async Task SendMessage(string message) 28 | { 29 | Debug.WriteLine(message); 30 | //byte[] bytes = JsonSerializer.SerializeToUtf8Bytes(message); 31 | 32 | // Enclose double quotes before and after since it is sent as a JSON string 33 | byte[] bytes = Encoding.UTF8.GetBytes('"' + message + '"'); 34 | 35 | var length = BitConverter.GetBytes(bytes.Length); 36 | await _proc.StandardInput.BaseStream.WriteAsync(length, 0, length.Length); 37 | await _proc.StandardInput.BaseStream.WriteAsync(bytes, 0, bytes.Length); 38 | await _proc.StandardInput.BaseStream.FlushAsync(); 39 | } 40 | 41 | public void Dispose() 42 | { 43 | _proc.Kill(); 44 | _proc.WaitForExit(); 45 | } 46 | } 47 | 48 | public class PDICSender : IDisposable 49 | { 50 | private readonly PipeClient _pipeClient; 51 | public FlyleafManager FL { get; } 52 | 53 | public PDICSender(FlyleafManager fl) 54 | { 55 | FL = fl; 56 | 57 | string? exePath = FL.Config.Subs.PDICPipeExecutablePath; 58 | 59 | if (!File.Exists(exePath)) 60 | { 61 | throw new FileNotFoundException($"PDIC executable is not set correctly: {exePath}"); 62 | } 63 | 64 | _pipeClient = new PipeClient(exePath); 65 | } 66 | 67 | public async void Dispose() 68 | { 69 | await _pipeClient.SendMessage("p:Dictionary,Close,"); 70 | _pipeClient.Dispose(); 71 | } 72 | 73 | public async Task Connect() 74 | { 75 | await _pipeClient.SendMessage("p:Dictionary,Open,"); 76 | } 77 | 78 | // Send the same way as Firepop 79 | // webextension native extensions 80 | // ref: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging 81 | 82 | // See Firepop source 83 | public async Task SendWithPipe(string sentence, int offset) 84 | { 85 | try 86 | { 87 | await _pipeClient.SendMessage($"p:Dictionary,SetUrl,{App.Name}"); 88 | await _pipeClient.SendMessage($"p:Dictionary,PopupSearch3,{offset},{sentence}"); 89 | 90 | // Incremental search 91 | //await _pipeClient.SendMessage($"p:Simulate,InputWord3,word"); 92 | 93 | return 0; 94 | } 95 | catch (Exception e) 96 | { 97 | Debug.WriteLine(e.ToString()); 98 | return -1; 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /LLPlayer/Services/SrtExporter.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Text; 3 | 4 | namespace LLPlayer.Services; 5 | 6 | public static class SrtExporter 7 | { 8 | // TODO: L: Supports tags such as ? 9 | public static void ExportSrt(List lines, string filePath, Encoding encoding) 10 | { 11 | using StreamWriter writer = new(filePath, false, encoding); 12 | 13 | foreach (var (i, line) in lines.Index()) 14 | { 15 | writer.WriteLine((i + 1).ToString()); 16 | writer.WriteLine($"{FormatTime(line.Start)} --> {FormatTime(line.End)}"); 17 | writer.WriteLine(line.Text); 18 | // blank line expect last 19 | if (i != lines.Count - 1) 20 | { 21 | writer.WriteLine(); 22 | } 23 | } 24 | } 25 | 26 | private static string FormatTime(TimeSpan time) 27 | { 28 | return string.Format("{0:00}:{1:00}:{2:00},{3:000}", 29 | (int)time.TotalHours, 30 | time.Minutes, 31 | time.Seconds, 32 | time.Milliseconds); 33 | } 34 | } 35 | 36 | public class SubtitleLine 37 | { 38 | public required TimeSpan Start { get; init; } 39 | public required TimeSpan End { get; init; } 40 | public required string Text { get; init; } 41 | } 42 | -------------------------------------------------------------------------------- /LLPlayer/Services/WhisperCppModelLoader.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using FlyleafLib; 3 | using Whisper.net.Ggml; 4 | 5 | namespace LLPlayer.Services; 6 | 7 | public class WhisperCppModelLoader 8 | { 9 | public static List LoadAllModels() 10 | { 11 | WhisperConfig.EnsureModelsDirectory(); 12 | 13 | List models = Enum.GetValues() 14 | .Select(t => new WhisperCppModel { Model = t, }) 15 | .ToList(); 16 | 17 | foreach (WhisperCppModel model in models) 18 | { 19 | // Update download status 20 | string path = model.ModelFilePath; 21 | if (File.Exists(path)) 22 | { 23 | model.Size = new FileInfo(path).Length; 24 | } 25 | } 26 | 27 | return models; 28 | } 29 | 30 | public static List LoadDownloadedModels() 31 | { 32 | return LoadAllModels() 33 | .Where(m => m.Downloaded) 34 | .ToList(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LLPlayer/Themes/Generic.xaml: -------------------------------------------------------------------------------- 1 |  3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /LLPlayer/Themes/SelectableTextBox.xaml: -------------------------------------------------------------------------------- 1 |  4 | 5 | 6 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /LLPlayer/ViewModels/SettingsDialogVM.cs: -------------------------------------------------------------------------------- 1 | using LLPlayer.Extensions; 2 | using LLPlayer.Services; 3 | 4 | namespace LLPlayer.ViewModels; 5 | 6 | public class SettingsDialogVM : Bindable, IDialogAware 7 | { 8 | public FlyleafManager FL { get; } 9 | public SettingsDialogVM(FlyleafManager fl) 10 | { 11 | FL = fl; 12 | } 13 | 14 | public DelegateCommand? CmdCloseDialog => field ??= new((parameter) => 15 | { 16 | ButtonResult result = ButtonResult.None; 17 | 18 | if (parameter == "Save") 19 | { 20 | result = ButtonResult.OK; 21 | } 22 | 23 | RequestClose.Invoke(result); 24 | }); 25 | 26 | #region IDialogAware 27 | public string Title { get; set => Set(ref field, value); } = $"Settings - {App.Name}"; 28 | public double WindowWidth { get; set => Set(ref field, value); } = 1000; 29 | public double WindowHeight { get; set => Set(ref field, value); } = 700; 30 | 31 | public bool CanCloseDialog() => true; 32 | 33 | public void OnDialogClosed() 34 | { 35 | } 36 | 37 | public void OnDialogOpened(IDialogParameters parameters) 38 | { 39 | } 40 | 41 | public DialogCloseListener RequestClose { get; } 42 | #endregion 43 | } 44 | -------------------------------------------------------------------------------- /LLPlayer/Views/CheatSheetDialog.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Windows; 3 | using System.Windows.Controls; 4 | using System.Windows.Data; 5 | using System.Windows.Input; 6 | using LLPlayer.ViewModels; 7 | 8 | namespace LLPlayer.Views; 9 | 10 | public partial class CheatSheetDialog : UserControl 11 | { 12 | public CheatSheetDialog() 13 | { 14 | InitializeComponent(); 15 | 16 | DataContext = ((App)Application.Current).Container.Resolve(); 17 | } 18 | 19 | private void OnLoaded(object sender, RoutedEventArgs e) 20 | { 21 | Window? window = Window.GetWindow(this); 22 | window!.CommandBindings.Add(new CommandBinding(ApplicationCommands.Find, OnFindExecuted)); 23 | } 24 | 25 | private void OnFindExecuted(object sender, ExecutedRoutedEventArgs e) 26 | { 27 | SearchBox.Focus(); 28 | SearchBox.SelectAll(); 29 | e.Handled = true; 30 | } 31 | } 32 | 33 | [ValueConversion(typeof(int), typeof(Visibility))] 34 | public class CountToVisibilityConverter : IValueConverter 35 | { 36 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 37 | { 38 | if (int.TryParse(value.ToString(), out var count)) 39 | { 40 | return count == 0 ? Visibility.Collapsed : Visibility.Visible; 41 | } 42 | 43 | return Visibility.Visible; 44 | } 45 | 46 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 47 | { 48 | throw new NotImplementedException(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /LLPlayer/Views/ErrorDialog.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using LLPlayer.ViewModels; 3 | using System.Windows.Controls; 4 | using System.Windows.Input; 5 | 6 | namespace LLPlayer.Views; 7 | 8 | public partial class ErrorDialog : UserControl 9 | { 10 | private ErrorDialogVM VM => (ErrorDialogVM)DataContext; 11 | 12 | public ErrorDialog() 13 | { 14 | InitializeComponent(); 15 | 16 | DataContext = ((App)Application.Current).Container.Resolve(); 17 | } 18 | 19 | private void FrameworkElement_OnLoaded(object sender, RoutedEventArgs e) 20 | { 21 | Keyboard.Focus(sender as IInputElement); 22 | } 23 | 24 | private void ErrorDialog_OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e) 25 | { 26 | Keyboard.Focus(sender as IInputElement); 27 | } 28 | 29 | // Topmost dialog, so it should be draggable 30 | private void Window_MouseDown(object sender, MouseButtonEventArgs e) 31 | { 32 | if (sender is not Window window) 33 | return; 34 | 35 | if (e.ChangedButton == MouseButton.Left) 36 | { 37 | window.DragMove(); 38 | } 39 | } 40 | 41 | // Make TextBox uncopyable 42 | private void TextBox_PreviewMouseDown(object sender, ExecutedRoutedEventArgs e) 43 | { 44 | if (e.Command == ApplicationCommands.Copy || 45 | e.Command == ApplicationCommands.Cut || 46 | e.Command == ApplicationCommands.Paste) 47 | { 48 | e.Handled = true; 49 | 50 | if (e.Command == ApplicationCommands.Copy) 51 | { 52 | // instead trigger copy command 53 | VM.CmdCopyMessage.Execute(); 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /LLPlayer/Views/FlyleafOverlay.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Controls; 3 | using LLPlayer.ViewModels; 4 | 5 | namespace LLPlayer.Views; 6 | 7 | public partial class FlyleafOverlay : UserControl 8 | { 9 | private FlyleafOverlayVM VM => (FlyleafOverlayVM)DataContext; 10 | 11 | public FlyleafOverlay() 12 | { 13 | InitializeComponent(); 14 | 15 | DataContext = ((App)Application.Current).Container.Resolve(); 16 | } 17 | 18 | private void FlyleafOverlay_OnSizeChanged(object sender, SizeChangedEventArgs e) 19 | { 20 | if (e.HeightChanged) 21 | { 22 | // The height of MainWindow cannot be used because it includes the title bar, 23 | // so the height is obtained here and passed on. 24 | double heightDiff = Math.Abs(e.NewSize.Height - e.PreviousSize.Height); 25 | 26 | if (heightDiff >= 1.0) 27 | { 28 | VM.FL.Config.ScreenWidth = e.NewSize.Width; 29 | VM.FL.Config.ScreenHeight = e.NewSize.Height; 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LLPlayer/Views/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using System.Windows; 3 | using System.Windows.Interop; 4 | using LLPlayer.Services; 5 | using LLPlayer.ViewModels; 6 | 7 | namespace LLPlayer.Views; 8 | 9 | public partial class MainWindow : Window 10 | { 11 | public MainWindow() 12 | { 13 | // If this is not called first, the constructor of the other control will 14 | // run before FlyleafHost is initialized, so it will not work. 15 | DataContext = ((App)Application.Current).Container.Resolve(); 16 | 17 | InitializeComponent(); 18 | 19 | SetWindowSize(); 20 | SetTitleBarDarkMode(this); 21 | } 22 | 23 | private void SetWindowSize() 24 | { 25 | // 16:9 size list 26 | List candidateSizes = 27 | [ 28 | new(1280, 720), 29 | new(1024, 576), 30 | new(960, 540), 31 | new(800, 450), 32 | new(640, 360), 33 | new(480, 270), 34 | new(320, 180) 35 | ]; 36 | 37 | // Get available screen width / height 38 | double availableWidth = SystemParameters.WorkArea.Width; 39 | double availableHeight = SystemParameters.WorkArea.Height; 40 | 41 | // Get the largest size that will fit on the screen 42 | Size selectedSize = candidateSizes.FirstOrDefault( 43 | s => s.Width <= availableWidth && s.Height <= availableHeight, 44 | candidateSizes[^1]); 45 | 46 | // Set 47 | Width = selectedSize.Width; 48 | Height = selectedSize.Height; 49 | } 50 | 51 | #region Dark Title Bar 52 | /// 53 | /// ref: 54 | /// 55 | [DllImport("DwmApi")] 56 | private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, int[] attrValue, int attrSize); 57 | const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20; 58 | 59 | public static void SetTitleBarDarkMode(Window window) 60 | { 61 | // Check OS Version 62 | if (!(Environment.OSVersion.Version >= new Version(10, 0, 18985))) 63 | { 64 | return; 65 | } 66 | 67 | var fl = ((App)Application.Current).Container.Resolve(); 68 | if (!fl.Config.IsDarkTitlebar) 69 | { 70 | return; 71 | } 72 | 73 | bool darkMode = true; 74 | 75 | // Set title bar to dark mode 76 | // ref: https://stackoverflow.com/questions/71362654/wpf-window-titlebar 77 | IntPtr hWnd = new WindowInteropHelper(window).EnsureHandle(); 78 | DwmSetWindowAttribute(hWnd, DWMWA_USE_IMMERSIVE_DARK_MODE, [darkMode ? 1 : 0], 4); 79 | } 80 | #endregion 81 | } 82 | -------------------------------------------------------------------------------- /LLPlayer/Views/SelectLanguageDialog.xaml.cs: -------------------------------------------------------------------------------- 1 | using LLPlayer.ViewModels; 2 | using System.Windows; 3 | using System.Windows.Controls; 4 | using System.Windows.Input; 5 | 6 | namespace LLPlayer.Views; 7 | 8 | public partial class SelectLanguageDialog : UserControl 9 | { 10 | public SelectLanguageDialog() 11 | { 12 | InitializeComponent(); 13 | 14 | DataContext = ((App)Application.Current).Container.Resolve(); 15 | } 16 | 17 | private void OnLoaded(object sender, RoutedEventArgs e) 18 | { 19 | Window? window = Window.GetWindow(this); 20 | window!.CommandBindings.Add(new CommandBinding(ApplicationCommands.Find, OnFindExecuted)); 21 | } 22 | 23 | private void OnFindExecuted(object sender, ExecutedRoutedEventArgs e) 24 | { 25 | SearchBox.Focus(); 26 | SearchBox.SelectAll(); 27 | e.Handled = true; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LLPlayer/Views/SettingsDialog.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Controls; 3 | using LLPlayer.Controls.Settings; 4 | using LLPlayer.ViewModels; 5 | 6 | namespace LLPlayer.Views; 7 | 8 | public partial class SettingsDialog : UserControl 9 | { 10 | public SettingsDialog() 11 | { 12 | InitializeComponent(); 13 | 14 | DataContext = ((App)Application.Current).Container.Resolve(); 15 | } 16 | 17 | private void SettingsTreeView_OnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs e) 18 | { 19 | if (SettingsContent == null) 20 | { 21 | return; 22 | } 23 | 24 | if (SettingsTreeView.SelectedItem is TreeViewItem selectedItem) 25 | { 26 | string? tag = selectedItem.Tag as string; 27 | switch (tag) 28 | { 29 | case nameof(SettingsPlayer): 30 | SettingsContent.Content = new SettingsPlayer(); 31 | break; 32 | 33 | case nameof(SettingsAudio): 34 | SettingsContent.Content = new SettingsAudio(); 35 | break; 36 | 37 | case nameof(SettingsVideo): 38 | SettingsContent.Content = new SettingsVideo(); 39 | break; 40 | 41 | case nameof(SettingsSubtitles): 42 | SettingsContent.Content = new SettingsSubtitles(); 43 | break; 44 | 45 | case nameof(SettingsSubtitlesPS): 46 | SettingsContent.Content = new SettingsSubtitlesPS(); 47 | break; 48 | 49 | case nameof(SettingsSubtitlesASR): 50 | SettingsContent.Content = new SettingsSubtitlesASR(); 51 | break; 52 | 53 | case nameof(SettingsSubtitlesOCR): 54 | SettingsContent.Content = new SettingsSubtitlesOCR(); 55 | break; 56 | 57 | case nameof(SettingsSubtitlesTrans): 58 | SettingsContent.Content = new SettingsSubtitlesTrans(); 59 | break; 60 | 61 | case nameof(SettingsSubtitlesAction): 62 | SettingsContent.Content = new SettingsSubtitlesAction(); 63 | break; 64 | 65 | case nameof(SettingsKeys): 66 | SettingsContent.Content = new SettingsKeys(); 67 | break; 68 | 69 | case nameof(SettingsKeysOffset): 70 | SettingsContent.Content = new SettingsKeysOffset(); 71 | break; 72 | 73 | case nameof(SettingsMouse): 74 | SettingsContent.Content = new SettingsMouse(); 75 | break; 76 | 77 | case nameof(SettingsThemes): 78 | SettingsContent.Content = new SettingsThemes(); 79 | break; 80 | 81 | case nameof(SettingsPlugins): 82 | SettingsContent.Content = new SettingsPlugins(); 83 | break; 84 | 85 | case nameof(SettingsAbout): 86 | SettingsContent.Content = new SettingsAbout(); 87 | break; 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /LLPlayer/Views/SubtitlesDownloaderDialog.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Controls; 3 | using LLPlayer.ViewModels; 4 | 5 | namespace LLPlayer.Views; 6 | 7 | public partial class SubtitlesDownloaderDialog : UserControl 8 | { 9 | public SubtitlesDownloaderDialog() 10 | { 11 | InitializeComponent(); 12 | 13 | DataContext = ((App)Application.Current).Container.Resolve(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /LLPlayer/Views/SubtitlesExportDialog.xaml: -------------------------------------------------------------------------------- 1 |  14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 44 | 45 | 46 | 52 | 53 | 60 | 61 | 65 | 66 | 67 |