├── .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 | [](https://github.com/Playboy-Player/Playboy/actions)
8 | [](https://github.com/Playboy-Player/Playboy/releases)  [](https://github.com/orgs/Playboy-Player/projects/3)
9 |
10 | 
11 | 
12 | 
13 | 
14 | 
15 | 
16 |
17 | ## 界面截图
18 |
19 |
20 |
21 |
22 |
23 | |
24 |
25 |
26 | |
27 |
28 |
29 |
30 |
31 | |
32 |
33 |
34 | |
35 |
36 |
37 |
38 | ## 功能
39 |
40 | > 可以通过[键盘快捷键](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 | [](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 | [](https://github.com/Playboy-Player/Playboy/actions)
8 | [](https://github.com/Playboy-Player/Playboy/releases)  [](https://github.com/orgs/Playboy-Player/projects/3)
9 |
10 | 
11 | 
12 | 
13 | 
14 | 
15 | 
16 |
17 | ## Screenshots
18 |
19 |
20 |
21 |
22 |
23 | |
24 |
25 |
26 | |
27 |
28 |
29 |
30 |
31 | |
32 |
33 |
34 | |
35 |
36 |
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 | [](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 |
--------------------------------------------------------------------------------