├── .github └── workflows │ └── build.yml ├── .gitignore ├── .metadata ├── .vscode └── settings.json ├── LICENSE ├── Privacy Statement.md ├── README.md ├── README_en.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── playboy │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-hdpi │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── drawable-mdpi │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable-xhdpi │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── drawable-xxhdpi │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── drawable-xxxhdpi │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ └── ic_launcher.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── devtools_options.yaml ├── l10n ├── en_us.json └── manifest.json ├── lib ├── backend │ ├── actions.dart │ ├── app.dart │ ├── constants.dart │ ├── keymap_helper.dart │ ├── library_helper.dart │ ├── ml │ │ ├── subtitle_generator.dart │ │ └── whisper_model_list.dart │ ├── models │ │ ├── contributor.dart │ │ ├── playitem.dart │ │ ├── playitem.g.dart │ │ ├── playlist_item.dart │ │ ├── playlist_item.g.dart │ │ ├── settings.dart │ │ └── settings.g.dart │ ├── player_ex.dart │ └── utils │ │ ├── l10n_utils.dart │ │ ├── media_utils.dart │ │ ├── route_utils.dart │ │ ├── sliver_utils.dart │ │ ├── string_utils.dart │ │ ├── theme_utils.dart │ │ └── time_utils.dart ├── main.dart ├── pages │ ├── file │ │ ├── file_card.dart │ │ ├── file_explorer.dart │ │ └── folder_listtile.dart │ ├── home.dart │ ├── home │ │ └── titlebar.dart │ ├── library │ │ ├── common_media_menu.dart │ │ ├── library_page.dart │ │ └── media_menu.dart │ ├── media │ │ ├── player_menu.dart │ │ ├── player_page.dart │ │ └── seekbar_builder.dart │ ├── playlist │ │ ├── common_playlist_menu.dart │ │ ├── playlist_card.dart │ │ ├── playlist_detail.dart │ │ ├── playlist_listtile.dart │ │ ├── playlist_loader.dart │ │ ├── playlist_menu.dart │ │ └── playlist_page.dart │ └── settings │ │ ├── categories │ │ ├── about_app_settings.dart │ │ ├── appearance_settings.dart │ │ ├── developer_settings.dart │ │ ├── keymap_settings.dart │ │ ├── language_settings.dart │ │ ├── player_settings.dart │ │ ├── storage_settings.dart │ │ └── whisper_settings.dart │ │ └── settings_page.dart └── widgets │ ├── animated_cross_slide.dart │ ├── basic_video.dart │ ├── cover.dart │ ├── cover_card.dart │ ├── cover_listtile.dart │ ├── empty_holder.dart │ ├── error_holder.dart │ ├── folding_holder.dart │ ├── icon_switch_listtile.dart │ ├── image.dart │ ├── interactive_wrapper.dart │ ├── library │ ├── library_header.dart │ ├── library_header_old.dart │ ├── library_listtile.dart │ ├── library_title.dart │ └── library_title_old.dart │ ├── loading_holder.dart │ ├── menu │ ├── menu_button.dart │ └── menu_item.dart │ ├── path_setting_card.dart │ ├── player_list.dart │ ├── playlist_picker.dart │ ├── readme.md │ └── settings_message_box.dart ├── linux ├── .gitignore ├── CMakeLists.txt ├── flutter │ ├── CMakeLists.txt │ ├── generated_plugin_registrant.cc │ ├── generated_plugin_registrant.h │ └── generated_plugins.cmake ├── main.cc ├── my_application.cc └── my_application.h ├── macos ├── .gitignore ├── Flutter │ ├── Flutter-Debug.xcconfig │ ├── Flutter-Release.xcconfig │ └── GeneratedPluginRegistrant.swift ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── app_icon_1024.png │ │ │ ├── app_icon_128.png │ │ │ ├── app_icon_16.png │ │ │ ├── app_icon_256.png │ │ │ ├── app_icon_32.png │ │ │ ├── app_icon_512.png │ │ │ └── app_icon_64.png │ ├── Base.lproj │ │ └── MainMenu.xib │ ├── Configs │ │ ├── AppInfo.xcconfig │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── Warnings.xcconfig │ ├── DebugProfile.entitlements │ ├── Info.plist │ ├── MainFlutterWindow.swift │ └── Release.entitlements └── RunnerTests │ └── RunnerTests.swift ├── pubspec.lock ├── pubspec.yaml ├── res ├── contributors │ ├── KernelInterrupt.jpg │ ├── rubbrt.jpg │ └── yui.jpg └── images │ ├── icon512.png │ ├── icon_bg.png │ ├── icon_fg.png │ └── icon_mono.png ├── screenshots ├── screenshot1.png ├── screenshot2.png ├── screenshot3.png └── screenshot4.png ├── versions.json └── windows ├── .gitignore ├── CMakeLists.txt ├── flutter ├── CMakeLists.txt ├── generated_plugin_registrant.cc ├── generated_plugin_registrant.h └── generated_plugins.cmake └── runner ├── CMakeLists.txt ├── Runner.rc ├── flutter_window.cpp ├── flutter_window.h ├── main.cpp ├── resource.h ├── resources └── app_icon.ico ├── runner.exe.manifest ├── utils.cpp ├── utils.h ├── win32_window.cpp └── win32_window.h /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build-windows: 8 | runs-on: windows-latest 9 | steps: 10 | - name: Checkout repository 11 | uses: actions/checkout@v4 12 | 13 | - name: Install Flutter 14 | uses: subosito/flutter-action@v2 15 | with: 16 | flutter-version: '3.29.0' 17 | 18 | - name: Install dependencies 19 | run: flutter pub get 20 | 21 | - name: Set up libmpv_dart 22 | run: dart run libmpv_dart:setup --platform windows 23 | 24 | - name: Set up whisper4dart (using prebuilt library) 25 | run: dart run whisper4dart:setup --prebuilt 26 | 27 | - name: Build Windows App 28 | run: flutter build windows 29 | 30 | - name: Upload Windows Build Artifacts 31 | uses: actions/upload-artifact@v4 32 | with: 33 | name: windows-build 34 | path: build/windows/x64/runner/Release/ 35 | 36 | build-linux: 37 | runs-on: ubuntu-latest 38 | steps: 39 | - name: Checkout repository 40 | uses: actions/checkout@v4 41 | 42 | - name: Install Flutter 43 | uses: subosito/flutter-action@v2 44 | with: 45 | flutter-version: '3.29.0' 46 | 47 | - name: Install dependencies 48 | run: flutter pub get 49 | 50 | - name: Set up whisper4dart (using prebuilt library) 51 | run: dart run whisper4dart:setup --prebuilt 52 | 53 | - name: Install necessary libraries 54 | run: | 55 | flutter doctor 56 | sudo apt-get update -y 57 | sudo apt-get install -y ninja-build libgtk-3-dev libmpv-dev 58 | flutter doctor 59 | 60 | - name: Build Linux App 61 | run: flutter build linux 62 | 63 | # - name: Upload Linux Build Artifacts 64 | # uses: actions/upload-artifact@v4 65 | # with: 66 | # name: linux-build 67 | # path: build/linux/ 68 | 69 | # build-macos: 70 | # runs-on: macos-latest 71 | # steps: 72 | # - name: Checkout repository 73 | # uses: actions/checkout@v4 74 | 75 | # - name: Install Flutter 76 | # uses: subosito/flutter-action@v2 77 | # with: 78 | # flutter-version: '3.29.0' 79 | 80 | # - name: Install dependencies 81 | # run: flutter pub get 82 | 83 | # - name: Set up Flutter for macOS 84 | # run: flutter config --enable-macos-desktop 85 | 86 | # - name: Build macOS App 87 | # run: flutter build macos 88 | 89 | # - name: Upload macOS Build Artifacts 90 | # uses: actions/upload-artifact@v4 91 | # with: 92 | # name: macos-build 93 | # path: build/macos/Build/Products/Release/ 94 | 95 | # build-android: 96 | # runs-on: ubuntu-latest 97 | # steps: 98 | # - name: Checkout repository 99 | # uses: actions/checkout@v4 100 | 101 | # - name: Install Flutter 102 | # uses: subosito/flutter-action@v2 103 | # with: 104 | # flutter-version: '3.29.0' 105 | 106 | # - name: Install dependencies 107 | # run: flutter pub get 108 | 109 | # - name: Build Android App 110 | # run: flutter build apk 111 | 112 | # - name: Upload Android Build Artifacts 113 | # uses: actions/upload-artifact@v4 114 | # with: 115 | # name: release-apk 116 | # path: build/app/outputs/apk/release/app-release.apk -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .build/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | .swiftpm/ 13 | migrate_working_dir/ 14 | 15 | # IntelliJ related 16 | *.iml 17 | *.ipr 18 | *.iws 19 | .idea/ 20 | 21 | # The .vscode folder contains launch configuration and tasks you configure in 22 | # VS Code which you may wish to be included in version control, so this line 23 | # is commented out by default. 24 | #.vscode/ 25 | 26 | # Flutter/Dart/Pub related 27 | **/doc/api/ 28 | **/ios/Flutter/.last_build_id 29 | .dart_tool/ 30 | .flutter-plugins 31 | .flutter-plugins-dependencies 32 | .pub-cache/ 33 | .pub/ 34 | /build/ 35 | 36 | # Symbolication related 37 | app.*.symbols 38 | 39 | # Obfuscation related 40 | app.*.map.json 41 | 42 | # Android Studio will place build artifacts here 43 | /android/app/debug 44 | /android/app/profile 45 | /android/app/release 46 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 17 | base_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 18 | - platform: android 19 | create_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 20 | base_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 21 | - platform: ios 22 | create_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 23 | base_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 24 | - platform: linux 25 | create_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 26 | base_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 27 | - platform: macos 28 | create_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 29 | base_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 30 | - platform: web 31 | create_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 32 | base_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 33 | - platform: windows 34 | create_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 35 | base_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | } -------------------------------------------------------------------------------- /Privacy Statement.md: -------------------------------------------------------------------------------- 1 | ### Privacy Statement 2 | 3 | We take your privacy seriously. This Privacy Statement outlines how we collect, use, and protect your personal data while interacting with our open-source software project. 4 | 5 | #### 1. Data Collection 6 | We do not collect any personal data from users of the software by default. However, if the software integrates with third-party services, these services may collect data as outlined in their respective privacy policies. 7 | 8 | #### 2. Usage of Data 9 | We do not store or use any personal data for any purposes beyond the functioning of the software itself. Any data collected through third-party services is subject to those services' privacy practices. 10 | 11 | #### 3. Third-Party Services 12 | Our open-source project may integrate with third-party services for additional functionality (such as analytics, crash reporting, or user authentication). When interacting with these services, data may be shared according to their privacy policies. 13 | 14 | #### 4. Data Security 15 | We prioritize the security of your data. Any data collected by third-party services is processed according to their security protocols. Our open-source project does not store or process personal data outside of necessary operations. 16 | 17 | #### 5. Changes to This Privacy Statement 18 | We may update this Privacy Statement as necessary to reflect changes in the software or our privacy practices. The latest version of the Privacy Statement will be available in the repository. 19 | 20 | #### 6. Contact Us 21 | If you have any questions or concerns about your privacy in relation to this project, please feel free to reach out to us at yuihrsw@outlook.com. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Playboy 2 | 3 | 中文 | [English](./README_en.md) 4 | 5 | 基于 `libmpv` 的 Material 3 风格跨平台媒体播放器. 6 | 7 | [![build](https://img.shields.io/github/actions/workflow/status/Playboy-Player/Playboy/build.yml?style=for-the-badge)](https://github.com/Playboy-Player/Playboy/actions) 8 | [![release](https://img.shields.io/badge/beta-2025.4-gold?style=for-the-badge)](https://github.com/Playboy-Player/Playboy/releases) ![downloads](https://img.shields.io/github/downloads/Playboy-Player/Playboy/total?style=for-the-badge&color=blue) [![project](https://img.shields.io/badge/project-grey?style=for-the-badge)](https://github.com/orgs/Playboy-Player/projects/3) 9 | 10 | ![](https://m3-markdown-badges.vercel.app/stars/7/2/Playboy-Player/Playboy) 11 | ![](https://m3-markdown-badges.vercel.app/issues/1/2/Playboy-Player/Playboy) 12 | ![](https://ziadoua.github.io/m3-Markdown-Badges/badges/Windows/windows3.svg) 13 | ![](https://ziadoua.github.io/m3-Markdown-Badges/badges/Linux/linux3.svg) 14 | ![](https://ziadoua.github.io/m3-Markdown-Badges/badges/macOS/macos3.svg) 15 | ![](https://ziadoua.github.io/m3-Markdown-Badges/badges/Android/android3.svg) 16 | 17 | ## 界面截图 18 | 19 | 20 | 21 | 24 | 27 | 28 | 29 | 32 | 35 | 36 |
22 | equalizer 23 | 25 | theme 26 |
30 | shaders 31 | 33 | library 34 |
37 | 38 | ## 功能 39 | 40 | > 可以通过[键盘快捷键](https://github.com/mpv-player/random-stuff/blob/master/key_bindings_chart/mpbindings.png)访问所有 mpv 功能, 在播放界面按 `SHIFT+O` 可显示 mpv OSD 41 | 42 | - [X] 自定义主题 & 深色模式 43 | - [X] 播放本地和网络媒体 44 | - [X] 迷你播放器模式 (Windows & macOS) 45 | - [X] 设置为系统打开方式 (Windows) 46 | - [X] 播放列表功能 (随机播放, 单曲循环) 47 | - [X] 章节和 AB 循环 (命令行) 48 | - [X] 任意倍速调节 49 | - [X] 搜索媒体文件和播放列表 50 | - [X] 多语言支持 51 | - [X] 字幕 (libass) 52 | - [X] 着色器支持 53 | - [X] 自定义快捷键映射 (input.conf 支持) 54 | - [X] 兼容 mpv.conf 配置文件 55 | - [X] 自动生成字幕 (Whisper) 56 | - [ ] LLM增强的视频分析(部分功能未完成,可以切换到llm分支提前体验一部分功能) 57 | 58 | ### 使用 Anime4K 着色器 59 | 60 | 参考 [Anime4K](https://github.com/bloc97/Anime4K) 官方的 GLSL/MPV 安装教程, 下载 template files. 61 | 62 | **顶部菜单 -> 应用偏好设置 -> 存储 -> 打开应用数据文件夹**, 把 `mpv.conf`, `input.conf`, `shaders 文件夹` 复制到数据文件夹下. 63 | 64 | 启用 **应用偏好设置 -> 播放器 -> 允许 libmpv 使用配置文件** 选项, 重启应用. 65 | 66 | ## For Developers 67 | 68 | 首先, 根据 [官方教程](https://docs.flutter.dev/get-started/install/) 配置 flutter 环境. 请使用不低于 **3.29.0** 的 flutter 版本. 69 | 70 | 终端进入项目根目录, 运行 `flutter pub get` 以获取依赖项. 71 | 运行 `dart run whisper4dart:setup --prebuilt` 72 | 73 | ### Windows 74 | 75 | 运行 `dart run libmpv_dart:setup --platform windows` 获取 mpv 库依赖 76 | 运行 `flutter build windows` 以生成 Windows 可执行程序 77 | 78 | ### Linux 79 | 80 | 配置完 flutter 后, 请通过系统包管理器或其他途径安装 `libmpv-dev`. 81 | 82 | 运行 `flutter build linux` 以生成 Linux 可执行程序 83 | 84 | ### macOS 85 | 86 | 运行 `flutter build macos` 以生成 macOS 可执行程序 87 | 88 | ### android 89 | 90 | > 请在平板设备上运行. 91 | 92 | 运行 `flutter build apk` 以生成 apk 安装包文件 93 | 94 | ## 为本项目做出贡献 95 | 96 | 如果您在使用中发现 bug 或者希望添加某些功能, 请 [新建一个 issue](https://github.com/Playboy-Player/Playboy/issues/new). 97 | 也欢迎直接 Pull Request 提交代码贡献. 98 | 99 | ## Star History 100 | 101 | [![Star History Chart](https://api.star-history.com/svg?repos=Playboy-Player/Playboy&type=Date)](https://star-history.com/#Playboy-Player/Playboy&Date) 102 | -------------------------------------------------------------------------------- /README_en.md: -------------------------------------------------------------------------------- 1 | # Playboy 2 | 3 | [中文](./README.md) | English 4 | 5 | A `libmpv` based media player with Material 3 design. 6 | 7 | [![build](https://img.shields.io/github/actions/workflow/status/Playboy-Player/Playboy/build.yml?style=for-the-badge)](https://github.com/Playboy-Player/Playboy/actions) 8 | [![release](https://img.shields.io/badge/beta-2025.4-gold?style=for-the-badge)](https://github.com/Playboy-Player/Playboy/releases) ![downloads](https://img.shields.io/github/downloads/Playboy-Player/Playboy/total?style=for-the-badge&color=blue) [![project](https://img.shields.io/badge/project-grey?style=for-the-badge)](https://github.com/orgs/Playboy-Player/projects/3) 9 | 10 | ![](https://m3-markdown-badges.vercel.app/stars/7/2/Playboy-Player/Playboy) 11 | ![](https://m3-markdown-badges.vercel.app/issues/1/2/Playboy-Player/Playboy) 12 | ![](https://ziadoua.github.io/m3-Markdown-Badges/badges/Windows/windows3.svg) 13 | ![](https://ziadoua.github.io/m3-Markdown-Badges/badges/Linux/linux3.svg) 14 | ![](https://ziadoua.github.io/m3-Markdown-Badges/badges/macOS/macos3.svg) 15 | ![](https://ziadoua.github.io/m3-Markdown-Badges/badges/Android/android3.svg) 16 | 17 | ## Screenshots 18 | 19 | 20 | 21 | 24 | 27 | 28 | 29 | 32 | 35 | 36 |
22 | equalizer 23 | 25 | theme 26 |
30 | shaders 31 | 33 | library 34 |
37 | 38 | ## Features 39 | 40 | You can access all mpv functions using [keyboard shortcuts](https://github.com/mpv-player/random-stuff/blob/master/key_bindings_chart/mpbindings.png). Press `SHIFT+O` while playing to display the mpv OSD interface. 41 | 42 | - [X] Custom themes & dark mode 43 | - [X] Play local and online media 44 | - [X] Mini player mode (Windows & macOS) 45 | - [X] Set as system default media player (Windows) 46 | - [X] Playlist support (shuffle, repeat one) 47 | - [X] Chapters and AB loop (via command line) 48 | - [X] Adjustable playback speed 49 | - [X] Media file and playlist search 50 | - [X] Multi-language support 51 | - [X] Subtitles (libass) 52 | - [X] Shader support, such as [Anime4K](https://github.com/bloc97/Anime4K) 53 | - [X] Custom key mapping (input.conf support) 54 | - [X] Compatible with `mpv.conf` configuration files 55 | - [X] Subtitle generation using Whisper 56 | - [ ] LLM-enhanced video analysis (Switch to llm branch to have early access to some new features) 57 | 58 | ## For Developers 59 | 60 | First, set up the Flutter environment according to the [official guide](https://docs.flutter.dev/get-started/install/). Please use Flutter version **3.29.0** or higher. 61 | 62 | Then run `flutter pub get` and `dart run whisper4dart:setup --prebuilt` to get necessary dependencies. 63 | 64 | ### Windows 65 | 66 | Before building the application, run `libmpv_dart:setup --platform windows` to get libmpv dependencies. 67 | 68 | Run `flutter build windows` in the project directory to generate the Windows executable. 69 | 70 | ### Linux 71 | 72 | After setting up Flutter, install `libmpv-dev` via your system package manager or other means. 73 | 74 | Run `flutter build linux` in the project directory to generate the Linux executable. 75 | 76 | ### macOS 77 | 78 | Run `flutter build macos` in the project directory to generate the macOS executable. 79 | 80 | ### Android 81 | 82 | > Please run on tablet devices. 83 | 84 | Run `flutter build apk` to generate the APK installation file. 85 | 86 | ## Contributing to This Project 87 | 88 | If you find a bug or want to suggest a feature, please [create a new issue](https://github.com/Playboy-Player/Playboy/issues/new). 89 | Pull requests with code contributions are also welcome. 90 | 91 | ## Star History 92 | 93 | [![Star History Chart](https://api.star-history.com/svg?repos=Playboy-Player/Playboy&type=Date)](https://star-history.com/#Playboy-Player/Playboy&Date) 94 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "dev.flutter.flutter-gradle-plugin" 5 | } 6 | 7 | def localProperties = new Properties() 8 | def localPropertiesFile = rootProject.file('local.properties') 9 | if (localPropertiesFile.exists()) { 10 | localPropertiesFile.withReader('UTF-8') { reader -> 11 | localProperties.load(reader) 12 | } 13 | } 14 | 15 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 16 | if (flutterVersionCode == null) { 17 | flutterVersionCode = '1' 18 | } 19 | 20 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 21 | if (flutterVersionName == null) { 22 | flutterVersionName = '1.0' 23 | } 24 | 25 | android { 26 | namespace "com.example.playboy" 27 | compileSdkVersion flutter.compileSdkVersion 28 | ndkVersion flutter.ndkVersion 29 | 30 | compileOptions { 31 | sourceCompatibility JavaVersion.VERSION_1_8 32 | targetCompatibility JavaVersion.VERSION_1_8 33 | } 34 | 35 | kotlinOptions { 36 | jvmTarget = '1.8' 37 | } 38 | 39 | sourceSets { 40 | main.java.srcDirs += 'src/main/kotlin' 41 | } 42 | 43 | defaultConfig { 44 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 45 | applicationId "com.example.playboy" 46 | // You can update the following values to match your application needs. 47 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. 48 | minSdkVersion flutter.minSdkVersion 49 | targetSdkVersion flutter.targetSdkVersion 50 | versionCode flutterVersionCode.toInteger() 51 | versionName flutterVersionName 52 | } 53 | 54 | buildTypes { 55 | release { 56 | // TODO: Add your own signing config for the release build. 57 | // Signing with the debug keys for now, so `flutter run --release` works. 58 | signingConfig signingConfigs.debug 59 | } 60 | } 61 | } 62 | 63 | flutter { 64 | source '../..' 65 | } 66 | 67 | dependencies {} 68 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 14 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 29 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/playboy/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.playboy 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/android/app/src/main/res/drawable-hdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/android/app/src/main/res/drawable-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/android/app/src/main/res/drawable-mdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/android/app/src/main/res/drawable-xhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/android/app/src/main/res/drawable-xxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.7.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 10 | } 11 | } 12 | 13 | allprojects { 14 | repositories { 15 | google() 16 | mavenCentral() 17 | } 18 | } 19 | 20 | rootProject.buildDir = '../build' 21 | subprojects { 22 | project.buildDir = "${rootProject.buildDir}/${project.name}" 23 | } 24 | subprojects { 25 | project.evaluationDependsOn(':app') 26 | } 27 | 28 | tasks.register("clean", Delete) { 29 | delete rootProject.buildDir 30 | } 31 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | android.defaults.buildfeatures.buildconfig=true 5 | android.nonTransitiveRClass=false 6 | android.nonFinalResIds=false 7 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Feb 01 02:04:52 CST 2025 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | } 9 | settings.ext.flutterSdkPath = flutterSdkPath() 10 | 11 | includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") 12 | 13 | repositories { 14 | google() 15 | mavenCentral() 16 | gradlePluginPortal() 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false 21 | } 22 | } 23 | 24 | plugins { 25 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 26 | id "com.android.application" version '8.1.0' apply false 27 | } 28 | 29 | include ":app" 30 | -------------------------------------------------------------------------------- /devtools_options.yaml: -------------------------------------------------------------------------------- 1 | extensions: 2 | -------------------------------------------------------------------------------- /l10n/en_us.json: -------------------------------------------------------------------------------- 1 | { 2 | "播放列表": "Playlist", 3 | "媒体库": "Library", 4 | "文件": "File", 5 | "搜索": "Search", 6 | "设置": "Settings", 7 | "外观": "Appearance", 8 | "播放器": "Player", 9 | "快捷键": "Keymap", 10 | "存储": "Storage", 11 | "语言": "Language", 12 | "关于": "About", 13 | "调试": "Developer", 14 | "字体": "Font", 15 | "显示字体": "Display Font", 16 | "留空以保持默认": "Leave empty to set default", 17 | "启动页面": "Startup Page", 18 | "默认播放列表视图": "Default Playlist Page View Mode", 19 | "默认媒体库视图": "Default Media Library View Mode", 20 | "主题": "Theme", 21 | "主题颜色": "Theme Color", 22 | "使用系统主题": "Follow System Theme", 23 | "深色模式": "Dark Mode", 24 | "自动开始播放": "Auto Play", 25 | "默认打开音乐视图": "Default Music View", 26 | "记忆播放器状态": "Remember Player Status", 27 | "音量和倍速": "Volume, Speed,...", 28 | "退出播放页面后继续播放": "Continue Play outside Player Page", 29 | "MPV 参数": "MPV args", 30 | "字幕": "Subtitle", 31 | "扫描选项": "Scan Options", 32 | "重新扫描媒体库": "Rescan Library", 33 | "扫描位置": "Scan Locations", 34 | "添加": "ADD", 35 | "文件夹": "Folder", 36 | "收藏夹": "Favourite", 37 | "截图保存位置": "Screenshot Location", 38 | "应用数据": "App Data", 39 | "打开应用数据文件夹": "Open App Data Folder", 40 | "恢复默认设置": "Restore Default Settings", 41 | "重启应用后生效": "Restart app to apply", 42 | "显示语言": "Display Language", 43 | "帮助": "Support", 44 | "项目主页": "Project Website", 45 | "反馈": "Feedback", 46 | "新建播放列表": "New Playlist", 47 | "切换显示视图": "Switch View Mode", 48 | "播放": "Play", 49 | "随机播放": "Shuffle", 50 | "追加到播放列表": "Append to Playlist", 51 | "添加到播放列表": "Add to Playlist", 52 | "导出": "Export", 53 | "修改封面": "Change Cover", 54 | "清除封面": "Clear Cover", 55 | "重命名": "Rename", 56 | "删除": "Delete", 57 | "属性": "Property", 58 | "播放器播放": "Player", 59 | "插播": "Play Next", 60 | "最后播放": "Play Last", 61 | "隐藏": "Hide", 62 | "切换显示样式": "Switch Library Style", 63 | "播放文件夹": "Play folder", 64 | "播放本地文件": "Play file", 65 | "浏览文件夹": "Browse folder", 66 | "播放网络串流": "Play stream", 67 | "网络设备": "LAN Devices", 68 | "最近播放": "Recent Played", 69 | "搜索播放列表, 媒体文件...": "Search playlist, media...", 70 | "按回车以确认": "Press ENTER to confirm", 71 | "最近搜索": "Recent Searched", 72 | "无内容": "Empty", 73 | "已折叠": "Folded", 74 | "取消": "Cancel", 75 | "确定": "OK", 76 | "截图": "Screenshot", 77 | "将当前画面设为封面": "Screenshot as cover", 78 | "从播放列表移除": "Remove from list", 79 | "设置播放速度": "Set Playback Speed", 80 | "生成字幕": "Generate Subtitle" 81 | } -------------------------------------------------------------------------------- /l10n/manifest.json: -------------------------------------------------------------------------------- 1 | [ 2 | "en_us" 3 | ] -------------------------------------------------------------------------------- /lib/backend/actions.dart: -------------------------------------------------------------------------------- 1 | const refresh = 'refresh'; 2 | const togglePlayer = 'togglePlayer'; 3 | const playlistView = 'playlistView'; 4 | const libraryView = 'libraryView'; 5 | const fileView = 'fileView'; 6 | const gotoDirectory = 'gotoDirectory'; 7 | const toggleFullscreen = 'toggleFullscreen'; 8 | const toggleDarkMode = 'toggleDarkMode'; 9 | -------------------------------------------------------------------------------- /lib/backend/app.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'dart:math'; 4 | 5 | import 'package:flutter/material.dart'; 6 | import 'package:media_kit/media_kit.dart'; 7 | import 'package:media_kit_video/basic/video_controller.dart'; 8 | import 'package:path/path.dart'; 9 | import 'package:path_provider/path_provider.dart'; 10 | 11 | import 'package:playboy/backend/keymap_helper.dart'; 12 | import 'package:playboy/backend/library_helper.dart'; 13 | import 'package:playboy/backend/models/playitem.dart'; 14 | import 'package:playboy/backend/models/playlist_item.dart'; 15 | import 'package:playboy/backend/models/settings.dart'; 16 | import 'package:playboy/pages/home.dart'; 17 | 18 | class App { 19 | late final String dataPath; 20 | 21 | late AppSettings settings; 22 | 23 | final contentKey = GlobalKey(); 24 | void dialog(Widget Function(BuildContext) builder) { 25 | if (contentKey.currentState != null) { 26 | KeyMapHelper.keyBindinglock++; 27 | showDialog( 28 | useRootNavigator: false, 29 | context: contentKey.currentState!.context, 30 | builder: builder, 31 | ).whenComplete(() { 32 | KeyMapHelper.keyBindinglock--; 33 | }); 34 | } 35 | } 36 | 37 | Map actions = {}; 38 | 39 | late final NativePlayer player; 40 | late final BasicVideoController controller; 41 | 42 | bool playlistLoaded = false; 43 | bool mediaLibraryLoaded = false; 44 | List playlists = []; 45 | List mediaLibrary = []; 46 | static final App _instance = App._internal(); 47 | factory App() => _instance; 48 | App._internal() { 49 | settings = AppSettings(); 50 | } 51 | 52 | Future init() async { 53 | dataPath = (await getApplicationSupportDirectory()).path; 54 | await loadSettings(); 55 | bool needsUpdate = false; 56 | if (settings.screenshotPath == '') { 57 | settings.screenshotPath = '$dataPath/screenshots'; 58 | var dir = Directory(settings.screenshotPath); 59 | if (!await dir.exists()) { 60 | dir.create(); 61 | } 62 | needsUpdate = true; 63 | } 64 | if (needsUpdate) { 65 | await saveSettings(); 66 | } 67 | player = NativePlayer( 68 | options: { 69 | 'config-dir': 70 | settings.mpvConfigPath != '' ? settings.mpvConfigPath : dataPath, 71 | 'config': settings.enableMpvConfig ? 'yes' : 'no', 72 | 'input-default-bindings': settings.useDefaultKeyBinding ? 'yes' : 'no', 73 | 'osd-level': settings.mpvOsdLevel.toString(), 74 | }, 75 | ); 76 | player.stream.playlist.listen( 77 | (event) { 78 | if (event.medias.isNotEmpty) { 79 | var src = event.medias[event.index].uri; 80 | playingTitle = basenameWithoutExtension(src); 81 | playingCover = '${withoutExtension(src)}.cover.jpg'; 82 | } else { 83 | playingTitle = 'Not Playing'; 84 | playingCover = null; 85 | } 86 | }, 87 | ); 88 | controller = await BasicVideoController.create(player); 89 | player.setVolume(settings.volume); 90 | settings.tempPath = (await getTemporaryDirectory()).path; 91 | } 92 | 93 | Future loadSettings() async { 94 | var settingsPath = "$dataPath/config/settings.json"; 95 | var fp = File(settingsPath); 96 | if (!await fp.exists()) { 97 | await fp.create(recursive: true); 98 | var data = AppSettings().toJson(); 99 | var str = jsonEncode(data); 100 | await fp.writeAsString(str); 101 | } 102 | settings = AppSettings.fromJson(jsonDecode(await fp.readAsString())); 103 | } 104 | 105 | Future saveSettings() async { 106 | var settingsPath = "$dataPath/config/settings.json"; 107 | var fp = File(settingsPath); 108 | var data = settings.toJson(); 109 | var str = jsonEncode(data); 110 | await fp.writeAsString(str); 111 | } 112 | 113 | void executeAction(String action) { 114 | actions[action]?.call(); 115 | } 116 | 117 | void updateStatus() { 118 | HomePage.refresh?.call(); 119 | } 120 | 121 | String? playingCover; 122 | String playingTitle = 'Not Playing'; 123 | 124 | bool loop = false; 125 | 126 | bool seeking = false; 127 | double seekingPos = 0; 128 | 129 | int voWidth = 0; 130 | int voHeight = 0; 131 | void refreshVO() { 132 | var voInfo = player.state.videoParams; 133 | int w = voInfo.dw ?? 1, h = voInfo.dh ?? 1; 134 | double fac = min(voWidth / w, voHeight / h); 135 | controller.setSize(width: (w * fac) ~/ 1, height: (h * fac) ~/ 1); 136 | player.command(['show-text', '已更新显示区域']); 137 | } 138 | 139 | void restoreVO() { 140 | controller.setSize(); 141 | player.command(['show-text', '已恢复默认显示大小']); 142 | } 143 | 144 | void openMedia(PlayItem media) { 145 | if (!settings.rememberStatus) { 146 | _resetPlayerStatus(); 147 | } 148 | final video = Media(media.source); 149 | player.open(video, play: settings.autoPlay); 150 | } 151 | 152 | void openPlaylist(PlaylistItem playlistItem, bool shuffleList) { 153 | if (playlistItem.items.isEmpty) { 154 | return; 155 | } 156 | if (!settings.rememberStatus) { 157 | _resetPlayerStatus(); 158 | } 159 | if (shuffleList) { 160 | player.open(LibraryHelper.convertToPlaylist(playlistItem), play: false); 161 | player.setShuffle(true); 162 | player.jump(0); 163 | if (App().settings.autoPlay) player.play(); 164 | } else { 165 | player.open( 166 | LibraryHelper.convertToPlaylist(playlistItem), 167 | play: App().settings.autoPlay, 168 | ); 169 | } 170 | } 171 | 172 | void _resetPlayerStatus() { 173 | player.setVolume(settings.defaultVolume); 174 | player.setRate(settings.defaultSpeed); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /lib/backend/constants.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:playboy/backend/models/contributor.dart'; 3 | 4 | const IconData appIcon = Icons.play_circle_outline; 5 | const String appName = 'Playboy'; 6 | const String version = 'Beta 2025.4'; 7 | const String flutterVersion = '3.29.0'; 8 | 9 | List contributors = [ 10 | Contributor( 11 | avatar: 'res/contributors/yui.jpg', 12 | name: 'YuiHrsw', 13 | url: 'https://github.com/YuiHrsw', 14 | ), 15 | Contributor( 16 | avatar: 'res/contributors/KernelInterrupt.jpg', 17 | name: 'KernelInterrupt', 18 | url: 'https://github.com/KernelInterrupt', 19 | ), 20 | Contributor( 21 | avatar: 'res/contributors/rubbrt.jpg', 22 | name: 'rubbrt', 23 | url: 'https://github.com/rubbrt', 24 | ) 25 | ]; 26 | -------------------------------------------------------------------------------- /lib/backend/library_helper.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | import 'dart:convert'; 3 | import 'dart:io'; 4 | 5 | import 'package:playboy/backend/models/playitem.dart'; 6 | import 'package:playboy/backend/models/playlist_item.dart'; 7 | import 'package:playboy/backend/app.dart'; 8 | 9 | import 'package:path/path.dart'; 10 | import 'package:media_kit/media_kit.dart'; 11 | 12 | class LibraryHelper { 13 | static const supportFormats = [ 14 | '.avi', 15 | '.flv', 16 | '.mkv', 17 | '.mov', 18 | '.mp4', 19 | '.mpeg', 20 | '.webm', 21 | '.wmv', 22 | '.aac', 23 | '.midi', 24 | '.mp3', 25 | '.ogg', 26 | '.wav', 27 | ]; 28 | 29 | static Playlist convertToPlaylist(PlaylistItem playlistItem) { 30 | List res = []; 31 | for (var item in playlistItem.items) { 32 | res.add(Media(item.source)); 33 | } 34 | return Playlist(res); 35 | } 36 | 37 | static Future> getMediaFromPaths(List paths) async { 38 | List res = []; 39 | var vis = {}; 40 | 41 | Queue q = Queue(); 42 | for (var path in paths) { 43 | if (vis.contains(path)) continue; 44 | vis.add(path); 45 | q.add(path); 46 | } 47 | 48 | vis.clear(); 49 | while (q.isNotEmpty) { 50 | int n = q.length; 51 | for (int i = 0; i < n; i++) { 52 | var p = q.removeFirst(); 53 | if (vis.contains(p)) continue; 54 | vis.add(p); 55 | var dir = Directory(p); 56 | await for (var item in dir.list()) { 57 | if (item is Directory) { 58 | if (vis.contains(item.path)) continue; 59 | q.add(item.path); 60 | } else if (supportFormats.contains(extension(item.path))) { 61 | res.add( 62 | PlayItem( 63 | source: item.path, 64 | title: basenameWithoutExtension(item.path), 65 | ), 66 | ); 67 | } 68 | } 69 | } 70 | } 71 | 72 | return res; 73 | } 74 | 75 | static Future getItemFromFile(String src) async { 76 | return PlayItem( 77 | source: src, 78 | title: basenameWithoutExtension(src), 79 | ); 80 | } 81 | 82 | static Future> loadPlaylists() async { 83 | var playlists = []; 84 | var dir = Directory('${App().dataPath}/playlists/'); 85 | if (!dir.existsSync()) { 86 | dir.createSync(recursive: true); 87 | } 88 | await for (var item in dir.list()) { 89 | if (item is File && extension(item.path) == '.json') { 90 | var pl = PlaylistItem.fromJson(jsonDecode(await item.readAsString())); 91 | playlists.add(pl); 92 | } 93 | } 94 | return playlists; 95 | } 96 | 97 | static void savePlaylist(PlaylistItem pl) { 98 | var fp = File('${App().dataPath}/playlists/${pl.title}.json'); 99 | if (!fp.existsSync()) { 100 | fp.createSync(recursive: true); 101 | } 102 | var data = pl.toJson(); 103 | fp.writeAsString(jsonEncode(data)); 104 | } 105 | 106 | static void deletePlaylist(PlaylistItem pl) { 107 | var fp = File('${App().dataPath}/playlists/${pl.title}.json'); 108 | if (fp.existsSync()) { 109 | fp.deleteSync(); 110 | } 111 | 112 | var cover = File('${App().dataPath}/playlists/${pl.title}.cover.jpg'); 113 | if (cover.existsSync()) { 114 | cover.deleteSync(); 115 | } 116 | } 117 | 118 | static void addItemToPlaylist(PlaylistItem pl, PlayItem p) { 119 | pl.items.add(p); 120 | savePlaylist(pl); 121 | } 122 | 123 | static void removeItemFromPlaylist(PlaylistItem pl, PlayItem p) { 124 | pl.items.remove(p); 125 | savePlaylist(pl); 126 | } 127 | 128 | static void renamePlaylist(PlaylistItem pl, String name) { 129 | pl.title = name; 130 | savePlaylist(pl); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /lib/backend/ml/subtitle_generator.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:path/path.dart' as path; 5 | 6 | import 'package:playboy/backend/app.dart'; 7 | import 'package:whisper4dart/whisper4dart.dart' as whisper; 8 | 9 | class SubtitleGenerator { 10 | bool initializing = false; 11 | late String modelnameString; 12 | late String modelDirectory; 13 | bool modelExists = false; 14 | late File modelFile; 15 | final String modelType; 16 | 17 | SubtitleGenerator(this.modelType) { 18 | init(); 19 | } 20 | 21 | void init() { 22 | modelnameString = modelType; 23 | modelDirectory = '${App().dataPath}/models'; 24 | modelFile = File(path.join(modelDirectory, modelnameString)); 25 | Directory(modelDirectory).createSync(recursive: true); 26 | debugPrint("Model path: ${modelFile.path}"); 27 | } 28 | 29 | Future downloadModel({int failCount = 0}) async { 30 | // if (failCount > 3) { 31 | // throw Exception("Failed to download model after 3 retries."); 32 | // } 33 | // var lock = Lock(); 34 | // await lock.synchronized(() async { 35 | // debugPrint("Downloading model..."); 36 | 37 | // var modelUri = Uri.parse( 38 | // "https://hf-mirror.com/ggerganov/whisper.cpp/resolve/main/$modelnameString?download=true"); 39 | 40 | // Directory(modelDirectory).createSync(recursive: true); 41 | // var response = await http.get(modelUri); 42 | // if (response.statusCode == 200) { 43 | // var file = File(path.join(modelDirectory, modelnameString)); 44 | // await file.writeAsBytes(response.bodyBytes); 45 | // debugPrint('File downloaded and saved as $modelnameString'); 46 | // debugPrint("Model downloaded."); 47 | // } else { 48 | // debugPrint("Failed to download model, retrying..."); 49 | // await downloadModel(failCount: failCount + 1); 50 | // } 51 | // }); 52 | } 53 | 54 | void genSubtitle( 55 | String mediaPath, 56 | int currentTime, 57 | ValueNotifier resultNotifier, 58 | ValueNotifier progressNotifier) { 59 | var cparams = whisper.createContextDefaultParams(); 60 | var whisperModel = whisper.Whisper( 61 | modelFile.path, 62 | cparams, 63 | outputMode: "srt", 64 | externalResultNotifier: resultNotifier, 65 | externalProgressNotifier: progressNotifier, 66 | ); 67 | whisperModel.inferIsolate( 68 | mediaPath, 69 | startTime: currentTime, 70 | useOriginalTime: true, 71 | newSegmentCallback: whisperModel.getSegmentCallback, 72 | progressCallback: whisperModel.getProgressCallback, 73 | ); 74 | whisperModel.free(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/backend/ml/whisper_model_list.dart: -------------------------------------------------------------------------------- 1 | class WhisperModel { 2 | WhisperModel(this.name, this.link); 3 | String name; 4 | String link; 5 | } 6 | 7 | List _modelNames = [ 8 | 'ggml-base-q5_1.bin', 9 | 'ggml-base-q8_0.bin', 10 | 'ggml-base.bin', 11 | 'ggml-tiny-q5_1.bin', 12 | 'ggml-tiny-q8_0.bin', 13 | 'ggml-tiny.bin', 14 | 'ggml-small-q5_1.bin', 15 | 'ggml-small-q8_0.bin', 16 | 'ggml-small.bin', 17 | 'ggml-medium-q5_0.bin', 18 | 'ggml-medium-q8_0.bin', 19 | 'ggml-medium.bin', 20 | 'ggml-large-v1.bin', 21 | 'ggml-large-v2-q5_0.bin', 22 | 'ggml-large-v2-q8_0.bin', 23 | 'ggml-large-v2.bin', 24 | 'ggml-large-v3-turbo-q5_0.bin', 25 | 'ggml-large-v3-q5_0.bin', 26 | 'ggml-large-v3-turbo-q8_0.bin', 27 | 'ggml-large-v3-turbo.bin', 28 | 'ggml-large-v3.bin', 29 | ]; 30 | 31 | List models = List.generate( 32 | _modelNames.length, 33 | (index) => WhisperModel( 34 | _modelNames[index], 35 | 'https://huggingface.co/ggerganov/whisper.cpp/resolve/main/${_modelNames[index]}?download=true', 36 | ), 37 | ); 38 | 39 | List modelsMirror = List.generate( 40 | _modelNames.length, 41 | (index) => WhisperModel( 42 | _modelNames[index], 43 | 'https://hf-mirror.com/ggerganov/whisper.cpp/resolve/main/${_modelNames[index]}?download=true', 44 | ), 45 | ); 46 | -------------------------------------------------------------------------------- /lib/backend/models/contributor.dart: -------------------------------------------------------------------------------- 1 | class Contributor { 2 | String avatar; 3 | String name; 4 | String url; 5 | 6 | Contributor({ 7 | required this.avatar, 8 | required this.name, 9 | required this.url, 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /lib/backend/models/playitem.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import 'package:path/path.dart'; 3 | part 'playitem.g.dart'; 4 | 5 | @JsonSerializable() 6 | class PlayItem { 7 | PlayItem({ 8 | required this.source, 9 | required this.title, 10 | }); 11 | String source; 12 | String title; 13 | 14 | factory PlayItem.fromJson(Map json) => 15 | _$PlayItemFromJson(json); 16 | Map toJson() => _$PlayItemToJson(this); 17 | 18 | String get cover => '${withoutExtension(source)}.cover.jpg'; 19 | } 20 | -------------------------------------------------------------------------------- /lib/backend/models/playitem.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'playitem.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | PlayItem _$PlayItemFromJson(Map json) => PlayItem( 10 | source: json['source'] as String, 11 | title: json['title'] as String, 12 | ); 13 | 14 | Map _$PlayItemToJson(PlayItem instance) => { 15 | 'source': instance.source, 16 | 'title': instance.title, 17 | }; 18 | -------------------------------------------------------------------------------- /lib/backend/models/playlist_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:playboy/backend/app.dart'; 2 | import 'package:playboy/backend/models/playitem.dart'; 3 | import 'package:json_annotation/json_annotation.dart'; 4 | part 'playlist_item.g.dart'; 5 | 6 | @JsonSerializable() 7 | class PlaylistItem { 8 | PlaylistItem({ 9 | // required this.uuid, 10 | required this.title, 11 | required this.items, 12 | // required this.cover, 13 | }); 14 | // String uuid; 15 | String title; 16 | List items; 17 | // String? cover; 18 | 19 | String get cover => '${App().dataPath}/playlists/$title.cover.jpg'; 20 | 21 | factory PlaylistItem.fromJson(Map json) => 22 | _$PlaylistItemFromJson(json); 23 | Map toJson() => _$PlaylistItemToJson(this); 24 | } 25 | -------------------------------------------------------------------------------- /lib/backend/models/playlist_item.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'playlist_item.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | PlaylistItem _$PlaylistItemFromJson(Map json) => PlaylistItem( 10 | title: json['title'] as String, 11 | items: (json['items'] as List) 12 | .map((e) => PlayItem.fromJson(e as Map)) 13 | .toList(), 14 | ); 15 | 16 | Map _$PlaylistItemToJson(PlaylistItem instance) => 17 | { 18 | 'title': instance.title, 19 | 'items': instance.items, 20 | }; 21 | -------------------------------------------------------------------------------- /lib/backend/models/settings.dart: -------------------------------------------------------------------------------- 1 | //dart run build_runner build 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:json_annotation/json_annotation.dart'; 5 | import 'package:playboy/backend/models/playitem.dart'; 6 | 7 | part 'settings.g.dart'; 8 | 9 | @JsonSerializable() 10 | class AppSettings { 11 | // Appearance Settings; 12 | String font; 13 | int initPage; 14 | bool playlistListview; 15 | bool videoLibListview; 16 | bool searchListview; 17 | ThemeMode themeMode; 18 | int themeCode; 19 | 20 | // Player Settings 21 | bool autoPlay; 22 | bool autoDownload; 23 | bool keepOpen; 24 | bool preciseSeek; 25 | int listMode; 26 | double defaultVolume; 27 | double defaultSpeed; 28 | bool defaultMusicMode; 29 | Map mpvProperties; 30 | Map mpvOptions; 31 | bool enableMpvConfig; 32 | bool useDefaultKeyBinding; 33 | String mpvConfigPath; 34 | int mpvOsdLevel; 35 | 36 | // 0:ask 1:never 2:always 37 | int continueToPlay; 38 | double volume; 39 | double speed; 40 | bool rememberStatus; 41 | bool playAfterExit; 42 | 43 | // Storage Settings 44 | bool getCoverOnScan; 45 | List videoPaths; 46 | List favouritePaths; 47 | String screenshotPath; 48 | String downloadPath; 49 | String tempPath; 50 | 51 | bool recordRecentSearched; 52 | List recentSearched; 53 | bool recordRecentPlayed; 54 | List recentPlayed; 55 | 56 | // Language Settings 57 | String language; 58 | 59 | // Dev Settings 60 | bool enableDevSettings; 61 | bool tabletUI; 62 | bool enableTitleBar; 63 | String libmpvPath; 64 | 65 | String model; 66 | bool useMirrorLink; 67 | 68 | AppSettings({ 69 | // Display Settings, 70 | this.font = '', 71 | this.initPage = 0, 72 | this.playlistListview = false, 73 | this.videoLibListview = false, 74 | this.searchListview = false, 75 | this.themeMode = ThemeMode.system, 76 | this.themeCode = 4, 77 | 78 | // Player Settings 79 | this.autoPlay = true, 80 | this.autoDownload = false, 81 | this.keepOpen = true, 82 | this.preciseSeek = false, 83 | this.listMode = 0, 84 | this.defaultVolume = 100, 85 | this.defaultSpeed = 1, 86 | this.defaultMusicMode = false, 87 | // 0:ask 1:never 2:always 88 | this.continueToPlay = 0, 89 | this.volume = 100, 90 | this.speed = 1, 91 | this.rememberStatus = true, 92 | this.playAfterExit = true, 93 | this.mpvOptions = const {}, 94 | this.mpvProperties = const {}, 95 | this.enableMpvConfig = true, 96 | this.useDefaultKeyBinding = true, 97 | this.mpvConfigPath = '', 98 | this.mpvOsdLevel = 0, 99 | 100 | // Storage Settings 101 | this.getCoverOnScan = false, 102 | this.videoPaths = const [], 103 | this.favouritePaths = const [], 104 | this.screenshotPath = '', 105 | this.downloadPath = '', 106 | this.recordRecentSearched = false, 107 | this.recentSearched = const [], 108 | this.recordRecentPlayed = false, 109 | this.recentPlayed = const [], 110 | this.tempPath = '', 111 | 112 | // Language Settings 113 | this.language = 'zh_hans', 114 | 115 | // Dev Settings 116 | this.enableDevSettings = false, 117 | this.tabletUI = true, 118 | this.enableTitleBar = true, 119 | this.libmpvPath = '', 120 | this.model = '', 121 | this.useMirrorLink = true, 122 | }); 123 | 124 | factory AppSettings.fromJson(Map json) => 125 | _$AppSettingsFromJson(json); 126 | 127 | Map toJson() => _$AppSettingsToJson(this); 128 | } 129 | -------------------------------------------------------------------------------- /lib/backend/models/settings.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'settings.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | AppSettings _$AppSettingsFromJson(Map json) => AppSettings( 10 | font: json['font'] as String? ?? '', 11 | initPage: (json['initPage'] as num?)?.toInt() ?? 0, 12 | playlistListview: json['playlistListview'] as bool? ?? false, 13 | videoLibListview: json['videoLibListview'] as bool? ?? false, 14 | searchListview: json['searchListview'] as bool? ?? false, 15 | themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ?? 16 | ThemeMode.system, 17 | themeCode: (json['themeCode'] as num?)?.toInt() ?? 4, 18 | autoPlay: json['autoPlay'] as bool? ?? true, 19 | autoDownload: json['autoDownload'] as bool? ?? false, 20 | keepOpen: json['keepOpen'] as bool? ?? true, 21 | preciseSeek: json['preciseSeek'] as bool? ?? false, 22 | listMode: (json['listMode'] as num?)?.toInt() ?? 0, 23 | defaultVolume: (json['defaultVolume'] as num?)?.toDouble() ?? 100, 24 | defaultSpeed: (json['defaultSpeed'] as num?)?.toDouble() ?? 1, 25 | defaultMusicMode: json['defaultMusicMode'] as bool? ?? false, 26 | continueToPlay: (json['continueToPlay'] as num?)?.toInt() ?? 0, 27 | volume: (json['volume'] as num?)?.toDouble() ?? 100, 28 | speed: (json['speed'] as num?)?.toDouble() ?? 1, 29 | rememberStatus: json['rememberStatus'] as bool? ?? true, 30 | playAfterExit: json['playAfterExit'] as bool? ?? true, 31 | mpvOptions: (json['mpvOptions'] as Map?)?.map( 32 | (k, e) => MapEntry(k, e as String), 33 | ) ?? 34 | const {}, 35 | mpvProperties: (json['mpvProperties'] as Map?)?.map( 36 | (k, e) => MapEntry(k, e as String), 37 | ) ?? 38 | const {}, 39 | enableMpvConfig: json['enableMpvConfig'] as bool? ?? true, 40 | useDefaultKeyBinding: json['useDefaultKeyBinding'] as bool? ?? true, 41 | mpvConfigPath: json['mpvConfigPath'] as String? ?? '', 42 | mpvOsdLevel: (json['mpvOsdLevel'] as num?)?.toInt() ?? 0, 43 | getCoverOnScan: json['getCoverOnScan'] as bool? ?? false, 44 | videoPaths: (json['videoPaths'] as List?) 45 | ?.map((e) => e as String) 46 | .toList() ?? 47 | const [], 48 | favouritePaths: (json['favouritePaths'] as List?) 49 | ?.map((e) => e as String) 50 | .toList() ?? 51 | const [], 52 | screenshotPath: json['screenshotPath'] as String? ?? '', 53 | downloadPath: json['downloadPath'] as String? ?? '', 54 | recordRecentSearched: json['recordRecentSearched'] as bool? ?? false, 55 | recentSearched: (json['recentSearched'] as List?) 56 | ?.map((e) => e as String) 57 | .toList() ?? 58 | const [], 59 | recordRecentPlayed: json['recordRecentPlayed'] as bool? ?? false, 60 | recentPlayed: (json['recentPlayed'] as List?) 61 | ?.map((e) => PlayItem.fromJson(e as Map)) 62 | .toList() ?? 63 | const [], 64 | language: json['language'] as String? ?? 'zh_hans', 65 | enableDevSettings: json['enableDevSettings'] as bool? ?? false, 66 | tabletUI: json['tabletUI'] as bool? ?? true, 67 | enableTitleBar: json['enableTitleBar'] as bool? ?? true, 68 | libmpvPath: json['libmpvPath'] as String? ?? '', 69 | model: json['model'] as String? ?? '', 70 | useMirrorLink: json['useMirrorLink'] as bool? ?? true, 71 | ); 72 | 73 | Map _$AppSettingsToJson(AppSettings instance) => 74 | { 75 | 'font': instance.font, 76 | 'initPage': instance.initPage, 77 | 'playlistListview': instance.playlistListview, 78 | 'videoLibListview': instance.videoLibListview, 79 | 'searchListview': instance.searchListview, 80 | 'themeMode': _$ThemeModeEnumMap[instance.themeMode]!, 81 | 'themeCode': instance.themeCode, 82 | 'autoPlay': instance.autoPlay, 83 | 'autoDownload': instance.autoDownload, 84 | 'keepOpen': instance.keepOpen, 85 | 'preciseSeek': instance.preciseSeek, 86 | 'listMode': instance.listMode, 87 | 'defaultVolume': instance.defaultVolume, 88 | 'defaultSpeed': instance.defaultSpeed, 89 | 'defaultMusicMode': instance.defaultMusicMode, 90 | 'mpvProperties': instance.mpvProperties, 91 | 'mpvOptions': instance.mpvOptions, 92 | 'enableMpvConfig': instance.enableMpvConfig, 93 | 'useDefaultKeyBinding': instance.useDefaultKeyBinding, 94 | 'mpvConfigPath': instance.mpvConfigPath, 95 | 'mpvOsdLevel': instance.mpvOsdLevel, 96 | 'continueToPlay': instance.continueToPlay, 97 | 'volume': instance.volume, 98 | 'speed': instance.speed, 99 | 'rememberStatus': instance.rememberStatus, 100 | 'playAfterExit': instance.playAfterExit, 101 | 'getCoverOnScan': instance.getCoverOnScan, 102 | 'videoPaths': instance.videoPaths, 103 | 'favouritePaths': instance.favouritePaths, 104 | 'screenshotPath': instance.screenshotPath, 105 | 'downloadPath': instance.downloadPath, 106 | 'recordRecentSearched': instance.recordRecentSearched, 107 | 'recentSearched': instance.recentSearched, 108 | 'recordRecentPlayed': instance.recordRecentPlayed, 109 | 'recentPlayed': instance.recentPlayed, 110 | 'language': instance.language, 111 | 'enableDevSettings': instance.enableDevSettings, 112 | 'tabletUI': instance.tabletUI, 113 | 'enableTitleBar': instance.enableTitleBar, 114 | 'libmpvPath': instance.libmpvPath, 115 | 'model': instance.model, 116 | 'useMirrorLink': instance.useMirrorLink, 117 | }; 118 | 119 | const _$ThemeModeEnumMap = { 120 | ThemeMode.system: 'system', 121 | ThemeMode.light: 'light', 122 | ThemeMode.dark: 'dark', 123 | }; 124 | -------------------------------------------------------------------------------- /lib/backend/player_ex.dart: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | import 'dart:ffi'; 4 | 5 | import 'package:ffi/ffi.dart'; 6 | import 'package:flutter/foundation.dart'; 7 | import 'package:libmpv_dart/libmpv.dart'; 8 | 9 | import 'package:playboy/backend/app.dart'; 10 | import 'package:playboy/backend/models/playitem.dart'; 11 | import 'package:playboy/backend/models/playlist_item.dart'; 12 | 13 | // mpv-version 14 | // mpv-configuration 15 | // ffmpeg-version 16 | // libass-version 17 | 18 | // video render feature from libmpv_dart 19 | 20 | class PlayerEx extends Player { 21 | final ValueNotifier videoAvaiable = ValueNotifier(false); 22 | final ValueNotifier shuffle = ValueNotifier(false); 23 | 24 | final ValueNotifier index = ValueNotifier(0); 25 | final ValueNotifier playlist = ValueNotifier(null); 26 | final ValueNotifier playingItem = ValueNotifier(null); 27 | final ValueNotifier loopMode = ValueNotifier(LoopMode.none); 28 | 29 | PlayerEx( 30 | super.options, { 31 | super.videoOutput, 32 | super.initialize, 33 | }) { 34 | { 35 | 'playlist-playing-pos': mpv_format.MPV_FORMAT_INT64, 36 | }.forEach( 37 | (name, format) => observeProperty(name, format), 38 | ); 39 | 40 | super.propertyChangedCallback = (event) { 41 | final prop = event.ref.data.cast(); 42 | final propName = prop.ref.name.cast().toDartString(); 43 | if (propName == 'playlist-playing-pos' && 44 | prop.ref.format == mpv_format.MPV_FORMAT_INT64 && 45 | prop.ref.data != nullptr) { 46 | final index_ = prop.ref.data.cast().value; 47 | if (index_ >= 0) { 48 | if (playlist.value != null) { 49 | int n = playlist.value!.items.length; 50 | if (0 <= index_ && index_ < n) { 51 | playingItem.value = playlist.value!.items[index_]; 52 | } 53 | } 54 | index.value = index_; 55 | } 56 | } 57 | }; 58 | } 59 | 60 | Future showText(String text) async { 61 | command(['show-text', text]); 62 | } 63 | 64 | Future open(PlayItem media) async { 65 | openList(PlaylistItem(title: 'default-playlist', items: [media])); 66 | } 67 | 68 | Future openList(PlaylistItem playlist_, {bool shuffle_ = false}) async { 69 | if (playlist_.items.isEmpty) return; 70 | pause(); 71 | stop(); 72 | videoAvaiable.value = true; 73 | for (var media in playlist_.items) { 74 | command(['loadfile', media.source, 'append']); 75 | } 76 | if (shuffle_) { 77 | setShuffle(true); 78 | jump(0); 79 | } 80 | playingItem.value = playlist_.items.first; 81 | playlist.value = playlist_; 82 | setPropertyFlag('pause', !App().settings.autoPlay); 83 | } 84 | 85 | Future stop() async { 86 | videoAvaiable.value = false; 87 | playlist.value = null; 88 | command(['stop']); 89 | } 90 | 91 | Future playOrPause() async { 92 | command(['cycle', 'pause']); 93 | } 94 | 95 | Future play() async { 96 | setPropertyFlag('pause', false); 97 | } 98 | 99 | Future pause() async { 100 | setPropertyFlag('pause', true); 101 | } 102 | 103 | Future jump(int pos) async { 104 | setPropertyInt64('playlist-pos', pos); 105 | } 106 | 107 | Future remove(int pos) async { 108 | var newList = playlist.value; 109 | if (newList != null) { 110 | if (0 <= pos && pos < newList.items.length) { 111 | command(['playlist-remove', pos.toString()]); 112 | playlist.value = PlaylistItem( 113 | title: newList.title, 114 | items: newList.items..removeAt(pos), 115 | ); 116 | } 117 | } 118 | } 119 | 120 | Future setSpeed(double value) async { 121 | if (value < 0) return; 122 | setPropertyDouble('speed', value); 123 | } 124 | 125 | Future setVolume(double value) async { 126 | if (value < 0) return; 127 | setPropertyDouble('volume', value); 128 | } 129 | 130 | Future setSubtitle() async {} 131 | 132 | Future setListMode(LoopMode mode) async { 133 | switch (mode) { 134 | case LoopMode.none: 135 | setPropertyString('loop-file', 'no'); 136 | setPropertyString('loop-playlist', 'no'); 137 | case LoopMode.loop: 138 | setPropertyString('loop-file', 'no'); 139 | setPropertyString('loop-playlist', 'yes'); 140 | case LoopMode.single: 141 | setPropertyString('loop-file', 'yes'); 142 | setPropertyString('loop-playlist', 'no'); 143 | } 144 | loopMode.value = mode; 145 | } 146 | 147 | Future setShuffle(bool value) async { 148 | if (value) { 149 | command(['playlist-shuffle']); 150 | } else { 151 | command(['playlist-unshuffle']); 152 | } 153 | } 154 | 155 | Future prev() async { 156 | command(['playlist-prev']); 157 | } 158 | 159 | Future next() async { 160 | command(['playlist-next']); 161 | } 162 | 163 | Future seek(int secs, {bool absoulte = true}) async { 164 | command([ 165 | 'seek', 166 | secs.toString(), 167 | if (absoulte) 'absolute', 168 | ]); 169 | } 170 | } 171 | 172 | enum LoopMode { 173 | none, 174 | loop, 175 | single, 176 | } 177 | 178 | */ 179 | -------------------------------------------------------------------------------- /lib/backend/utils/l10n_utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter/services.dart'; 4 | import 'package:playboy/backend/app.dart'; 5 | 6 | extension L10n on String { 7 | String _translate() { 8 | var lang = App().settings.language; 9 | return (translations[lang]?[this]) ?? this; 10 | } 11 | 12 | String get l10n => _translate(); 13 | 14 | static final Map> translations = {}; 15 | 16 | static Future init() async { 17 | var languages = jsonDecode( 18 | await rootBundle.loadString('l10n/manifest.json'), 19 | ); 20 | 21 | for (var lang in languages) { 22 | var data = await rootBundle.loadString("l10n/$lang.json"); 23 | Map translationData = jsonDecode(data); 24 | translations[lang] = translationData.map( 25 | (k, v) => MapEntry(k, v.toString()), 26 | ); 27 | } 28 | } 29 | } 30 | 31 | extension ListTranslation on List { 32 | List _translate() { 33 | return List.generate(length, (index) => this[index].l10n); 34 | } 35 | 36 | List get l10n => _translate(); 37 | } 38 | -------------------------------------------------------------------------------- /lib/backend/utils/media_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:media_kit/media_kit.dart'; 2 | 3 | extension MediaUtils on Playlist { 4 | Media get current => medias[index]; 5 | } 6 | -------------------------------------------------------------------------------- /lib/backend/utils/route_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | Future pushPage( 4 | BuildContext context, 5 | Widget page, 6 | ) async { 7 | return await Navigator.of( 8 | context, 9 | rootNavigator: false, 10 | ).push( 11 | MaterialPageRoute( 12 | builder: (context) => page, 13 | ), 14 | ); 15 | } 16 | 17 | Future pushPageNoAnimation( 18 | BuildContext context, 19 | Widget page, 20 | ) async { 21 | return await Navigator.of( 22 | context, 23 | rootNavigator: false, 24 | ).push( 25 | PageRouteBuilder( 26 | pageBuilder: (context, animation1, animation2) => page, 27 | transitionDuration: Duration.zero, 28 | reverseTransitionDuration: Duration.zero, 29 | ), 30 | ); 31 | } 32 | 33 | Future pushRootPage( 34 | BuildContext context, 35 | Widget page, 36 | ) async { 37 | return await Navigator.of( 38 | context, 39 | rootNavigator: true, 40 | ).push( 41 | MaterialPageRoute( 42 | builder: (context) => page, 43 | ), 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /lib/backend/utils/sliver_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | extension SliverToBoxAdapterExtension on Widget { 4 | SliverToBoxAdapter toSliver() { 5 | return SliverToBoxAdapter( 6 | child: this, 7 | ); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/backend/utils/string_utils.dart: -------------------------------------------------------------------------------- 1 | // check if target is sub-sequence of text 2 | bool isSubsequence(String target, String text) { 3 | int n = target.length, m = text.length; 4 | 5 | int i = 0; 6 | for (int j = 0; j < m && i < n; j++) { 7 | if (text[j].toLowerCase() == target[i].toLowerCase()) { 8 | i++; 9 | } 10 | } 11 | 12 | return i == n; 13 | } 14 | 15 | // TODO: use normalize url from path package 16 | String unifyPath(String path, {bool endSlash = true}) { 17 | String result = (path).replaceAll(r'\', '/'); 18 | if (endSlash) { 19 | if (!result.endsWith('/')) result += '/'; 20 | } else { 21 | if (result.endsWith('/')) { 22 | int n = result.length; 23 | result = result.substring(0, n - 1); 24 | } 25 | } 26 | 27 | return result; 28 | } 29 | -------------------------------------------------------------------------------- /lib/backend/utils/theme_utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:playboy/backend/app.dart'; 5 | 6 | MaterialColor getColorTheme() { 7 | return Colors.primaries[App().settings.themeCode]; 8 | } 9 | 10 | ThemeData getThemeData(App value, ColorScheme colorScheme) { 11 | return ThemeData( 12 | pageTransitionsTheme: const PageTransitionsTheme( 13 | builders: { 14 | TargetPlatform.windows: CupertinoPageTransitionsBuilder(), 15 | TargetPlatform.macOS: CupertinoPageTransitionsBuilder(), 16 | TargetPlatform.linux: CupertinoPageTransitionsBuilder(), 17 | }, 18 | ), 19 | fontFamily: value.settings.font != '' ? value.settings.font : null, 20 | fontFamilyFallback: Platform.isWindows ? ['Microsoft YaHei UI'] : null, 21 | colorScheme: colorScheme, 22 | tooltipTheme: TooltipThemeData( 23 | decoration: BoxDecoration( 24 | color: colorScheme.secondary, 25 | borderRadius: BorderRadius.circular(6), 26 | ), 27 | textStyle: TextStyle( 28 | color: colorScheme.onSecondary, 29 | fontWeight: FontWeight.w500, 30 | fontSize: 12, 31 | ), 32 | ), 33 | dialogTheme: DialogTheme( 34 | backgroundColor: colorScheme.surface, 35 | surfaceTintColor: Colors.transparent, 36 | barrierColor: colorScheme.surfaceTint.withValues(alpha: 0.1), 37 | shadowColor: Colors.black, 38 | ), 39 | appBarTheme: AppBarTheme( 40 | scrolledUnderElevation: 0, 41 | backgroundColor: colorScheme.surface, 42 | ), 43 | navigationRailTheme: NavigationRailThemeData( 44 | backgroundColor: colorScheme.appBackground, 45 | indicatorColor: colorScheme.primaryContainer, 46 | ), 47 | iconButtonTheme: const IconButtonThemeData( 48 | style: ButtonStyle( 49 | iconSize: WidgetStatePropertyAll(22), 50 | ), 51 | ), 52 | menuTheme: MenuThemeData( 53 | style: MenuStyle( 54 | backgroundColor: WidgetStatePropertyAll(colorScheme.appBackground), 55 | shape: WidgetStatePropertyAll( 56 | RoundedRectangleBorder( 57 | borderRadius: BorderRadius.circular(10), 58 | ), 59 | ), 60 | ), 61 | ), 62 | sliderTheme: SliderThemeData( 63 | // ignore: deprecated_member_use 64 | year2023: false, 65 | trackHeight: 4, 66 | thumbSize: const WidgetStatePropertyAll(Size(4, 12)), 67 | overlayShape: SliderComponentShape.noOverlay, 68 | ), 69 | ); 70 | } 71 | 72 | extension ThemeUtils on ColorScheme { 73 | Color get appBackground => Color.alphaBlend( 74 | primary.withValues(alpha: 0.04), 75 | surface, 76 | ); 77 | 78 | Color get actionHoverColor => Color.alphaBlend( 79 | primary.withValues(alpha: 0.08), 80 | surface, 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /lib/backend/utils/time_utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | String getProgressString(Duration duration) { 4 | if (duration.inSeconds ~/ 3600 == 0) { 5 | return '${(duration.inSeconds % 3600 ~/ 60).toString().padLeft(2, '0')}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}'; 6 | } else { 7 | return '${duration.inSeconds ~/ 3600}:${(duration.inSeconds % 3600 ~/ 60).toString().padLeft(2, '0')}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}'; 8 | } 9 | } 10 | 11 | String getCurrentTimeString() { 12 | final now = DateTime.now(); 13 | return now.microsecondsSinceEpoch.toString(); 14 | } 15 | 16 | double bounded(double l, double val, double r) { 17 | return max(min(val, r), l); 18 | } 19 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:playboy/backend/keymap_helper.dart'; 5 | import 'package:wakelock_plus/wakelock_plus.dart'; 6 | import 'package:window_manager/window_manager.dart'; 7 | import 'package:media_kit/media_kit.dart'; 8 | 9 | import 'package:playboy/backend/library_helper.dart'; 10 | import 'package:playboy/backend/app.dart'; 11 | import 'package:playboy/backend/utils/l10n_utils.dart'; 12 | import 'package:playboy/pages/home.dart'; 13 | 14 | void main(List arguments) async { 15 | WidgetsFlutterBinding.ensureInitialized(); 16 | MediaKit.ensureInitialized(); 17 | WakelockPlus.enable(); 18 | 19 | await App().init(); 20 | await L10n.init(); 21 | 22 | if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { 23 | await windowManager.ensureInitialized(); 24 | WindowOptions windowOptions = const WindowOptions( 25 | minimumSize: Size(700, 500), 26 | backgroundColor: Colors.transparent, 27 | titleBarStyle: TitleBarStyle.hidden, 28 | ); 29 | windowManager.waitUntilReadyToShow(windowOptions, () async { 30 | await windowManager.show(); 31 | await windowManager.focus(); 32 | }); 33 | } 34 | 35 | KeyMapHelper.init(); 36 | 37 | if (arguments.isNotEmpty) { 38 | String mediaToOpen = arguments[0]; 39 | App().openMedia(await LibraryHelper.getItemFromFile(mediaToOpen)); 40 | runApp(const HomePage(playerView: true)); 41 | } else { 42 | runApp(const HomePage(playerView: false)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/pages/file/file_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:path/path.dart'; 3 | import 'package:playboy/backend/library_helper.dart'; 4 | import 'package:playboy/backend/models/playitem.dart'; 5 | import 'package:playboy/backend/app.dart'; 6 | import 'package:playboy/pages/library/common_media_menu.dart'; 7 | import 'package:playboy/widgets/cover_card.dart'; 8 | import 'package:playboy/widgets/interactive_wrapper.dart'; 9 | 10 | class FileCard extends StatelessWidget { 11 | const FileCard({ 12 | super.key, 13 | required this.source, 14 | required this.icon, 15 | }); 16 | final String source; 17 | final IconData? icon; 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | late final colorScheme = Theme.of(context).colorScheme; 22 | String name = basename(source); 23 | 24 | return MInteractiveWrapper( 25 | menuController: MenuController(), 26 | menuChildren: [ 27 | const SizedBox(height: 10), 28 | ...buildCommonMediaMenuItems( 29 | context, 30 | colorScheme, 31 | PlayItem( 32 | source: source, 33 | title: name, 34 | ), 35 | ), 36 | const SizedBox(height: 10), 37 | ], 38 | onTap: () { 39 | if (LibraryHelper.supportFormats.contains(extension(source))) { 40 | App().openMedia( 41 | PlayItem(source: source, title: source), 42 | ); 43 | 44 | App().actions['togglePlayer']?.call(); 45 | } 46 | }, 47 | borderRadius: 20, 48 | child: MCoverCard( 49 | cover: null, 50 | title: name, 51 | aspectRatio: 1, 52 | icon: icon ?? Icons.insert_drive_file_outlined, 53 | ), 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/pages/file/folder_listtile.dart: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/lib/pages/file/folder_listtile.dart -------------------------------------------------------------------------------- /lib/pages/home/titlebar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AppTitleBar extends StatefulWidget { 4 | const AppTitleBar({ 5 | super.key, 6 | this.leftCaptionButton = false, 7 | }); 8 | 9 | final bool leftCaptionButton; 10 | 11 | @override 12 | State createState() => _AppTitleBarState(); 13 | } 14 | 15 | class _AppTitleBarState extends State { 16 | @override 17 | Widget build(BuildContext context) { 18 | return const Placeholder(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/pages/library/common_media_menu.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:playboy/backend/library_helper.dart'; 3 | import 'package:playboy/backend/models/playitem.dart'; 4 | import 'package:playboy/backend/app.dart'; 5 | import 'package:playboy/backend/utils/l10n_utils.dart'; 6 | import 'package:playboy/widgets/menu/menu_item.dart'; 7 | import 'package:playboy/widgets/playlist_picker.dart'; 8 | 9 | List buildCommonMediaMenuItems( 10 | BuildContext context, 11 | ColorScheme colorScheme, 12 | PlayItem item, 13 | ) { 14 | return [ 15 | MMenuItem( 16 | icon: Icons.open_in_new, 17 | label: '播放器播放'.l10n, 18 | onPressed: () { 19 | App().openMedia(item); 20 | App().actions['togglePlayer']?.call(); 21 | }, 22 | ), 23 | MMenuItem( 24 | icon: Icons.play_circle_outline_rounded, 25 | label: '播放'.l10n, 26 | onPressed: () { 27 | App().openMedia(item); 28 | }, 29 | ), 30 | MMenuItem( 31 | icon: Icons.navigate_next_outlined, 32 | label: '插播'.l10n, 33 | onPressed: null, 34 | ), 35 | MMenuItem( 36 | icon: Icons.last_page, 37 | label: '最后播放'.l10n, 38 | onPressed: null, 39 | ), 40 | MMenuItem( 41 | icon: Icons.add_circle_outline, 42 | label: '添加到播放列表'.l10n, 43 | onPressed: () { 44 | App().dialog( 45 | (context) => AlertDialog( 46 | // surfaceTintColor: Colors.transparent, 47 | title: Text('添加到播放列表'.l10n), 48 | content: SizedBox( 49 | width: 300, 50 | height: 300, 51 | child: ListView.builder( 52 | itemBuilder: (context, indexList) { 53 | return SizedBox( 54 | height: 60, 55 | child: InkWell( 56 | borderRadius: BorderRadius.circular(20), 57 | onTap: () { 58 | LibraryHelper.addItemToPlaylist( 59 | App().playlists[indexList], 60 | item, 61 | ); 62 | Navigator.pop(context); 63 | }, 64 | child: PlaylistPickerItem( 65 | info: App().playlists[indexList], 66 | ), 67 | ), 68 | ); 69 | }, 70 | itemCount: App().playlists.length, 71 | ), 72 | ), 73 | ), 74 | ); 75 | }, 76 | ), 77 | ]; 78 | } 79 | -------------------------------------------------------------------------------- /lib/pages/library/media_menu.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:file_picker/file_picker.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:playboy/backend/models/playitem.dart'; 6 | import 'package:playboy/backend/utils/l10n_utils.dart'; 7 | import 'package:playboy/pages/library/common_media_menu.dart'; 8 | import 'package:playboy/widgets/menu/menu_item.dart'; 9 | 10 | List buildMediaMenuItems( 11 | Function callback, 12 | BuildContext context, 13 | ColorScheme colorScheme, 14 | PlayItem item, 15 | ) { 16 | return [ 17 | const SizedBox(height: 10), 18 | ...buildCommonMediaMenuItems(context, colorScheme, item), 19 | const Divider(), 20 | MMenuItem( 21 | icon: Icons.design_services_outlined, 22 | label: '修改封面'.l10n, 23 | onPressed: () async { 24 | String? coverPath = 25 | await FilePicker.platform.pickFiles(type: FileType.image).then( 26 | (result) { 27 | return result?.files.single.path; 28 | }, 29 | ); 30 | if (coverPath != null) { 31 | var savePath = item.cover; 32 | var originalFile = File(coverPath); 33 | var newFile = File(savePath); 34 | // item.cover = savePath; 35 | await originalFile.copy(newFile.path).then((_) { 36 | // final ImageProvider imageProvider = FileImage(newFile); 37 | // imageProvider.evict(); 38 | // state.setState(() {}); 39 | callback(); 40 | }); 41 | } 42 | }, 43 | ), 44 | MMenuItem( 45 | icon: Icons.cleaning_services, 46 | label: '清除封面'.l10n, 47 | onPressed: () async { 48 | var file = File(item.cover); 49 | if (await file.exists()) { 50 | file.delete(); 51 | final ImageProvider imageProvider = FileImage(file); 52 | imageProvider.evict(); 53 | } 54 | // setState(() {}); 55 | callback(); 56 | }, 57 | ), 58 | const Divider(), 59 | MMenuItem( 60 | icon: Icons.info_outline, 61 | label: '属性'.l10n, 62 | onPressed: null, 63 | ), 64 | const SizedBox(height: 10), 65 | ]; 66 | } 67 | -------------------------------------------------------------------------------- /lib/pages/media/player_menu.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:file_picker/file_picker.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:playboy/backend/app.dart'; 6 | import 'package:playboy/backend/models/playitem.dart'; 7 | 8 | import 'package:playboy/backend/utils/l10n_utils.dart'; 9 | import 'package:playboy/backend/utils/time_utils.dart'; 10 | import 'package:playboy/widgets/menu/menu_item.dart'; 11 | 12 | List buildPlayerMenu(BuildContext context) { 13 | return [ 14 | MMenuItem( 15 | icon: Icons.cut, 16 | label: '截图'.l10n, 17 | onPressed: () async { 18 | if (App().playingTitle == 'Not Playing') return; 19 | var image = await App().player.screenshot(); 20 | if (image != null) { 21 | var file = File( 22 | '${App().settings.screenshotPath}/${getCurrentTimeString()}.png', 23 | ); 24 | await file.writeAsBytes(image); 25 | } 26 | }, 27 | ), 28 | MMenuItem( 29 | icon: Icons.cut, 30 | label: '将当前画面设为封面'.l10n, 31 | onPressed: () async { 32 | if (App().playingTitle == 'Not Playing') return; 33 | var image = await App().player.screenshot(); 34 | if (image != null) { 35 | var file = File('${App().playingCover}'); 36 | await file.writeAsBytes(image); 37 | final ImageProvider imageProvider = FileImage(file); 38 | imageProvider.evict(); 39 | } 40 | App().updateStatus(); 41 | }, 42 | ), 43 | const Divider(), 44 | MMenuItem( 45 | icon: Icons.file_open_outlined, 46 | label: '打开文件'.l10n, 47 | onPressed: () async { 48 | var res = await FilePicker.platform.pickFiles(lockParentWindow: true); 49 | if (res != null) { 50 | String link = res.files.single.path!; 51 | _openLink(link); 52 | } 53 | }, 54 | ), 55 | MMenuItem( 56 | icon: Icons.folder_open, 57 | label: '打开文件夹'.l10n, 58 | onPressed: () async { 59 | var res = await FilePicker.platform.getDirectoryPath( 60 | lockParentWindow: true, 61 | ); 62 | if (res != null) { 63 | String link = res; 64 | _openLink(link); 65 | } 66 | }, 67 | ), 68 | MMenuItem( 69 | icon: Icons.link, 70 | label: '打开URL'.l10n, 71 | onPressed: () { 72 | var editingController = TextEditingController(); 73 | App().dialog( 74 | (BuildContext context) => AlertDialog( 75 | surfaceTintColor: Colors.transparent, 76 | title: Text('播放网络串流'.l10n), 77 | content: TextField( 78 | autofocus: true, 79 | maxLines: 1, 80 | controller: editingController, 81 | decoration: const InputDecoration( 82 | prefixIcon: Icon(Icons.link), 83 | border: OutlineInputBorder(), 84 | labelText: 'URL', 85 | ), 86 | onSubmitted: (value) async { 87 | _openLink(value); 88 | }, 89 | ), 90 | actions: [ 91 | TextButton( 92 | onPressed: () { 93 | Navigator.pop(context); 94 | }, 95 | child: Text('取消'.l10n), 96 | ), 97 | TextButton( 98 | onPressed: () async { 99 | _openLink(editingController.text); 100 | }, 101 | child: Text('确定'.l10n), 102 | ), 103 | ], 104 | ), 105 | ); 106 | }, 107 | ), 108 | ]; 109 | } 110 | 111 | void _openLink(String source) async { 112 | App().openMedia( 113 | PlayItem(source: source, title: source), 114 | ); 115 | } 116 | -------------------------------------------------------------------------------- /lib/pages/media/seekbar_builder.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:playboy/backend/app.dart'; 5 | 6 | Widget buildMediaSeekbar(Function callback) { 7 | return StreamBuilder( 8 | stream: App().player.stream.duration, 9 | builder: (context, snapshot) { 10 | return StreamBuilder( 11 | stream: App().player.stream.position, 12 | builder: (context, snapshot) { 13 | double pos = App().seeking 14 | ? App().seekingPos 15 | : App().player.state.position.inMilliseconds.toDouble(); 16 | double dur = App().player.state.duration.inMilliseconds.toDouble(); 17 | pos = min(pos, dur); 18 | return Slider( 19 | max: dur, 20 | value: pos, 21 | onChanged: (value) { 22 | App().seekingPos = value; 23 | callback(); 24 | }, 25 | onChangeStart: (value) { 26 | App().seeking = true; 27 | callback(); 28 | }, 29 | onChangeEnd: (value) { 30 | App().player.seek(Duration(milliseconds: value.toInt())); 31 | App().seeking = false; 32 | callback(); 33 | }, 34 | ); 35 | }, 36 | ); 37 | }, 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /lib/pages/playlist/common_playlist_menu.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:file_picker/file_picker.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:playboy/backend/models/playlist_item.dart'; 6 | import 'package:playboy/backend/app.dart'; 7 | import 'package:playboy/backend/utils/l10n_utils.dart'; 8 | import 'package:playboy/widgets/menu/menu_item.dart'; 9 | 10 | List buildCommonPlaylistMenuItems( 11 | BuildContext context, 12 | ColorScheme colorScheme, 13 | PlaylistItem item, 14 | ) { 15 | return [ 16 | MMenuItem( 17 | icon: Icons.play_circle_outline_rounded, 18 | label: '播放'.l10n, 19 | onPressed: () { 20 | App().openPlaylist(item, false); 21 | }, 22 | ), 23 | MMenuItem( 24 | icon: Icons.shuffle, 25 | label: '随机播放'.l10n, 26 | onPressed: () { 27 | App().openPlaylist( 28 | item, 29 | true, 30 | ); 31 | }, 32 | ), 33 | MMenuItem( 34 | icon: Icons.share, 35 | label: '导出'.l10n, 36 | onPressed: () async { 37 | final originalFile = File( 38 | '${App().dataPath}/playlists/${item.title}.json', 39 | ); 40 | String? newFilePath = await FilePicker.platform.saveFile( 41 | dialogTitle: '另存为', 42 | fileName: '${item.title}.json', 43 | ); 44 | 45 | if (newFilePath != null) { 46 | final newFile = File(newFilePath); 47 | 48 | await originalFile.copy(newFile.path); 49 | if (!context.mounted) return; 50 | ScaffoldMessenger.of(context).showSnackBar( 51 | SnackBar( 52 | content: Text( 53 | '已经保存到: $newFilePath', 54 | ), 55 | ), 56 | ); 57 | } 58 | }, 59 | ), 60 | ]; 61 | } 62 | -------------------------------------------------------------------------------- /lib/pages/playlist/playlist_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:playboy/backend/models/playlist_item.dart'; 3 | import 'package:playboy/widgets/cover_card.dart'; 4 | import 'package:playboy/widgets/interactive_wrapper.dart'; 5 | 6 | class PlaylistCard extends StatefulWidget { 7 | const PlaylistCard({ 8 | super.key, 9 | required this.info, 10 | required this.menuItems, 11 | this.onTap, 12 | }); 13 | 14 | final PlaylistItem info; 15 | final List menuItems; 16 | final Function()? onTap; 17 | 18 | @override 19 | State createState() => _PlaylistCardState(); 20 | } 21 | 22 | class _PlaylistCardState extends State { 23 | @override 24 | Widget build(BuildContext context) { 25 | return MInteractiveWrapper( 26 | menuController: MenuController(), 27 | menuChildren: widget.menuItems, 28 | onTap: widget.onTap, 29 | borderRadius: 20, 30 | child: MCoverCard( 31 | aspectRatio: 1, 32 | icon: Icons.playlist_play_rounded, 33 | cover: widget.info.cover, 34 | title: widget.info.title, 35 | ), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/pages/playlist/playlist_listtile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:playboy/backend/models/playlist_item.dart'; 3 | import 'package:playboy/widgets/cover_listtile.dart'; 4 | import 'package:playboy/widgets/menu/menu_button.dart'; 5 | 6 | class PlaylistListtile extends StatefulWidget { 7 | const PlaylistListtile({ 8 | super.key, 9 | required this.info, 10 | required this.actions, 11 | required this.menuItems, 12 | this.onTap, 13 | }); 14 | 15 | final PlaylistItem info; 16 | final Function()? onTap; 17 | final List actions; 18 | final List menuItems; 19 | 20 | @override 21 | State createState() => _PlaylistListtileState(); 22 | } 23 | 24 | class _PlaylistListtileState extends State { 25 | @override 26 | Widget build(BuildContext context) { 27 | return MCoverListTile( 28 | aspectRatio: 1, 29 | height: 60, 30 | cover: widget.info.cover, 31 | icon: Icons.playlist_play_rounded, 32 | label: widget.info.title, 33 | onTap: widget.onTap, 34 | actions: [ 35 | ...widget.actions, 36 | MenuButton( 37 | menuChildren: widget.menuItems, 38 | ), 39 | ], 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/pages/playlist/playlist_loader.dart: -------------------------------------------------------------------------------- 1 | import 'package:playboy/backend/app.dart'; 2 | import 'package:playboy/backend/library_helper.dart'; 3 | 4 | void loadPlaylists(Function callback) async { 5 | if (App().playlistLoaded) return; 6 | App().playlists.clear(); 7 | App().playlists.addAll(await LibraryHelper.loadPlaylists()); 8 | App().playlistLoaded = true; 9 | callback(); 10 | } 11 | -------------------------------------------------------------------------------- /lib/pages/playlist/playlist_menu.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:file_picker/file_picker.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:playboy/backend/app.dart'; 6 | import 'package:playboy/backend/library_helper.dart'; 7 | import 'package:playboy/backend/models/playlist_item.dart'; 8 | import 'package:playboy/backend/utils/l10n_utils.dart'; 9 | import 'package:playboy/pages/playlist/common_playlist_menu.dart'; 10 | import 'package:playboy/widgets/menu/menu_item.dart'; 11 | 12 | List buildPlaylistMenuItems( 13 | Function callback, 14 | BuildContext context, 15 | ColorScheme colorScheme, 16 | PlaylistItem item, 17 | ) { 18 | TextEditingController editingController = TextEditingController(); 19 | return [ 20 | const SizedBox(height: 10), 21 | ...buildCommonPlaylistMenuItems( 22 | context, 23 | colorScheme, 24 | item, 25 | ), 26 | const Divider(), 27 | MMenuItem( 28 | icon: Icons.design_services_outlined, 29 | label: '修改封面'.l10n, 30 | onPressed: () async { 31 | String? coverPath = 32 | await FilePicker.platform.pickFiles(type: FileType.image).then( 33 | (result) { 34 | return result?.files.single.path; 35 | }, 36 | ); 37 | if (coverPath != null) { 38 | var savePath = '${App().dataPath}/playlists/${item.title}.cover.jpg'; 39 | var originalFile = File(coverPath); 40 | var newFile = File(savePath); 41 | await originalFile.copy(newFile.path).then( 42 | (_) { 43 | final ImageProvider imageProvider = FileImage(newFile); 44 | imageProvider.evict(); 45 | callback(); 46 | }, 47 | ); 48 | } 49 | }, 50 | ), 51 | MMenuItem( 52 | icon: Icons.cleaning_services, 53 | label: '清除封面'.l10n, 54 | onPressed: () async { 55 | var cover = File(item.cover); 56 | if (await cover.exists()) { 57 | await cover.delete(); 58 | } 59 | callback(); 60 | }, 61 | ), 62 | MMenuItem( 63 | icon: Icons.drive_file_rename_outline, 64 | label: '重命名'.l10n, 65 | onPressed: () { 66 | editingController.clear(); 67 | editingController.text = item.title; 68 | App().dialog( 69 | (BuildContext context) => AlertDialog( 70 | surfaceTintColor: Colors.transparent, 71 | title: Text('重命名'.l10n), 72 | content: TextField( 73 | autofocus: true, 74 | maxLines: 1, 75 | controller: editingController, 76 | decoration: InputDecoration( 77 | border: const OutlineInputBorder(), 78 | labelText: '名称'.l10n, 79 | ), 80 | onSubmitted: (value) { 81 | LibraryHelper.renamePlaylist( 82 | item, 83 | value, 84 | ); 85 | callback(); 86 | Navigator.pop(context); 87 | }, 88 | ), 89 | actions: [ 90 | TextButton( 91 | onPressed: () { 92 | Navigator.pop(context); 93 | }, 94 | child: Text('取消'.l10n), 95 | ), 96 | TextButton( 97 | onPressed: () { 98 | LibraryHelper.renamePlaylist( 99 | item, 100 | editingController.text, 101 | ); 102 | callback(); 103 | Navigator.pop(context); 104 | Navigator.pop(context); 105 | }, 106 | child: Text('确定'.l10n), 107 | ), 108 | ], 109 | ), 110 | ); 111 | }, 112 | ), 113 | MMenuItem( 114 | icon: Icons.delete_outline, 115 | label: '删除'.l10n, 116 | onPressed: () { 117 | App().dialog( 118 | (context) { 119 | return AlertDialog( 120 | title: Text('操作确认'.l10n), 121 | content: Text('确定要删除播放列表吗?'.l10n), 122 | actions: [ 123 | TextButton( 124 | child: Text('取消'.l10n), 125 | onPressed: () { 126 | Navigator.of(context).pop(); 127 | }, 128 | ), 129 | TextButton( 130 | child: Text('确定'.l10n), 131 | onPressed: () { 132 | LibraryHelper.deletePlaylist( 133 | item, 134 | ); 135 | // AppStorage().playlists.removeAt(index); 136 | App().playlists.remove(item); 137 | callback(); 138 | Navigator.of(context).pop(); 139 | }, 140 | ), 141 | ], 142 | ); 143 | }, 144 | ); 145 | }, 146 | ), 147 | const Divider(), 148 | MMenuItem( 149 | icon: Icons.info_outline, 150 | label: '属性'.l10n, 151 | onPressed: null, 152 | ), 153 | const SizedBox(height: 10), 154 | ]; 155 | } 156 | -------------------------------------------------------------------------------- /lib/pages/settings/categories/developer_settings.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:playboy/backend/app.dart'; 3 | import 'package:playboy/backend/utils/l10n_utils.dart'; 4 | 5 | class DeveloperSettings extends StatefulWidget { 6 | const DeveloperSettings({super.key}); 7 | 8 | @override 9 | State createState() => DeveloperSettingsState(); 10 | } 11 | 12 | class DeveloperSettingsState extends State { 13 | // final TextEditingController _controller = TextEditingController(); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | ColorScheme colorScheme = Theme.of(context).colorScheme; 18 | return Scaffold( 19 | body: ListView( 20 | children: [ 21 | Container( 22 | padding: const EdgeInsets.all(12), 23 | child: Text( 24 | '调试'.l10n, 25 | style: TextStyle( 26 | fontSize: 20, 27 | fontWeight: FontWeight.w500, 28 | color: Theme.of(context).colorScheme.secondary, 29 | ), 30 | ), 31 | ), 32 | SwitchListTile( 33 | tileColor: App().settings.enableDevSettings 34 | ? colorScheme.primaryContainer 35 | : colorScheme.primaryContainer.withValues(alpha: 0.2), 36 | shape: RoundedRectangleBorder( 37 | borderRadius: BorderRadius.circular(16), 38 | ), 39 | title: Container( 40 | alignment: Alignment.centerLeft, 41 | height: 40, 42 | child: Text('启用调试选项'.l10n), 43 | ), 44 | value: App().settings.enableDevSettings, 45 | onChanged: (bool value) { 46 | setState(() { 47 | App().settings.enableDevSettings = value; 48 | }); 49 | App().saveSettings(); 50 | }, 51 | ), 52 | ], 53 | ), 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/pages/settings/categories/keymap_settings.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:playboy/backend/utils/l10n_utils.dart'; 3 | import 'package:playboy/widgets/settings_message_box.dart'; 4 | import 'package:url_launcher/url_launcher.dart'; 5 | // import 'package:playboy/backend/storage.dart'; 6 | // import 'package:playboy/l10n/i10n.dart'; 7 | 8 | class KeymapSettings extends StatefulWidget { 9 | const KeymapSettings({super.key}); 10 | 11 | @override 12 | State createState() => _KeymapSettingsState(); 13 | } 14 | 15 | class _KeymapSettingsState extends State { 16 | @override 17 | Widget build(BuildContext context) { 18 | return Scaffold( 19 | body: ListView( 20 | children: [ 21 | Container( 22 | padding: const EdgeInsets.all(12), 23 | child: Text( 24 | '快捷键'.l10n, 25 | style: TextStyle( 26 | fontSize: 20, 27 | fontWeight: FontWeight.w500, 28 | color: Theme.of(context).colorScheme.secondary, 29 | ), 30 | ), 31 | ), 32 | SettingsMessageBox( 33 | message: '修改 input.conf 文件以自定义快捷键'.l10n, 34 | trailing: TextButton( 35 | onPressed: () { 36 | launchUrl( 37 | Uri.parse( 38 | 'https://github.com/mpv-player/mpv/blob/master/etc/input.conf'), 39 | ); 40 | }, 41 | child: Text('示例'.l10n), 42 | ), 43 | ), 44 | ], 45 | ), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/pages/settings/categories/language_settings.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:playboy/backend/app.dart'; 3 | import 'package:playboy/backend/utils/l10n_utils.dart'; 4 | 5 | class LanguageSettingsPage extends StatefulWidget { 6 | const LanguageSettingsPage({super.key}); 7 | 8 | @override 9 | State createState() => _LanguageSettingsPageState(); 10 | } 11 | 12 | class _LanguageSettingsPageState extends State { 13 | @override 14 | Widget build(BuildContext context) { 15 | return Scaffold( 16 | body: ListView( 17 | children: [ 18 | Container( 19 | padding: const EdgeInsets.all(12), 20 | child: Text( 21 | '显示语言'.l10n, 22 | style: TextStyle( 23 | fontSize: 20, 24 | fontWeight: FontWeight.w500, 25 | color: Theme.of(context).colorScheme.secondary, 26 | ), 27 | ), 28 | ), 29 | RadioListTile( 30 | title: const Text('简体中文'), 31 | value: 'zh_hans', 32 | groupValue: App().settings.language, 33 | onChanged: (value) { 34 | if (value != null) { 35 | setState(() { 36 | App().settings.language = value; 37 | }); 38 | App().saveSettings(); 39 | App().updateStatus(); 40 | } 41 | }, 42 | ), 43 | RadioListTile( 44 | title: const Text('English (US)'), 45 | value: 'en_us', 46 | groupValue: App().settings.language, 47 | onChanged: (value) { 48 | if (value != null) { 49 | setState(() { 50 | App().settings.language = value; 51 | }); 52 | App().saveSettings(); 53 | App().updateStatus(); 54 | } 55 | }, 56 | ), 57 | ], 58 | ), 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/pages/settings/categories/whisper_settings.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:path/path.dart'; 5 | import 'package:playboy/backend/app.dart'; 6 | import 'package:playboy/backend/ml/whisper_model_list.dart'; 7 | import 'package:playboy/backend/utils/l10n_utils.dart'; 8 | import 'package:playboy/backend/utils/sliver_utils.dart'; 9 | import 'package:playboy/widgets/path_setting_card.dart'; 10 | import 'package:playboy/widgets/settings_message_box.dart'; 11 | import 'package:url_launcher/url_launcher.dart'; 12 | 13 | class WhisperSettingsPage extends StatefulWidget { 14 | const WhisperSettingsPage({super.key}); 15 | 16 | @override 17 | State createState() => _WhisperSettingsPageState(); 18 | } 19 | 20 | class _WhisperSettingsPageState extends State { 21 | String _errorMessage = ''; 22 | final List _modelFiles = []; 23 | 24 | @override 25 | void initState() { 26 | super.initState(); 27 | _init(); 28 | } 29 | 30 | void _init() async { 31 | try { 32 | var dir = Directory('${App().dataPath}/models'); 33 | if (!await dir.exists()) { 34 | await dir.create(); 35 | } 36 | await for (var item in dir.list()) { 37 | if (item is File && extension(item.path) == '.bin') { 38 | _modelFiles.add(basename(item.path)); 39 | } 40 | } 41 | setState(() {}); 42 | } catch (e) { 43 | setState(() { 44 | _errorMessage = e.toString(); 45 | }); 46 | } 47 | } 48 | 49 | @override 50 | Widget build(BuildContext context) { 51 | return Scaffold( 52 | body: CustomScrollView( 53 | slivers: [ 54 | Container( 55 | padding: const EdgeInsets.all(12), 56 | child: Text( 57 | 'Whisper'.l10n, 58 | style: TextStyle( 59 | fontSize: 20, 60 | fontWeight: FontWeight.w500, 61 | color: Theme.of(context).colorScheme.secondary, 62 | ), 63 | ), 64 | ).toSliver(), 65 | SliverToBoxAdapter( 66 | child: ListTile( 67 | onTap: () { 68 | launchUrl(Uri.directory('${App().dataPath}/models')); 69 | }, 70 | leading: const Icon(Icons.folder_outlined), 71 | title: Text('打开模型文件夹'.l10n), 72 | subtitle: Text( 73 | normalize('${App().dataPath}/models'), 74 | overflow: TextOverflow.ellipsis, 75 | ), 76 | ), 77 | ), 78 | Container( 79 | padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), 80 | child: Text( 81 | '本地模型'.l10n, 82 | style: const TextStyle( 83 | fontSize: 16, 84 | fontWeight: FontWeight.w500, 85 | ), 86 | ), 87 | ).toSliver(), 88 | SettingsMessageBox(message: '当前使用模型: ${App().settings.model}') 89 | .toSliver(), 90 | const SizedBox(height: 6).toSliver(), 91 | _errorMessage != '' 92 | ? SettingsMessageBox(message: 'ERROR: $_errorMessage').toSliver() 93 | : SliverList.builder( 94 | itemBuilder: (context, index) { 95 | return PathSettingCard( 96 | path: _modelFiles[index], 97 | actions: [ 98 | App().settings.model == _modelFiles[index] 99 | ? const SizedBox( 100 | width: 40, 101 | child: Icon(Icons.check_circle_outline), 102 | ) 103 | : TextButton( 104 | onPressed: () { 105 | setState(() { 106 | App().settings.model = _modelFiles[index]; 107 | App().saveSettings(); 108 | }); 109 | }, 110 | child: Text( 111 | '选择'.l10n, 112 | ), 113 | ), 114 | ], 115 | ); 116 | }, 117 | itemCount: _modelFiles.length, 118 | ), 119 | Container( 120 | padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), 121 | child: Text( 122 | '从 Hugging Face 下载'.l10n, 123 | style: const TextStyle( 124 | fontSize: 16, 125 | fontWeight: FontWeight.w500, 126 | ), 127 | ), 128 | ).toSliver(), 129 | SwitchListTile( 130 | title: Text('使用镜像链接'.l10n), 131 | value: App().settings.useMirrorLink, 132 | onChanged: (value) { 133 | setState(() { 134 | App().settings.useMirrorLink = value; 135 | }); 136 | }, 137 | ).toSliver(), 138 | SliverList.builder( 139 | itemBuilder: (context, index) { 140 | return PathSettingCard( 141 | path: models[index].name, 142 | actions: [ 143 | SizedBox( 144 | width: 40, 145 | child: IconButton( 146 | onPressed: () { 147 | if (App().settings.useMirrorLink) { 148 | launchUrl(Uri.parse(modelsMirror[index].link)); 149 | } else { 150 | launchUrl(Uri.parse(models[index].link)); 151 | } 152 | }, 153 | icon: const Icon(Icons.file_download_outlined), 154 | ), 155 | ), 156 | ], 157 | ); 158 | }, 159 | itemCount: models.length, 160 | ), 161 | ], 162 | ), 163 | ); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /lib/widgets/animated_cross_slide.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// Slimilar to AnimatedCrossFade, 4 | /// this widget provides slide transition in two widgets. 5 | /// 6 | /// CAUTIONS 7 | /// The performance of this widget isn't so good. 8 | /// don't use it to build complicated widgets. 9 | class AnimatedCrossSlide extends StatefulWidget { 10 | final Widget firstChild; 11 | final Widget secondChild; 12 | final CrossFadeState crossFadeState; 13 | final Duration duration; 14 | final Duration? reverseDuration; 15 | final Curve firstCurve; 16 | final Curve secondCurve; 17 | final Curve sizeCurve; 18 | final AlignmentGeometry alignment; 19 | 20 | const AnimatedCrossSlide({ 21 | super.key, 22 | required this.firstChild, 23 | required this.secondChild, 24 | required this.crossFadeState, 25 | this.duration = const Duration(milliseconds: 300), 26 | this.reverseDuration, 27 | this.firstCurve = Curves.linear, 28 | this.secondCurve = Curves.linear, 29 | this.sizeCurve = Curves.linear, 30 | this.alignment = Alignment.center, 31 | }); 32 | 33 | @override 34 | AnimatedCrossSlideState createState() => AnimatedCrossSlideState(); 35 | } 36 | 37 | class AnimatedCrossSlideState extends State 38 | with TickerProviderStateMixin { 39 | late AnimationController _controller; 40 | late Animation _firstAnimation; 41 | late Animation _secondAnimation; 42 | 43 | @override 44 | void initState() { 45 | super.initState(); 46 | _controller = AnimationController( 47 | duration: widget.duration, 48 | reverseDuration: widget.reverseDuration, 49 | vsync: this, 50 | ); 51 | _updateAnimations(); 52 | _controller.value = 53 | widget.crossFadeState == CrossFadeState.showFirst ? 0.0 : 1.0; 54 | } 55 | 56 | @override 57 | void didUpdateWidget(AnimatedCrossSlide oldWidget) { 58 | super.didUpdateWidget(oldWidget); 59 | 60 | if (widget.crossFadeState != oldWidget.crossFadeState) { 61 | widget.crossFadeState == CrossFadeState.showFirst 62 | ? _controller.reverse() 63 | : _controller.forward(); 64 | } 65 | 66 | if (widget.duration != oldWidget.duration) { 67 | _controller.duration = widget.duration; 68 | } 69 | 70 | if (widget.firstCurve != oldWidget.firstCurve || 71 | widget.secondCurve != oldWidget.secondCurve) { 72 | _updateAnimations(); 73 | } 74 | } 75 | 76 | void _updateAnimations() { 77 | _firstAnimation = Tween( 78 | begin: const Offset(-1.0, 0.0), 79 | end: Offset.zero, 80 | ).animate(CurvedAnimation( 81 | parent: _controller, 82 | curve: widget.firstCurve, 83 | )); 84 | 85 | _secondAnimation = Tween( 86 | begin: Offset.zero, 87 | end: const Offset(1.0, 0.0), 88 | ).animate(CurvedAnimation( 89 | parent: _controller, 90 | curve: widget.secondCurve, 91 | )); 92 | } 93 | 94 | @override 95 | void dispose() { 96 | _controller.dispose(); 97 | super.dispose(); 98 | } 99 | 100 | @override 101 | Widget build(BuildContext context) { 102 | return AnimatedBuilder( 103 | animation: _controller, 104 | builder: (context, _) { 105 | return AnimatedSize( 106 | duration: widget.duration, 107 | curve: widget.sizeCurve, 108 | alignment: widget.alignment, 109 | child: Stack( 110 | alignment: widget.alignment, 111 | children: [ 112 | SlideTransition( 113 | position: _firstAnimation, 114 | child: widget.firstChild, 115 | ), 116 | SlideTransition( 117 | position: _secondAnimation, 118 | child: widget.secondChild, 119 | ), 120 | ], 121 | ), 122 | ); 123 | }, 124 | ); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /lib/widgets/cover.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | class MCover extends StatelessWidget { 6 | const MCover({ 7 | super.key, 8 | required this.cover, 9 | required this.aspectRatio, 10 | required this.icon, 11 | required this.iconSize, 12 | required this.borderRadius, 13 | required this.colorScheme, 14 | }); 15 | 16 | final String? cover; 17 | final double aspectRatio; 18 | final IconData icon; 19 | final double iconSize; 20 | final double borderRadius; 21 | final ColorScheme colorScheme; 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | Color backgroundColor = colorScheme.primaryContainer.withValues(alpha: 0.4); 26 | Color foregroundColor = colorScheme.onPrimaryContainer; 27 | return AspectRatio( 28 | aspectRatio: aspectRatio, 29 | child: cover == null || !File(cover!).existsSync() 30 | ? Container( 31 | width: double.infinity, 32 | decoration: BoxDecoration( 33 | borderRadius: BorderRadius.circular(borderRadius), 34 | color: backgroundColor, 35 | ), 36 | child: Icon( 37 | icon, 38 | color: foregroundColor, 39 | size: iconSize, 40 | ), 41 | ) 42 | : Container( 43 | width: double.infinity, 44 | decoration: BoxDecoration( 45 | borderRadius: BorderRadius.circular(borderRadius), 46 | color: backgroundColor, 47 | image: DecorationImage( 48 | fit: BoxFit.cover, 49 | image: FileImage(File(cover!)), 50 | ), 51 | ), 52 | ), 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/widgets/cover_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:playboy/widgets/cover.dart'; 3 | 4 | class MCoverCard extends StatelessWidget { 5 | const MCoverCard({ 6 | super.key, 7 | required this.icon, 8 | required this.cover, 9 | this.aspectRatio = 1, 10 | required this.title, 11 | }); 12 | 13 | final IconData icon; 14 | final String? cover; 15 | final double aspectRatio; 16 | final String title; 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | late final colorScheme = Theme.of(context).colorScheme; 21 | return Padding( 22 | padding: const EdgeInsets.all(6), 23 | child: Column( 24 | children: [ 25 | Expanded( 26 | flex: 5, 27 | child: MCover( 28 | cover: cover, 29 | aspectRatio: aspectRatio, 30 | icon: icon, 31 | iconSize: 50, 32 | borderRadius: 20, 33 | colorScheme: colorScheme, 34 | ), 35 | ), 36 | const SizedBox(height: 6), 37 | Expanded( 38 | child: Tooltip( 39 | message: title, 40 | waitDuration: const Duration(seconds: 1), 41 | child: Text( 42 | title, 43 | overflow: TextOverflow.ellipsis, 44 | ), 45 | ), 46 | ) 47 | ], 48 | ), 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/widgets/cover_listtile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:playboy/widgets/cover.dart'; 3 | 4 | class MCoverListTile extends StatelessWidget { 5 | const MCoverListTile({ 6 | super.key, 7 | required this.onTap, 8 | required this.height, 9 | required this.icon, 10 | required this.cover, 11 | required this.aspectRatio, 12 | required this.label, 13 | required this.actions, 14 | }); 15 | 16 | final Function()? onTap; 17 | final double height; 18 | 19 | final IconData icon; 20 | final String? cover; 21 | final double aspectRatio; 22 | final String label; 23 | final List actions; 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | late final colorScheme = Theme.of(context).colorScheme; 28 | return InkWell( 29 | focusColor: Colors.transparent, 30 | borderRadius: BorderRadius.circular(16), 31 | onTap: onTap, 32 | child: SizedBox( 33 | height: height, 34 | child: Row( 35 | children: [ 36 | Padding( 37 | padding: const EdgeInsets.all(6), 38 | child: MCover( 39 | cover: cover, 40 | aspectRatio: aspectRatio, 41 | icon: icon, 42 | iconSize: height / 2, 43 | borderRadius: 12, 44 | colorScheme: colorScheme, 45 | ), 46 | ), 47 | const SizedBox(width: 10), 48 | Expanded( 49 | child: Text( 50 | label, 51 | overflow: TextOverflow.ellipsis, 52 | style: const TextStyle(fontSize: 16), 53 | ), 54 | ), 55 | ...actions, 56 | const SizedBox(width: 6), 57 | ], 58 | ), 59 | ), 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/widgets/empty_holder.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:playboy/backend/utils/l10n_utils.dart'; 3 | 4 | class MEmptyHolder extends StatelessWidget { 5 | const MEmptyHolder({super.key}); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | return Padding( 10 | padding: const EdgeInsets.symmetric(horizontal: 16), 11 | child: Card( 12 | elevation: 0, 13 | shape: const RoundedRectangleBorder( 14 | borderRadius: BorderRadius.all(Radius.circular(14)), 15 | ), 16 | child: SizedBox( 17 | height: 100, 18 | child: Row( 19 | mainAxisAlignment: MainAxisAlignment.center, 20 | children: [ 21 | const Icon(Icons.upcoming_rounded), 22 | const SizedBox(width: 10), 23 | Text('无内容'.l10n), 24 | ], 25 | ), 26 | ), 27 | ), 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/widgets/error_holder.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ErrorHolder extends StatelessWidget { 4 | const ErrorHolder({ 5 | super.key, 6 | required this.message, 7 | }); 8 | 9 | final String message; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Padding( 14 | padding: const EdgeInsets.symmetric(horizontal: 16), 15 | child: Card( 16 | elevation: 0, 17 | shape: const RoundedRectangleBorder( 18 | borderRadius: BorderRadius.all(Radius.circular(14)), 19 | ), 20 | child: SizedBox( 21 | height: 100, 22 | child: Row( 23 | mainAxisAlignment: MainAxisAlignment.center, 24 | children: [ 25 | const SizedBox(width: 16), 26 | const Icon( 27 | Icons.error, 28 | size: 30, 29 | ), 30 | const SizedBox(width: 10), 31 | Expanded( 32 | child: Text('Error: $message'), 33 | ), 34 | const SizedBox(width: 16), 35 | ], 36 | ), 37 | ), 38 | ), 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/widgets/folding_holder.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:playboy/backend/utils/l10n_utils.dart'; 3 | 4 | class MFoldingHolder extends StatelessWidget { 5 | const MFoldingHolder({super.key}); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | return Padding( 10 | padding: const EdgeInsets.symmetric(horizontal: 16), 11 | child: Card( 12 | elevation: 0, 13 | shape: const RoundedRectangleBorder( 14 | borderRadius: BorderRadius.all(Radius.circular(14)), 15 | ), 16 | child: SizedBox( 17 | height: 50, 18 | child: Row( 19 | mainAxisAlignment: MainAxisAlignment.center, 20 | children: [ 21 | const Icon(Icons.visibility_off), 22 | const SizedBox(width: 10), 23 | Text('已折叠'.l10n), 24 | ], 25 | ), 26 | ), 27 | ), 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/widgets/icon_switch_listtile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class MIconSwitchListTile extends StatelessWidget { 4 | const MIconSwitchListTile({ 5 | super.key, 6 | required this.icon, 7 | required this.label, 8 | required this.value, 9 | required this.onChanged, 10 | }); 11 | 12 | final IconData icon; 13 | final String label; 14 | 15 | final bool value; 16 | final void Function(bool)? onChanged; 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return SliverToBoxAdapter( 21 | child: SwitchListTile( 22 | value: value, 23 | onChanged: onChanged, 24 | title: Row( 25 | children: [ 26 | Icon(icon), 27 | const SizedBox(width: 12), 28 | Text(label), 29 | ], 30 | ), 31 | ), 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/widgets/image.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | class MImageProvider { 6 | const MImageProvider({required this.url}); 7 | final String url; 8 | 9 | ImageProvider getImage() { 10 | return url.startsWith('http:') 11 | ? NetworkImage(url) 12 | : FileImage(File(url)) as ImageProvider; 13 | } 14 | } 15 | 16 | class MImage extends StatelessWidget { 17 | const MImage({super.key, required this.url}); 18 | final String url; 19 | @override 20 | Widget build(BuildContext context) { 21 | return url.startsWith('http:') 22 | ? Image.network( 23 | url, 24 | width: double.maxFinite, 25 | fit: BoxFit.cover, 26 | ) 27 | : Image.file( 28 | File(url), 29 | width: double.maxFinite, 30 | fit: BoxFit.cover, 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/widgets/interactive_wrapper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class MInteractiveWrapper extends StatelessWidget { 4 | const MInteractiveWrapper({ 5 | super.key, 6 | required this.menuController, 7 | required this.menuChildren, 8 | required this.onTap, 9 | required this.borderRadius, 10 | required this.child, 11 | }); 12 | 13 | final MenuController menuController; 14 | final List menuChildren; 15 | final Widget child; 16 | final Function()? onTap; 17 | final double borderRadius; 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return GestureDetector( 22 | onTap: () { 23 | menuController.close(); 24 | }, 25 | onSecondaryTapDown: (details) { 26 | menuController.open(position: details.localPosition); 27 | }, 28 | child: MenuAnchor( 29 | controller: menuController, 30 | menuChildren: menuChildren, 31 | child: InkWell( 32 | onTap: onTap, 33 | borderRadius: BorderRadius.circular(borderRadius), 34 | child: child, 35 | ), 36 | ), 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/widgets/library/library_header.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class MLibraryHeader extends StatelessWidget { 4 | const MLibraryHeader({ 5 | super.key, 6 | required this.title, 7 | required this.actions, 8 | }); 9 | 10 | final List? actions; 11 | final String title; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return SliverAppBar( 16 | titleSpacing: 22, 17 | scrolledUnderElevation: 0, 18 | backgroundColor: Theme.of(context).scaffoldBackgroundColor, 19 | title: Text( 20 | title, 21 | style: TextStyle( 22 | color: Theme.of(context).colorScheme.onPrimaryContainer, 23 | fontWeight: FontWeight.w500, 24 | ), 25 | ), 26 | pinned: true, 27 | actions: actions, 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/widgets/library/library_header_old.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class MLibraryHeader extends StatelessWidget { 4 | const MLibraryHeader({ 5 | super.key, 6 | required this.title, 7 | required this.actions, 8 | }); 9 | 10 | final List? actions; 11 | final String title; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return SliverAppBar( 16 | scrolledUnderElevation: 0, 17 | backgroundColor: Theme.of(context).scaffoldBackgroundColor, 18 | flexibleSpace: FlexibleSpaceBar( 19 | centerTitle: false, 20 | titlePadding: const EdgeInsetsDirectional.only( 21 | start: 16, 22 | bottom: 16, 23 | ), 24 | title: Text( 25 | title, 26 | style: TextStyle( 27 | color: Theme.of(context).colorScheme.onPrimaryContainer, 28 | fontSize: 25, 29 | fontWeight: FontWeight.w500, 30 | ), 31 | ), 32 | ), 33 | pinned: true, 34 | expandedHeight: 80, 35 | collapsedHeight: 60, 36 | actions: actions, 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/widgets/library/library_listtile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class MLibraryListTile extends StatelessWidget { 4 | const MLibraryListTile({ 5 | super.key, 6 | required this.icon, 7 | required this.title, 8 | required this.onTap, 9 | }); 10 | 11 | final IconData? icon; 12 | final String title; 13 | final Function()? onTap; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return ListTile( 18 | leading: Icon(icon), 19 | title: Text(title), 20 | trailing: const Icon(Icons.keyboard_arrow_right), 21 | onTap: onTap, 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/widgets/library/library_title.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class MLibraryTitle extends StatelessWidget { 4 | const MLibraryTitle({ 5 | super.key, 6 | required this.title, 7 | this.trailing = const SizedBox(), 8 | }); 9 | 10 | final String title; 11 | final Widget trailing; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return SliverToBoxAdapter( 16 | child: Padding( 17 | padding: const EdgeInsets.symmetric(horizontal: 16), 18 | child: Row( 19 | children: [ 20 | Expanded( 21 | child: Text( 22 | title, 23 | style: const TextStyle( 24 | fontSize: 20, 25 | fontWeight: FontWeight.w500, 26 | ), 27 | ), 28 | ), 29 | trailing, 30 | ], 31 | ), 32 | ), 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/widgets/library/library_title_old.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class MLibraryTitle extends StatelessWidget { 4 | const MLibraryTitle({ 5 | super.key, 6 | required this.title, 7 | this.trailing = const SizedBox(), 8 | }); 9 | 10 | final String title; 11 | final Widget trailing; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return SliverToBoxAdapter( 16 | child: Padding( 17 | padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), 18 | child: Row( 19 | children: [ 20 | Expanded( 21 | child: Text( 22 | title, 23 | style: const TextStyle( 24 | fontSize: 25, 25 | fontWeight: FontWeight.w500, 26 | ), 27 | ), 28 | ), 29 | trailing, 30 | ], 31 | ), 32 | ), 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/widgets/loading_holder.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class MLoadingPlaceHolder extends StatelessWidget { 4 | const MLoadingPlaceHolder({super.key}); 5 | 6 | @override 7 | Widget build(BuildContext context) { 8 | return const SliverToBoxAdapter( 9 | child: Center( 10 | heightFactor: 10, 11 | child: CircularProgressIndicator(), 12 | ), 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/widgets/menu/menu_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:playboy/backend/utils/l10n_utils.dart'; 3 | 4 | class MenuButton extends StatelessWidget { 5 | const MenuButton({ 6 | super.key, 7 | required this.menuChildren, 8 | this.style, 9 | this.constraints, 10 | this.icon = Icons.more_vert, 11 | this.tooltip, 12 | }); 13 | 14 | final List menuChildren; 15 | final ButtonStyle? style; 16 | final BoxConstraints? constraints; 17 | final IconData icon; 18 | final String? tooltip; 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return MenuAnchor( 23 | builder: (context, controller, child) { 24 | return IconButton( 25 | style: style, 26 | constraints: constraints, 27 | tooltip: tooltip ?? '菜单'.l10n, 28 | onPressed: () { 29 | if (controller.isOpen) { 30 | controller.close(); 31 | } else { 32 | controller.open(); 33 | } 34 | }, 35 | icon: Icon(icon), 36 | ); 37 | }, 38 | menuChildren: menuChildren, 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/widgets/menu/menu_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class MMenuItem extends StatelessWidget { 4 | final IconData icon; 5 | final String label; 6 | final Function()? onPressed; 7 | final String keymap; 8 | 9 | const MMenuItem({ 10 | super.key, 11 | required this.icon, 12 | required this.label, 13 | required this.onPressed, 14 | this.keymap = '', 15 | }); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | late final colorScheme = Theme.of(context).colorScheme; 20 | return SizedBox( 21 | height: 34, 22 | child: MenuItemButton( 23 | leadingIcon: Icon( 24 | icon, 25 | size: 18, 26 | ), 27 | onPressed: onPressed, 28 | trailingIcon: Padding( 29 | padding: const EdgeInsets.symmetric(horizontal: 6), 30 | child: Text( 31 | keymap, 32 | style: TextStyle( 33 | color: colorScheme.onSurface.withValues(alpha: 0.4), 34 | ), 35 | ), 36 | ), 37 | child: Padding( 38 | padding: const EdgeInsets.symmetric(horizontal: 6), 39 | child: Text(label), 40 | ), 41 | ), 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/widgets/path_setting_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class PathSettingCard extends StatelessWidget { 4 | const PathSettingCard({ 5 | super.key, 6 | required this.path, 7 | required this.actions, 8 | }); 9 | 10 | final String path; 11 | final List actions; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | late final colorScheme = Theme.of(context).colorScheme; 16 | return Padding( 17 | padding: const EdgeInsets.symmetric(horizontal: 0), 18 | child: Card( 19 | elevation: 0, 20 | shape: RoundedRectangleBorder( 21 | borderRadius: BorderRadius.circular(14), 22 | ), 23 | color: colorScheme.secondaryContainer.withValues(alpha: 0.4), 24 | child: SizedBox( 25 | height: 50, 26 | child: Row( 27 | children: [ 28 | const SizedBox(width: 10), 29 | Expanded( 30 | child: Text( 31 | path, 32 | style: TextStyle( 33 | overflow: TextOverflow.ellipsis, 34 | color: colorScheme.onSecondaryContainer, 35 | ), 36 | ), 37 | ), 38 | ...actions, 39 | const SizedBox(width: 10), 40 | ], 41 | ), 42 | ), 43 | ), 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/widgets/player_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:playboy/backend/models/playitem.dart'; 3 | import 'package:playboy/widgets/cover.dart'; 4 | 5 | class PlayerListCard extends StatelessWidget { 6 | const PlayerListCard({ 7 | super.key, 8 | required this.info, 9 | required this.isPlaying, 10 | }); 11 | final PlayItem info; 12 | final bool isPlaying; 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | late final colorScheme = Theme.of(context).colorScheme; 17 | return Row( 18 | children: [ 19 | Padding( 20 | padding: const EdgeInsets.all(6), 21 | child: MCover( 22 | icon: Icons.music_note, 23 | iconSize: 20, 24 | borderRadius: 10, 25 | cover: info.cover, 26 | aspectRatio: 1, 27 | colorScheme: colorScheme, 28 | ), 29 | ), 30 | const SizedBox(width: 10), 31 | Expanded( 32 | child: Text( 33 | info.title, 34 | maxLines: 2, 35 | overflow: TextOverflow.ellipsis, 36 | style: TextStyle( 37 | fontSize: 14, 38 | color: isPlaying ? colorScheme.primary : null, 39 | ), 40 | ), 41 | ), 42 | ], 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/widgets/playlist_picker.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:playboy/backend/models/playlist_item.dart'; 5 | 6 | class PlaylistPickerItem extends StatelessWidget { 7 | const PlaylistPickerItem({super.key, required this.info}); 8 | final PlaylistItem info; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | late final colorScheme = Theme.of(context).colorScheme; 13 | return Row( 14 | children: [ 15 | Padding( 16 | padding: const EdgeInsets.all(6), 17 | child: AspectRatio( 18 | aspectRatio: 1, 19 | child: !File(info.cover).existsSync() 20 | ? Container( 21 | width: double.infinity, 22 | decoration: BoxDecoration( 23 | borderRadius: BorderRadius.circular(16), 24 | color: colorScheme.secondaryContainer, 25 | ), 26 | child: Icon( 27 | Icons.playlist_play_rounded, 28 | color: colorScheme.secondary, 29 | size: 30, 30 | ), 31 | ) 32 | : Ink( 33 | width: double.infinity, 34 | decoration: BoxDecoration( 35 | borderRadius: BorderRadius.circular(16), 36 | color: colorScheme.secondaryContainer, 37 | image: DecorationImage( 38 | fit: BoxFit.cover, 39 | image: FileImage( 40 | File(info.cover), 41 | ), 42 | ), 43 | ), 44 | ), 45 | ), 46 | ), 47 | const SizedBox( 48 | width: 10, 49 | ), 50 | Expanded( 51 | child: Text( 52 | info.title, 53 | style: const TextStyle(fontSize: 18), 54 | )), 55 | ], 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/widgets/readme.md: -------------------------------------------------------------------------------- 1 | # Playboy common UI lib 2 | 3 | These widgets aims to make the application have the unified appearance. 4 | 5 | To use them in `CustomScrollView`, call `.toSliver()` after consturct them. 6 | 7 | ## Contents -------------------------------------------------------------------------------- /lib/widgets/settings_message_box.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SettingsMessageBox extends StatelessWidget { 4 | const SettingsMessageBox({ 5 | super.key, 6 | required this.message, 7 | this.trailing, 8 | }); 9 | 10 | final String message; 11 | final Widget? trailing; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | late final ColorScheme colorScheme = Theme.of(context).colorScheme; 16 | return Container( 17 | decoration: ShapeDecoration( 18 | color: colorScheme.primaryContainer.withValues(alpha: 0.2), 19 | shape: RoundedRectangleBorder( 20 | borderRadius: BorderRadius.circular(14), 21 | ), 22 | ), 23 | child: SizedBox( 24 | height: 50, 25 | child: Row( 26 | children: [ 27 | Expanded( 28 | child: Container( 29 | alignment: Alignment.centerLeft, 30 | padding: const EdgeInsets.symmetric(horizontal: 16), 31 | child: Text( 32 | message, 33 | style: TextStyle(color: colorScheme.onPrimaryContainer), 34 | ), 35 | ), 36 | ), 37 | trailing ?? const SizedBox(), 38 | const SizedBox(width: 6), 39 | ], 40 | ), 41 | ), 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | -------------------------------------------------------------------------------- /linux/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Project-level configuration. 2 | cmake_minimum_required(VERSION 3.10) 3 | project(runner LANGUAGES CXX) 4 | 5 | # The name of the executable created for the application. Change this to change 6 | # the on-disk name of your application. 7 | set(BINARY_NAME "playboy") 8 | # The unique GTK application identifier for this application. See: 9 | # https://wiki.gnome.org/HowDoI/ChooseApplicationID 10 | set(APPLICATION_ID "com.example.playboy") 11 | 12 | # Explicitly opt in to modern CMake behaviors to avoid warnings with recent 13 | # versions of CMake. 14 | cmake_policy(SET CMP0063 NEW) 15 | 16 | # Load bundled libraries from the lib/ directory relative to the binary. 17 | set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") 18 | 19 | # Root filesystem for cross-building. 20 | if(FLUTTER_TARGET_PLATFORM_SYSROOT) 21 | set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) 22 | set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) 23 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) 24 | set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) 25 | set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) 26 | set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) 27 | endif() 28 | 29 | # Define build configuration options. 30 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 31 | set(CMAKE_BUILD_TYPE "Debug" CACHE 32 | STRING "Flutter build mode" FORCE) 33 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 34 | "Debug" "Profile" "Release") 35 | endif() 36 | 37 | # Compilation settings that should be applied to most targets. 38 | # 39 | # Be cautious about adding new options here, as plugins use this function by 40 | # default. In most cases, you should add new options to specific targets instead 41 | # of modifying this function. 42 | function(APPLY_STANDARD_SETTINGS TARGET) 43 | target_compile_features(${TARGET} PUBLIC cxx_std_14) 44 | target_compile_options(${TARGET} PRIVATE -Wall -Werror) 45 | target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") 46 | target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") 47 | endfunction() 48 | 49 | # Flutter library and tool build rules. 50 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") 51 | add_subdirectory(${FLUTTER_MANAGED_DIR}) 52 | 53 | # System-level dependencies. 54 | find_package(PkgConfig REQUIRED) 55 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) 56 | 57 | add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") 58 | 59 | # Define the application target. To change its name, change BINARY_NAME above, 60 | # not the value here, or `flutter run` will no longer work. 61 | # 62 | # Any new source files that you add to the application should be added here. 63 | add_executable(${BINARY_NAME} 64 | "main.cc" 65 | "my_application.cc" 66 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 67 | ) 68 | 69 | # Apply the standard set of build settings. This can be removed for applications 70 | # that need different build settings. 71 | apply_standard_settings(${BINARY_NAME}) 72 | 73 | # Add dependency libraries. Add any application-specific dependencies here. 74 | target_link_libraries(${BINARY_NAME} PRIVATE flutter) 75 | target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) 76 | 77 | # Run the Flutter tool portions of the build. This must not be removed. 78 | add_dependencies(${BINARY_NAME} flutter_assemble) 79 | 80 | # Only the install-generated bundle's copy of the executable will launch 81 | # correctly, since the resources must in the right relative locations. To avoid 82 | # people trying to run the unbundled copy, put it in a subdirectory instead of 83 | # the default top-level location. 84 | set_target_properties(${BINARY_NAME} 85 | PROPERTIES 86 | RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" 87 | ) 88 | 89 | 90 | # Generated plugin build rules, which manage building the plugins and adding 91 | # them to the application. 92 | include(flutter/generated_plugins.cmake) 93 | 94 | 95 | # === Installation === 96 | # By default, "installing" just makes a relocatable bundle in the build 97 | # directory. 98 | set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") 99 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 100 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) 101 | endif() 102 | 103 | # Start with a clean build bundle directory every time. 104 | install(CODE " 105 | file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") 106 | " COMPONENT Runtime) 107 | 108 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") 109 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") 110 | 111 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" 112 | COMPONENT Runtime) 113 | 114 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 115 | COMPONENT Runtime) 116 | 117 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 118 | COMPONENT Runtime) 119 | 120 | foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) 121 | install(FILES "${bundled_library}" 122 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 123 | COMPONENT Runtime) 124 | endforeach(bundled_library) 125 | 126 | # Copy the native assets provided by the build.dart from all packages. 127 | set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") 128 | install(DIRECTORY "${NATIVE_ASSETS_DIR}" 129 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 130 | COMPONENT Runtime) 131 | 132 | # Fully re-copy the assets directory on each build to avoid having stale files 133 | # from a previous install. 134 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets") 135 | install(CODE " 136 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") 137 | " COMPONENT Runtime) 138 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" 139 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) 140 | 141 | # Install the AOT library on non-Debug builds only. 142 | if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") 143 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 144 | COMPONENT Runtime) 145 | endif() 146 | -------------------------------------------------------------------------------- /linux/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file controls Flutter-level build steps. It should not be edited. 2 | cmake_minimum_required(VERSION 3.10) 3 | 4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 5 | 6 | # Configuration provided via flutter tool. 7 | include(${EPHEMERAL_DIR}/generated_config.cmake) 8 | 9 | # TODO: Move the rest of this into files in ephemeral. See 10 | # https://github.com/flutter/flutter/issues/57146. 11 | 12 | # Serves the same purpose as list(TRANSFORM ... PREPEND ...), 13 | # which isn't available in 3.10. 14 | function(list_prepend LIST_NAME PREFIX) 15 | set(NEW_LIST "") 16 | foreach(element ${${LIST_NAME}}) 17 | list(APPEND NEW_LIST "${PREFIX}${element}") 18 | endforeach(element) 19 | set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) 20 | endfunction() 21 | 22 | # === Flutter Library === 23 | # System-level dependencies. 24 | find_package(PkgConfig REQUIRED) 25 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) 26 | pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) 27 | pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) 28 | 29 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") 30 | 31 | # Published to parent scope for install step. 32 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 33 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 34 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 35 | set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) 36 | 37 | list(APPEND FLUTTER_LIBRARY_HEADERS 38 | "fl_basic_message_channel.h" 39 | "fl_binary_codec.h" 40 | "fl_binary_messenger.h" 41 | "fl_dart_project.h" 42 | "fl_engine.h" 43 | "fl_json_message_codec.h" 44 | "fl_json_method_codec.h" 45 | "fl_message_codec.h" 46 | "fl_method_call.h" 47 | "fl_method_channel.h" 48 | "fl_method_codec.h" 49 | "fl_method_response.h" 50 | "fl_plugin_registrar.h" 51 | "fl_plugin_registry.h" 52 | "fl_standard_message_codec.h" 53 | "fl_standard_method_codec.h" 54 | "fl_string_codec.h" 55 | "fl_value.h" 56 | "fl_view.h" 57 | "flutter_linux.h" 58 | ) 59 | list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") 60 | add_library(flutter INTERFACE) 61 | target_include_directories(flutter INTERFACE 62 | "${EPHEMERAL_DIR}" 63 | ) 64 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") 65 | target_link_libraries(flutter INTERFACE 66 | PkgConfig::GTK 67 | PkgConfig::GLIB 68 | PkgConfig::GIO 69 | ) 70 | add_dependencies(flutter flutter_assemble) 71 | 72 | # === Flutter tool backend === 73 | # _phony_ is a non-existent file to force this command to run every time, 74 | # since currently there's no way to get a full input/output list from the 75 | # flutter tool. 76 | add_custom_command( 77 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 78 | ${CMAKE_CURRENT_BINARY_DIR}/_phony_ 79 | COMMAND ${CMAKE_COMMAND} -E env 80 | ${FLUTTER_TOOL_ENVIRONMENT} 81 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" 82 | ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} 83 | VERBATIM 84 | ) 85 | add_custom_target(flutter_assemble DEPENDS 86 | "${FLUTTER_LIBRARY}" 87 | ${FLUTTER_LIBRARY_HEADERS} 88 | ) 89 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | void fl_register_plugins(FlPluginRegistry* registry) { 16 | g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = 17 | fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); 18 | media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); 19 | g_autoptr(FlPluginRegistrar) media_kit_video_registrar = 20 | fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitVideoPlugin"); 21 | media_kit_video_plugin_register_with_registrar(media_kit_video_registrar); 22 | g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar = 23 | fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin"); 24 | screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar); 25 | g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = 26 | fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); 27 | url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); 28 | g_autoptr(FlPluginRegistrar) window_manager_registrar = 29 | fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); 30 | window_manager_plugin_register_with_registrar(window_manager_registrar); 31 | } 32 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void fl_register_plugins(FlPluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | media_kit_libs_linux 7 | media_kit_video 8 | screen_retriever_linux 9 | url_launcher_linux 10 | window_manager 11 | ) 12 | 13 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 14 | libmpv_dart 15 | whisper4dart 16 | ) 17 | 18 | set(PLUGIN_BUNDLED_LIBRARIES) 19 | 20 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 21 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) 22 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 23 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 24 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 25 | endforeach(plugin) 26 | 27 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 28 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) 29 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 30 | endforeach(ffi_plugin) 31 | -------------------------------------------------------------------------------- /linux/main.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | int main(int argc, char** argv) { 4 | g_autoptr(MyApplication) app = my_application_new(); 5 | return g_application_run(G_APPLICATION(app), argc, argv); 6 | } 7 | -------------------------------------------------------------------------------- /linux/my_application.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | #include 4 | #ifdef GDK_WINDOWING_X11 5 | #include 6 | #endif 7 | 8 | #include "flutter/generated_plugin_registrant.h" 9 | 10 | struct _MyApplication { 11 | GtkApplication parent_instance; 12 | char** dart_entrypoint_arguments; 13 | }; 14 | 15 | G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) 16 | 17 | // Implements GApplication::activate. 18 | static void my_application_activate(GApplication* application) { 19 | MyApplication* self = MY_APPLICATION(application); 20 | GtkWindow* window = 21 | GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); 22 | 23 | // Use a header bar when running in GNOME as this is the common style used 24 | // by applications and is the setup most users will be using (e.g. Ubuntu 25 | // desktop). 26 | // If running on X and not using GNOME then just use a traditional title bar 27 | // in case the window manager does more exotic layout, e.g. tiling. 28 | // If running on Wayland assume the header bar will work (may need changing 29 | // if future cases occur). 30 | gboolean use_header_bar = TRUE; 31 | #ifdef GDK_WINDOWING_X11 32 | GdkScreen* screen = gtk_window_get_screen(window); 33 | if (GDK_IS_X11_SCREEN(screen)) { 34 | const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); 35 | if (g_strcmp0(wm_name, "GNOME Shell") != 0) { 36 | use_header_bar = FALSE; 37 | } 38 | } 39 | #endif 40 | if (use_header_bar) { 41 | GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); 42 | gtk_widget_show(GTK_WIDGET(header_bar)); 43 | gtk_header_bar_set_title(header_bar, "playboy"); 44 | gtk_header_bar_set_show_close_button(header_bar, TRUE); 45 | gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); 46 | } else { 47 | gtk_window_set_title(window, "playboy"); 48 | } 49 | 50 | gtk_window_set_default_size(window, 900, 700); 51 | gtk_widget_show(GTK_WIDGET(window)); 52 | 53 | g_autoptr(FlDartProject) project = fl_dart_project_new(); 54 | fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); 55 | 56 | FlView* view = fl_view_new(project); 57 | gtk_widget_show(GTK_WIDGET(view)); 58 | gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); 59 | 60 | fl_register_plugins(FL_PLUGIN_REGISTRY(view)); 61 | 62 | gtk_widget_grab_focus(GTK_WIDGET(view)); 63 | } 64 | 65 | // Implements GApplication::local_command_line. 66 | static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { 67 | MyApplication* self = MY_APPLICATION(application); 68 | // Strip out the first argument as it is the binary name. 69 | self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); 70 | 71 | g_autoptr(GError) error = nullptr; 72 | if (!g_application_register(application, nullptr, &error)) { 73 | g_warning("Failed to register: %s", error->message); 74 | *exit_status = 1; 75 | return TRUE; 76 | } 77 | 78 | g_application_activate(application); 79 | *exit_status = 0; 80 | 81 | return TRUE; 82 | } 83 | 84 | // Implements GObject::dispose. 85 | static void my_application_dispose(GObject* object) { 86 | MyApplication* self = MY_APPLICATION(object); 87 | g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); 88 | G_OBJECT_CLASS(my_application_parent_class)->dispose(object); 89 | } 90 | 91 | static void my_application_class_init(MyApplicationClass* klass) { 92 | G_APPLICATION_CLASS(klass)->activate = my_application_activate; 93 | G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; 94 | G_OBJECT_CLASS(klass)->dispose = my_application_dispose; 95 | } 96 | 97 | static void my_application_init(MyApplication* self) {} 98 | 99 | MyApplication* my_application_new() { 100 | return MY_APPLICATION(g_object_new(my_application_get_type(), 101 | "application-id", APPLICATION_ID, 102 | "flags", G_APPLICATION_NON_UNIQUE, 103 | nullptr)); 104 | } 105 | -------------------------------------------------------------------------------- /linux/my_application.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_MY_APPLICATION_H_ 2 | #define FLUTTER_MY_APPLICATION_H_ 3 | 4 | #include 5 | 6 | G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, 7 | GtkApplication) 8 | 9 | /** 10 | * my_application_new: 11 | * 12 | * Creates a new Flutter-based application. 13 | * 14 | * Returns: a new #MyApplication. 15 | */ 16 | MyApplication* my_application_new(); 17 | 18 | #endif // FLUTTER_MY_APPLICATION_H_ 19 | -------------------------------------------------------------------------------- /macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/dgph 7 | **/xcuserdata/ 8 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import file_picker 9 | import media_kit_libs_macos_video 10 | import media_kit_video 11 | import package_info_plus 12 | import path_provider_foundation 13 | import screen_retriever_macos 14 | import url_launcher_macos 15 | import wakelock_plus 16 | import window_manager 17 | 18 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 19 | FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) 20 | MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) 21 | MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) 22 | FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) 23 | PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) 24 | ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) 25 | UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) 26 | WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) 27 | WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) 28 | } 29 | -------------------------------------------------------------------------------- /macos/Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '10.14.6' 2 | 3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 | 6 | project 'Runner', { 7 | 'Debug' => :debug, 8 | 'Profile' => :release, 9 | 'Release' => :release, 10 | } 11 | 12 | def flutter_root 13 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) 14 | unless File.exist?(generated_xcode_build_settings_path) 15 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" 16 | end 17 | 18 | File.foreach(generated_xcode_build_settings_path) do |line| 19 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 20 | return matches[1].strip if matches 21 | end 22 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" 23 | end 24 | 25 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 26 | 27 | flutter_macos_podfile_setup 28 | 29 | target 'Runner' do 30 | use_frameworks! 31 | use_modular_headers! 32 | 33 | flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) 34 | target 'RunnerTests' do 35 | inherit! :search_paths 36 | end 37 | end 38 | 39 | post_install do |installer| 40 | installer.pods_project.targets.each do |target| 41 | flutter_additional_macos_build_settings(target) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /macos/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - file_picker (0.0.1): 3 | - FlutterMacOS 4 | - FlutterMacOS (1.0.0) 5 | - media_kit_libs_video (1.0.4): 6 | - FlutterMacOS 7 | - media_kit_video (0.0.1): 8 | - FlutterMacOS 9 | - path_provider_foundation (0.0.1): 10 | - Flutter 11 | - FlutterMacOS 12 | - screen_retriever_macos (0.0.1): 13 | - FlutterMacOS 14 | - url_launcher_macos (0.0.1): 15 | - FlutterMacOS 16 | - window_manager (0.2.0): 17 | - FlutterMacOS 18 | 19 | DEPENDENCIES: 20 | - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`) 21 | - FlutterMacOS (from `Flutter/ephemeral`) 22 | - media_kit_libs_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_video/macos`) 23 | - media_kit_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos`) 24 | - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) 25 | - screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`) 26 | - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) 27 | - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) 28 | 29 | EXTERNAL SOURCES: 30 | file_picker: 31 | :path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos 32 | FlutterMacOS: 33 | :path: Flutter/ephemeral 34 | media_kit_libs_video: 35 | :path: Flutter/ephemeral/.symlinks/plugins/media_kit_libs_video/macos 36 | media_kit_video: 37 | :path: Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos 38 | path_provider_foundation: 39 | :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin 40 | screen_retriever_macos: 41 | :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos 42 | url_launcher_macos: 43 | :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos 44 | window_manager: 45 | :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos 46 | 47 | SPEC CHECKSUMS: 48 | file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a 49 | FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 50 | media_kit_libs_video: 85a23e549b5f480e72cae3e5634b5514bc692f65 51 | media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758 52 | path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 53 | screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f 54 | url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 55 | window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c 56 | 57 | PODFILE CHECKSUM: 16208599a12443d53889ba2270a4985981cfb204 58 | 59 | COCOAPODS: 1.16.2 60 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 64 | 66 | 72 | 73 | 74 | 75 | 81 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @main 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | 10 | override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { 11 | return true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "app_icon_16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "app_icon_32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "app_icon_32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "app_icon_64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "app_icon_128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "app_icon_256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "app_icon_256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "app_icon_512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "app_icon_512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "app_icon_1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = Playboy 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = org.playboy.player 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2024 org.playboy. All rights reserved. 15 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.assets.movies.read-write 8 | 9 | com.apple.security.assets.music.read-write 10 | 11 | com.apple.security.assets.pictures.read-write 12 | 13 | com.apple.security.cs.allow-jit 14 | 15 | com.apple.security.files.downloads.read-write 16 | 17 | com.apple.security.files.user-selected.read-write 18 | 19 | com.apple.security.network.server 20 | 21 | com.apple.security.network.client 22 | 23 | com.apple.security.app-sandbox 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | $(PRODUCT_COPYRIGHT) 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | import window_manager 4 | 5 | class MainFlutterWindow: NSWindow { 6 | override func awakeFromNib() { 7 | let flutterViewController = FlutterViewController() 8 | let windowFrame = self.frame 9 | self.contentViewController = flutterViewController 10 | self.setFrame(windowFrame, display: true) 11 | 12 | RegisterGeneratedPlugins(registry: flutterViewController) 13 | 14 | super.awakeFromNib() 15 | } 16 | override public func order(_ place: NSWindow.OrderingMode, relativeTo otherWin: Int) { 17 | super.order(place, relativeTo: otherWin) 18 | hiddenWindowAtLaunch() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.assets.movies.read-write 8 | 9 | com.apple.security.assets.music.read-write 10 | 11 | com.apple.security.assets.pictures.read-write 12 | 13 | com.apple.security.files.downloads.read-write 14 | 15 | com.apple.security.files.user-selected.read-write 16 | 17 | com.apple.security.network.client 18 | 19 | com.apple.security.app-sandbox 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /macos/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import FlutterMacOS 2 | import Cocoa 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: playboy 2 | description: A flutter video player. 3 | publish_to: 'none' 4 | 5 | version: 0.25.3 6 | 7 | environment: 8 | sdk: '>=3.1.3 <4.0.0' 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | 14 | path: ^1.8.3 15 | path_provider: ^2.1.1 16 | file_picker: ^8.3.1 17 | json_annotation: ^4.8.1 18 | url_launcher: ^6.2.5 19 | wakelock_plus: ^1.2.11 20 | window_manager: 21 | git: 22 | url: https://github.com/YuiHrsw/window_manager.git 23 | path: packages/window_manager 24 | ref: windows_fullscreen_fix 25 | 26 | 27 | # Media 28 | whisper4dart: ^0.1.4 29 | # libmpv_dart: ^0.0.4 30 | 31 | media_kit: 32 | git: 33 | url: https://github.com/Playboy-Player/media_cat.git 34 | path: media_kit 35 | ref: 445c94d0a6a8076fef8f424fabcfd53709bb2d5e 36 | media_kit_video: 37 | git: 38 | url: https://github.com/Playboy-Player/media_cat.git 39 | path: media_kit_video 40 | ref: 445c94d0a6a8076fef8f424fabcfd53709bb2d5e 41 | media_kit_libs_video: 42 | git: 43 | url: https://github.com/Playboy-Player/media_cat.git 44 | path: libs/universal/media_kit_libs_video 45 | ref: 445c94d0a6a8076fef8f424fabcfd53709bb2d5e 46 | 47 | dev_dependencies: 48 | flutter_test: 49 | sdk: flutter 50 | flutter_lints: ^3.0.1 51 | build_runner: ^2.3.3 52 | json_serializable: ^6.7.1 53 | 54 | flutter: 55 | assets: 56 | - res/contributors/ 57 | - l10n/ 58 | uses-material-design: true 59 | generate: true 60 | -------------------------------------------------------------------------------- /res/contributors/KernelInterrupt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/res/contributors/KernelInterrupt.jpg -------------------------------------------------------------------------------- /res/contributors/rubbrt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/res/contributors/rubbrt.jpg -------------------------------------------------------------------------------- /res/contributors/yui.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/res/contributors/yui.jpg -------------------------------------------------------------------------------- /res/images/icon512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/res/images/icon512.png -------------------------------------------------------------------------------- /res/images/icon_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/res/images/icon_bg.png -------------------------------------------------------------------------------- /res/images/icon_fg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/res/images/icon_fg.png -------------------------------------------------------------------------------- /res/images/icon_mono.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/res/images/icon_mono.png -------------------------------------------------------------------------------- /screenshots/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/screenshots/screenshot1.png -------------------------------------------------------------------------------- /screenshots/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/screenshots/screenshot2.png -------------------------------------------------------------------------------- /screenshots/screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/screenshots/screenshot3.png -------------------------------------------------------------------------------- /screenshots/screenshot4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/screenshots/screenshot4.png -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "latest": "Beta 2025.4" 3 | } -------------------------------------------------------------------------------- /windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral/ 2 | 3 | # Visual Studio user-specific files. 4 | *.suo 5 | *.user 6 | *.userosscache 7 | *.sln.docstates 8 | 9 | # Visual Studio build-related files. 10 | x64/ 11 | x86/ 12 | 13 | # Visual Studio cache files 14 | # files ending in .cache can be ignored 15 | *.[Cc]ache 16 | # but keep track of directories ending in .cache 17 | !*.[Cc]ache/ 18 | -------------------------------------------------------------------------------- /windows/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Project-level configuration. 2 | cmake_minimum_required(VERSION 3.14) 3 | project(playboy LANGUAGES CXX) 4 | 5 | # The name of the executable created for the application. Change this to change 6 | # the on-disk name of your application. 7 | set(BINARY_NAME "playboy") 8 | 9 | # Explicitly opt in to modern CMake behaviors to avoid warnings with recent 10 | # versions of CMake. 11 | cmake_policy(VERSION 3.14...3.25) 12 | 13 | # Define build configuration option. 14 | get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) 15 | if(IS_MULTICONFIG) 16 | set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" 17 | CACHE STRING "" FORCE) 18 | else() 19 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 20 | set(CMAKE_BUILD_TYPE "Debug" CACHE 21 | STRING "Flutter build mode" FORCE) 22 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 23 | "Debug" "Profile" "Release") 24 | endif() 25 | endif() 26 | # Define settings for the Profile build mode. 27 | set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") 28 | set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") 29 | set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") 30 | set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") 31 | 32 | # Use Unicode for all projects. 33 | add_definitions(-DUNICODE -D_UNICODE) 34 | 35 | # Compilation settings that should be applied to most targets. 36 | # 37 | # Be cautious about adding new options here, as plugins use this function by 38 | # default. In most cases, you should add new options to specific targets instead 39 | # of modifying this function. 40 | function(APPLY_STANDARD_SETTINGS TARGET) 41 | target_compile_features(${TARGET} PUBLIC cxx_std_17) 42 | target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") 43 | target_compile_options(${TARGET} PRIVATE /EHsc) 44 | target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") 45 | target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") 46 | endfunction() 47 | 48 | # Flutter library and tool build rules. 49 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") 50 | add_subdirectory(${FLUTTER_MANAGED_DIR}) 51 | 52 | # Application build; see runner/CMakeLists.txt. 53 | add_subdirectory("runner") 54 | 55 | 56 | # Generated plugin build rules, which manage building the plugins and adding 57 | # them to the application. 58 | include(flutter/generated_plugins.cmake) 59 | 60 | 61 | # === Installation === 62 | # Support files are copied into place next to the executable, so that it can 63 | # run in place. This is done instead of making a separate bundle (as on Linux) 64 | # so that building and running from within Visual Studio will work. 65 | set(BUILD_BUNDLE_DIR "$") 66 | # Make the "install" step default, as it's required to run. 67 | set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) 68 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 69 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) 70 | endif() 71 | 72 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") 73 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") 74 | 75 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" 76 | COMPONENT Runtime) 77 | 78 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 79 | COMPONENT Runtime) 80 | 81 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 82 | COMPONENT Runtime) 83 | 84 | if(PLUGIN_BUNDLED_LIBRARIES) 85 | install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" 86 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 87 | COMPONENT Runtime) 88 | endif() 89 | 90 | # Copy the native assets provided by the build.dart from all packages. 91 | set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") 92 | install(DIRECTORY "${NATIVE_ASSETS_DIR}" 93 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 94 | COMPONENT Runtime) 95 | 96 | # Fully re-copy the assets directory on each build to avoid having stale files 97 | # from a previous install. 98 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets") 99 | install(CODE " 100 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") 101 | " COMPONENT Runtime) 102 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" 103 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) 104 | 105 | # Install the AOT library on non-Debug builds only. 106 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 107 | CONFIGURATIONS Profile;Release 108 | COMPONENT Runtime) 109 | -------------------------------------------------------------------------------- /windows/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file controls Flutter-level build steps. It should not be edited. 2 | cmake_minimum_required(VERSION 3.14) 3 | 4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 5 | 6 | # Configuration provided via flutter tool. 7 | include(${EPHEMERAL_DIR}/generated_config.cmake) 8 | 9 | # TODO: Move the rest of this into files in ephemeral. See 10 | # https://github.com/flutter/flutter/issues/57146. 11 | set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") 12 | 13 | # Set fallback configurations for older versions of the flutter tool. 14 | if (NOT DEFINED FLUTTER_TARGET_PLATFORM) 15 | set(FLUTTER_TARGET_PLATFORM "windows-x64") 16 | endif() 17 | 18 | # === Flutter Library === 19 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") 20 | 21 | # Published to parent scope for install step. 22 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 23 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 24 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 25 | set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) 26 | 27 | list(APPEND FLUTTER_LIBRARY_HEADERS 28 | "flutter_export.h" 29 | "flutter_windows.h" 30 | "flutter_messenger.h" 31 | "flutter_plugin_registrar.h" 32 | "flutter_texture_registrar.h" 33 | ) 34 | list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") 35 | add_library(flutter INTERFACE) 36 | target_include_directories(flutter INTERFACE 37 | "${EPHEMERAL_DIR}" 38 | ) 39 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") 40 | add_dependencies(flutter flutter_assemble) 41 | 42 | # === Wrapper === 43 | list(APPEND CPP_WRAPPER_SOURCES_CORE 44 | "core_implementations.cc" 45 | "standard_codec.cc" 46 | ) 47 | list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") 48 | list(APPEND CPP_WRAPPER_SOURCES_PLUGIN 49 | "plugin_registrar.cc" 50 | ) 51 | list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") 52 | list(APPEND CPP_WRAPPER_SOURCES_APP 53 | "flutter_engine.cc" 54 | "flutter_view_controller.cc" 55 | ) 56 | list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") 57 | 58 | # Wrapper sources needed for a plugin. 59 | add_library(flutter_wrapper_plugin STATIC 60 | ${CPP_WRAPPER_SOURCES_CORE} 61 | ${CPP_WRAPPER_SOURCES_PLUGIN} 62 | ) 63 | apply_standard_settings(flutter_wrapper_plugin) 64 | set_target_properties(flutter_wrapper_plugin PROPERTIES 65 | POSITION_INDEPENDENT_CODE ON) 66 | set_target_properties(flutter_wrapper_plugin PROPERTIES 67 | CXX_VISIBILITY_PRESET hidden) 68 | target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) 69 | target_include_directories(flutter_wrapper_plugin PUBLIC 70 | "${WRAPPER_ROOT}/include" 71 | ) 72 | add_dependencies(flutter_wrapper_plugin flutter_assemble) 73 | 74 | # Wrapper sources needed for the runner. 75 | add_library(flutter_wrapper_app STATIC 76 | ${CPP_WRAPPER_SOURCES_CORE} 77 | ${CPP_WRAPPER_SOURCES_APP} 78 | ) 79 | apply_standard_settings(flutter_wrapper_app) 80 | target_link_libraries(flutter_wrapper_app PUBLIC flutter) 81 | target_include_directories(flutter_wrapper_app PUBLIC 82 | "${WRAPPER_ROOT}/include" 83 | ) 84 | add_dependencies(flutter_wrapper_app flutter_assemble) 85 | 86 | # === Flutter tool backend === 87 | # _phony_ is a non-existent file to force this command to run every time, 88 | # since currently there's no way to get a full input/output list from the 89 | # flutter tool. 90 | set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") 91 | set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) 92 | add_custom_command( 93 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 94 | ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} 95 | ${CPP_WRAPPER_SOURCES_APP} 96 | ${PHONY_OUTPUT} 97 | COMMAND ${CMAKE_COMMAND} -E env 98 | ${FLUTTER_TOOL_ENVIRONMENT} 99 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" 100 | ${FLUTTER_TARGET_PLATFORM} $ 101 | VERBATIM 102 | ) 103 | add_custom_target(flutter_assemble DEPENDS 104 | "${FLUTTER_LIBRARY}" 105 | ${FLUTTER_LIBRARY_HEADERS} 106 | ${CPP_WRAPPER_SOURCES_CORE} 107 | ${CPP_WRAPPER_SOURCES_PLUGIN} 108 | ${CPP_WRAPPER_SOURCES_APP} 109 | ) 110 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | void RegisterPlugins(flutter::PluginRegistry* registry) { 17 | LibmpvDartPluginRegisterWithRegistrar( 18 | registry->GetRegistrarForPlugin("LibmpvDartPlugin")); 19 | MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar( 20 | registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi")); 21 | MediaKitVideoPluginCApiRegisterWithRegistrar( 22 | registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi")); 23 | ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( 24 | registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); 25 | UrlLauncherWindowsRegisterWithRegistrar( 26 | registry->GetRegistrarForPlugin("UrlLauncherWindows")); 27 | WindowManagerPluginRegisterWithRegistrar( 28 | registry->GetRegistrarForPlugin("WindowManagerPlugin")); 29 | } 30 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void RegisterPlugins(flutter::PluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | libmpv_dart 7 | media_kit_libs_windows_video 8 | media_kit_video 9 | screen_retriever_windows 10 | url_launcher_windows 11 | window_manager 12 | ) 13 | 14 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 15 | whisper4dart 16 | ) 17 | 18 | set(PLUGIN_BUNDLED_LIBRARIES) 19 | 20 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 21 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 22 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 23 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 24 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 25 | endforeach(plugin) 26 | 27 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 28 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 29 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 30 | endforeach(ffi_plugin) 31 | -------------------------------------------------------------------------------- /windows/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(runner LANGUAGES CXX) 3 | 4 | # Define the application target. To change its name, change BINARY_NAME in the 5 | # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer 6 | # work. 7 | # 8 | # Any new source files that you add to the application should be added here. 9 | add_executable(${BINARY_NAME} WIN32 10 | "flutter_window.cpp" 11 | "main.cpp" 12 | "utils.cpp" 13 | "win32_window.cpp" 14 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 15 | "Runner.rc" 16 | "runner.exe.manifest" 17 | ) 18 | 19 | # Apply the standard set of build settings. This can be removed for applications 20 | # that need different build settings. 21 | apply_standard_settings(${BINARY_NAME}) 22 | 23 | # Add preprocessor definitions for the build version. 24 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") 25 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") 26 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") 27 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") 28 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") 29 | 30 | # Disable Windows macros that collide with C++ standard library functions. 31 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 32 | 33 | # Add dependency libraries and include directories. Add any application-specific 34 | # dependencies here. 35 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) 36 | target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") 37 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 38 | 39 | # Run the Flutter tool portions of the build. This must not be removed. 40 | add_dependencies(${BINARY_NAME} flutter_assemble) 41 | -------------------------------------------------------------------------------- /windows/runner/Runner.rc: -------------------------------------------------------------------------------- 1 | // Microsoft Visual C++ generated resource script. 2 | // 3 | #pragma code_page(65001) 4 | #include "resource.h" 5 | 6 | #define APSTUDIO_READONLY_SYMBOLS 7 | ///////////////////////////////////////////////////////////////////////////// 8 | // 9 | // Generated from the TEXTINCLUDE 2 resource. 10 | // 11 | #include "winres.h" 12 | 13 | ///////////////////////////////////////////////////////////////////////////// 14 | #undef APSTUDIO_READONLY_SYMBOLS 15 | 16 | ///////////////////////////////////////////////////////////////////////////// 17 | // English (United States) resources 18 | 19 | #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) 20 | LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US 21 | 22 | #ifdef APSTUDIO_INVOKED 23 | ///////////////////////////////////////////////////////////////////////////// 24 | // 25 | // TEXTINCLUDE 26 | // 27 | 28 | 1 TEXTINCLUDE 29 | BEGIN 30 | "resource.h\0" 31 | END 32 | 33 | 2 TEXTINCLUDE 34 | BEGIN 35 | "#include ""winres.h""\r\n" 36 | "\0" 37 | END 38 | 39 | 3 TEXTINCLUDE 40 | BEGIN 41 | "\r\n" 42 | "\0" 43 | END 44 | 45 | #endif // APSTUDIO_INVOKED 46 | 47 | 48 | ///////////////////////////////////////////////////////////////////////////// 49 | // 50 | // Icon 51 | // 52 | 53 | // Icon with lowest ID value placed first to ensure application icon 54 | // remains consistent on all systems. 55 | IDI_APP_ICON ICON "resources\\app_icon.ico" 56 | 57 | 58 | ///////////////////////////////////////////////////////////////////////////// 59 | // 60 | // Version 61 | // 62 | 63 | #if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) 64 | #define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD 65 | #else 66 | #define VERSION_AS_NUMBER 1,0,0,0 67 | #endif 68 | 69 | #if defined(FLUTTER_VERSION) 70 | #define VERSION_AS_STRING FLUTTER_VERSION 71 | #else 72 | #define VERSION_AS_STRING "1.0.0" 73 | #endif 74 | 75 | VS_VERSION_INFO VERSIONINFO 76 | FILEVERSION VERSION_AS_NUMBER 77 | PRODUCTVERSION VERSION_AS_NUMBER 78 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK 79 | #ifdef _DEBUG 80 | FILEFLAGS VS_FF_DEBUG 81 | #else 82 | FILEFLAGS 0x0L 83 | #endif 84 | FILEOS VOS__WINDOWS32 85 | FILETYPE VFT_APP 86 | FILESUBTYPE 0x0L 87 | BEGIN 88 | BLOCK "StringFileInfo" 89 | BEGIN 90 | BLOCK "040904e4" 91 | BEGIN 92 | VALUE "CompanyName", "org.playboy" "\0" 93 | VALUE "FileDescription", "playboy" "\0" 94 | VALUE "FileVersion", VERSION_AS_STRING "\0" 95 | VALUE "InternalName", "playboy" "\0" 96 | VALUE "LegalCopyright", "Copyright (C) 2024 org.playboy. All rights reserved." "\0" 97 | VALUE "OriginalFilename", "playboy.exe" "\0" 98 | VALUE "ProductName", "playboy" "\0" 99 | VALUE "ProductVersion", VERSION_AS_STRING "\0" 100 | END 101 | END 102 | BLOCK "VarFileInfo" 103 | BEGIN 104 | VALUE "Translation", 0x409, 1252 105 | END 106 | END 107 | 108 | #endif // English (United States) resources 109 | ///////////////////////////////////////////////////////////////////////////// 110 | 111 | 112 | 113 | #ifndef APSTUDIO_INVOKED 114 | ///////////////////////////////////////////////////////////////////////////// 115 | // 116 | // Generated from the TEXTINCLUDE 3 resource. 117 | // 118 | 119 | 120 | ///////////////////////////////////////////////////////////////////////////// 121 | #endif // not APSTUDIO_INVOKED 122 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.cpp: -------------------------------------------------------------------------------- 1 | #include "flutter_window.h" 2 | 3 | #include 4 | 5 | #include "flutter/generated_plugin_registrant.h" 6 | 7 | FlutterWindow::FlutterWindow(const flutter::DartProject& project) 8 | : project_(project) {} 9 | 10 | FlutterWindow::~FlutterWindow() {} 11 | 12 | bool FlutterWindow::OnCreate() { 13 | if (!Win32Window::OnCreate()) { 14 | return false; 15 | } 16 | 17 | RECT frame = GetClientArea(); 18 | 19 | // The size here must match the window dimensions to avoid unnecessary surface 20 | // creation / destruction in the startup path. 21 | flutter_controller_ = std::make_unique( 22 | frame.right - frame.left, frame.bottom - frame.top, project_); 23 | // Ensure that basic setup of the controller was successful. 24 | if (!flutter_controller_->engine() || !flutter_controller_->view()) { 25 | return false; 26 | } 27 | RegisterPlugins(flutter_controller_->engine()); 28 | SetChildContent(flutter_controller_->view()->GetNativeWindow()); 29 | 30 | flutter_controller_->engine()->SetNextFrameCallback([&]() { 31 | this->Show(); 32 | }); 33 | 34 | // Flutter can complete the first frame before the "show window" callback is 35 | // registered. The following call ensures a frame is pending to ensure the 36 | // window is shown. It is a no-op if the first frame hasn't completed yet. 37 | flutter_controller_->ForceRedraw(); 38 | 39 | return true; 40 | } 41 | 42 | void FlutterWindow::OnDestroy() { 43 | if (flutter_controller_) { 44 | flutter_controller_ = nullptr; 45 | } 46 | 47 | Win32Window::OnDestroy(); 48 | } 49 | 50 | LRESULT 51 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message, 52 | WPARAM const wparam, 53 | LPARAM const lparam) noexcept { 54 | // Give Flutter, including plugins, an opportunity to handle window messages. 55 | if (flutter_controller_) { 56 | std::optional result = 57 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, 58 | lparam); 59 | if (result) { 60 | return *result; 61 | } 62 | } 63 | 64 | switch (message) { 65 | case WM_FONTCHANGE: 66 | flutter_controller_->engine()->ReloadSystemFonts(); 67 | break; 68 | } 69 | 70 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam); 71 | } 72 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_FLUTTER_WINDOW_H_ 2 | #define RUNNER_FLUTTER_WINDOW_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "win32_window.h" 10 | 11 | // A window that does nothing but host a Flutter view. 12 | class FlutterWindow : public Win32Window { 13 | public: 14 | // Creates a new FlutterWindow hosting a Flutter view running |project|. 15 | explicit FlutterWindow(const flutter::DartProject& project); 16 | virtual ~FlutterWindow(); 17 | 18 | protected: 19 | // Win32Window: 20 | bool OnCreate() override; 21 | void OnDestroy() override; 22 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, 23 | LPARAM const lparam) noexcept override; 24 | 25 | private: 26 | // The project to run. 27 | flutter::DartProject project_; 28 | 29 | // The Flutter instance hosted by this window. 30 | std::unique_ptr flutter_controller_; 31 | }; 32 | 33 | #endif // RUNNER_FLUTTER_WINDOW_H_ 34 | -------------------------------------------------------------------------------- /windows/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "flutter_window.h" 6 | #include "utils.h" 7 | 8 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, 9 | _In_ wchar_t *command_line, _In_ int show_command) { 10 | // Attach to console when present (e.g., 'flutter run') or create a 11 | // new console when running with a debugger. 12 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { 13 | CreateAndAttachConsole(); 14 | } 15 | 16 | // Initialize COM, so that it is available for use in the library and/or 17 | // plugins. 18 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 19 | 20 | flutter::DartProject project(L"data"); 21 | 22 | std::vector command_line_arguments = 23 | GetCommandLineArguments(); 24 | 25 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 26 | 27 | FlutterWindow window(project); 28 | Win32Window::Point origin(10, 10); 29 | Win32Window::Size size(900, 700); 30 | if (!window.Create(L"playboy", origin, size)) { 31 | return EXIT_FAILURE; 32 | } 33 | window.SetQuitOnClose(true); 34 | 35 | ::MSG msg; 36 | while (::GetMessage(&msg, nullptr, 0, 0)) { 37 | ::TranslateMessage(&msg); 38 | ::DispatchMessage(&msg); 39 | } 40 | 41 | ::CoUninitialize(); 42 | return EXIT_SUCCESS; 43 | } 44 | -------------------------------------------------------------------------------- /windows/runner/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by Runner.rc 4 | // 5 | #define IDI_APP_ICON 101 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 102 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Playboy-Player/Playboy/453d6c28780dda3385a0e386a8eca8bfbc46aee8/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /windows/runner/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | void CreateAndAttachConsole() { 11 | if (::AllocConsole()) { 12 | FILE *unused; 13 | if (freopen_s(&unused, "CONOUT$", "w", stdout)) { 14 | _dup2(_fileno(stdout), 1); 15 | } 16 | if (freopen_s(&unused, "CONOUT$", "w", stderr)) { 17 | _dup2(_fileno(stdout), 2); 18 | } 19 | std::ios::sync_with_stdio(); 20 | FlutterDesktopResyncOutputStreams(); 21 | } 22 | } 23 | 24 | std::vector GetCommandLineArguments() { 25 | // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. 26 | int argc; 27 | wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); 28 | if (argv == nullptr) { 29 | return std::vector(); 30 | } 31 | 32 | std::vector command_line_arguments; 33 | 34 | // Skip the first argument as it's the binary name. 35 | for (int i = 1; i < argc; i++) { 36 | command_line_arguments.push_back(Utf8FromUtf16(argv[i])); 37 | } 38 | 39 | ::LocalFree(argv); 40 | 41 | return command_line_arguments; 42 | } 43 | 44 | std::string Utf8FromUtf16(const wchar_t* utf16_string) { 45 | if (utf16_string == nullptr) { 46 | return std::string(); 47 | } 48 | int target_length = ::WideCharToMultiByte( 49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 50 | -1, nullptr, 0, nullptr, nullptr) 51 | -1; // remove the trailing null character 52 | int input_length = (int)wcslen(utf16_string); 53 | std::string utf8_string; 54 | if (target_length <= 0 || target_length > utf8_string.max_size()) { 55 | return utf8_string; 56 | } 57 | utf8_string.resize(target_length); 58 | int converted_length = ::WideCharToMultiByte( 59 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 60 | input_length, utf8_string.data(), target_length, nullptr, nullptr); 61 | if (converted_length == 0) { 62 | return std::string(); 63 | } 64 | return utf8_string; 65 | } 66 | -------------------------------------------------------------------------------- /windows/runner/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_UTILS_H_ 2 | #define RUNNER_UTILS_H_ 3 | 4 | #include 5 | #include 6 | 7 | // Creates a console for the process, and redirects stdout and stderr to 8 | // it for both the runner and the Flutter library. 9 | void CreateAndAttachConsole(); 10 | 11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string 12 | // encoded in UTF-8. Returns an empty std::string on failure. 13 | std::string Utf8FromUtf16(const wchar_t* utf16_string); 14 | 15 | // Gets the command line arguments passed in as a std::vector, 16 | // encoded in UTF-8. Returns an empty std::vector on failure. 17 | std::vector GetCommandLineArguments(); 18 | 19 | #endif // RUNNER_UTILS_H_ 20 | -------------------------------------------------------------------------------- /windows/runner/win32_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_WIN32_WINDOW_H_ 2 | #define RUNNER_WIN32_WINDOW_H_ 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | // A class abstraction for a high DPI-aware Win32 Window. Intended to be 11 | // inherited from by classes that wish to specialize with custom 12 | // rendering and input handling 13 | class Win32Window { 14 | public: 15 | struct Point { 16 | unsigned int x; 17 | unsigned int y; 18 | Point(unsigned int x, unsigned int y) : x(x), y(y) {} 19 | }; 20 | 21 | struct Size { 22 | unsigned int width; 23 | unsigned int height; 24 | Size(unsigned int width, unsigned int height) 25 | : width(width), height(height) {} 26 | }; 27 | 28 | Win32Window(); 29 | virtual ~Win32Window(); 30 | 31 | // Creates a win32 window with |title| that is positioned and sized using 32 | // |origin| and |size|. New windows are created on the default monitor. Window 33 | // sizes are specified to the OS in physical pixels, hence to ensure a 34 | // consistent size this function will scale the inputted width and height as 35 | // as appropriate for the default monitor. The window is invisible until 36 | // |Show| is called. Returns true if the window was created successfully. 37 | bool Create(const std::wstring& title, const Point& origin, const Size& size); 38 | 39 | // Show the current window. Returns true if the window was successfully shown. 40 | bool Show(); 41 | 42 | // Release OS resources associated with window. 43 | void Destroy(); 44 | 45 | // Inserts |content| into the window tree. 46 | void SetChildContent(HWND content); 47 | 48 | // Returns the backing Window handle to enable clients to set icon and other 49 | // window properties. Returns nullptr if the window has been destroyed. 50 | HWND GetHandle(); 51 | 52 | // If true, closing this window will quit the application. 53 | void SetQuitOnClose(bool quit_on_close); 54 | 55 | // Return a RECT representing the bounds of the current client area. 56 | RECT GetClientArea(); 57 | 58 | protected: 59 | // Processes and route salient window messages for mouse handling, 60 | // size change and DPI. Delegates handling of these to member overloads that 61 | // inheriting classes can handle. 62 | virtual LRESULT MessageHandler(HWND window, 63 | UINT const message, 64 | WPARAM const wparam, 65 | LPARAM const lparam) noexcept; 66 | 67 | // Called when CreateAndShow is called, allowing subclass window-related 68 | // setup. Subclasses should return false if setup fails. 69 | virtual bool OnCreate(); 70 | 71 | // Called when Destroy is called. 72 | virtual void OnDestroy(); 73 | 74 | private: 75 | friend class WindowClassRegistrar; 76 | 77 | // OS callback called by message pump. Handles the WM_NCCREATE message which 78 | // is passed when the non-client area is being created and enables automatic 79 | // non-client DPI scaling so that the non-client area automatically 80 | // responds to changes in DPI. All other messages are handled by 81 | // MessageHandler. 82 | static LRESULT CALLBACK WndProc(HWND const window, 83 | UINT const message, 84 | WPARAM const wparam, 85 | LPARAM const lparam) noexcept; 86 | 87 | // Retrieves a class instance pointer for |window| 88 | static Win32Window* GetThisFromHandle(HWND const window) noexcept; 89 | 90 | // Update the window frame's theme to match the system theme. 91 | static void UpdateTheme(HWND const window); 92 | 93 | bool quit_on_close_ = false; 94 | 95 | // window handle for top level window. 96 | HWND window_handle_ = nullptr; 97 | 98 | // window handle for hosted content. 99 | HWND child_content_ = nullptr; 100 | }; 101 | 102 | #endif // RUNNER_WIN32_WINDOW_H_ 103 | --------------------------------------------------------------------------------