├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── assets ├── CupertinoControls.png ├── MaterialControls.png ├── MaterialDesktopControls.png └── Options.png ├── devtools_options.yaml ├── example ├── .gitignore ├── .metadata ├── README.md ├── analysis_options.yaml ├── android │ ├── .gitignore │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── example │ │ │ │ │ └── MainActivity.kt │ │ │ └── res │ │ │ │ ├── drawable-v21 │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable │ │ │ │ └── launch_background.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 ├── ios │ ├── .gitignore │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── WorkspaceSettings.xcsettings │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ ├── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ │ └── LaunchImage.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ └── README.md │ │ ├── Base.lproj │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── Runner-Bridging-Header.h │ └── RunnerTests │ │ └── RunnerTests.swift ├── lib │ ├── app │ │ ├── app.dart │ │ └── theme.dart │ └── main.dart ├── pubspec.yaml ├── test │ └── widget_test.dart └── web │ ├── favicon.png │ ├── icons │ ├── Icon-192.png │ └── Icon-512.png │ ├── index.html │ └── manifest.json ├── lib ├── chewie.dart └── src │ ├── animated_play_pause.dart │ ├── center_play_button.dart │ ├── center_seek_button.dart │ ├── chewie_player.dart │ ├── chewie_progress_colors.dart │ ├── cupertino │ ├── cupertino_controls.dart │ ├── cupertino_progress_bar.dart │ └── widgets │ │ └── cupertino_options_dialog.dart │ ├── helpers │ ├── adaptive_controls.dart │ └── utils.dart │ ├── material │ ├── color_compat_extensions.dart │ ├── material_controls.dart │ ├── material_desktop_controls.dart │ ├── material_progress_bar.dart │ └── widgets │ │ ├── options_dialog.dart │ │ └── playback_speed_dialog.dart │ ├── models │ ├── index.dart │ ├── option_item.dart │ ├── options_translation.dart │ └── subtitle_model.dart │ ├── notifiers │ ├── index.dart │ └── player_notifier.dart │ ├── player_with_controls.dart │ └── progress_bar.dart ├── pubspec.yaml └── test └── uninitialized_controls_state_test.dart /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - '**.md' 7 | push: 8 | branches: 9 | - master 10 | paths-ignore: 11 | - '**.md' 12 | workflow_dispatch: 13 | 14 | concurrency: 15 | group: ${{ github.workflow }}-${{ github.ref }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | # Does a sanity check that packages at least pass analysis on the N-1 20 | # versions of Flutter stable if the package claims to support that version. 21 | # This is to minimize accidentally making changes that break old versions 22 | # (which we don't commit to supporting, but don't want to actively break) 23 | # without updating the constraints. 24 | lint_and_build: 25 | name: Flutter Version ${{ matrix.flutter-version }} Lint and Build. 26 | runs-on: ubuntu-latest 27 | strategy: 28 | matrix: 29 | flutter-version: 30 | # The version of Flutter to use should use the minimum Dart SDK version supported by the package, 31 | # refer to https://docs.flutter.dev/development/tools/sdk/releases. 32 | # Note: The version below should be manually updated to the latest second most recent version 33 | # after a new stable version comes out. 34 | - "3.27.4" 35 | - "3.x" 36 | steps: 37 | - name: 📚 Git Checkout 38 | uses: actions/checkout@v4 39 | 40 | - name: 🐦 Setup Flutter 41 | uses: subosito/flutter-action@v2 42 | with: 43 | flutter-version: ${{ matrix.flutter-version }} 44 | channel: stable 45 | cache: true 46 | cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} 47 | 48 | - name: 📦 Install Dependencies 49 | run: flutter packages get 50 | 51 | - name: ✨ Check Formatting 52 | run: dart format --set-exit-if-changed lib 53 | 54 | - name: 🕵️ Analyze 55 | run: flutter analyze lib 56 | 57 | - name: 🧪 Run Tests 58 | run: flutter test --no-pub --coverage --test-randomize-ordering-seed random 59 | 60 | - name: 📁 Upload coverage to Codecov 61 | uses: codecov/codecov-action@v5 62 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create release 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | changelog: 8 | name: Create changelog 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v2 14 | - name: Conventional Changelog Action 15 | id: changelog 16 | uses: TriPSs/conventional-changelog-action@v3 17 | with: 18 | github-token: ${{ secrets.GITHUB_TOKEN }} 19 | version-file: ./pubspec.yaml 20 | - name: Create Release 21 | id: create_release 22 | uses: actions/create-release@v1 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | with: 26 | tag_name: ${{ steps.changelog.outputs.tag }} 27 | release_name: ${{ steps.changelog.outputs.tag }} 28 | body: ${{ steps.changelog.outputs.clean_changelog }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .atom/ 3 | .idea/ 4 | .vscode/ 5 | 6 | .packages 7 | .pub/ 8 | .dart_tool/ 9 | pubspec.lock 10 | flutter_export_environment.sh 11 | 12 | examples/all_plugins/pubspec.yaml 13 | 14 | Podfile 15 | Podfile.lock 16 | Pods/ 17 | .symlinks/ 18 | **/Flutter/App.framework/ 19 | **/Flutter/ephemeral/ 20 | **/Flutter/Flutter.framework/ 21 | **/Flutter/Generated.xcconfig 22 | **/Flutter/flutter_assets/ 23 | 24 | ServiceDefinitions.json 25 | xcuserdata/ 26 | **/DerivedData/ 27 | 28 | local.properties 29 | keystore.properties 30 | .gradle/ 31 | gradlew 32 | gradlew.bat 33 | gradle-wrapper.jar 34 | .flutter-plugins-dependencies 35 | *.iml 36 | 37 | generated_plugin_registrant.dart 38 | GeneratedPluginRegistrant.h 39 | GeneratedPluginRegistrant.m 40 | generated_plugin_registrant.cc 41 | GeneratedPluginRegistrant.java 42 | GeneratedPluginRegistrant.swift 43 | build/ 44 | .flutter-plugins 45 | 46 | .project 47 | .classpath 48 | .settings -------------------------------------------------------------------------------- /.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: d957c8f040902aa3fd44b367150bde56b64cec83 8 | channel: alpha 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.11.3] 2 | * 🛠️ [#917](https://github.com/fluttercommunity/chewie/pull/917): Resolve issue where 'subtitleOn' doesn't enable subtitles by default on iOS. Thanks [alideep5](https://github.com/alideep5). 3 | 4 | ## [1.11.2] 5 | * 🛠️ [#912](https://github.com/fluttercommunity/chewie/pull/912): Add workaround for invalid buffering info on android. Thanks [timoxd7](https://github.com/timoxd7). 6 | 7 | ## [1.11.1] 8 | * ⬆️ [#875](https://github.com/fluttercommunity/chewie/pull/875): Add background tap to pause video feature. Thanks [Ortes](https://github.com/Ortes). 9 | * 🛠️ [#896](https://github.com/fluttercommunity/chewie/pull/896): Fixed allowMute being ignored on Desktop. Thanks [mpoimer](https://github.com/mpoimer). 10 | * 🛠️ [#910](https://github.com/fluttercommunity/chewie/pull/910): Fix example on web. Thanks [Ortes](https://github.com/Ortes). 11 | 12 | ## [1.11.0] 13 | * ⬆️ [#900](https://github.com/fluttercommunity/chewie/pull/900): Flutter `3.29` upgrade. Thanks [diegotori](https://github.com/diegotori). 14 | * **BREAKING CHANGE**: Library now requires at least Flutter version `3.27.0`, for real this time. 15 | 16 | ## [1.10.0] 17 | * 🛠️ [#871](https://github.com/fluttercommunity/chewie/pull/871): Fixed pop the wrong page when changing the speed. Thanks [akmalova](https://github.com/akmalova). 18 | * **BREAKING CHANGES**: 19 | * `OptionItem.onTap` now takes in a `BuildContext`. 20 | * `OptionItem`'s properties are now marked as `final`. Use `copyWith` to mutate its properties into 21 | a new instance. 22 | 23 | ## [1.9.2] 24 | * Fixed broken Table of Contents links in `README.md`, take two. 25 | 26 | ## [1.9.1+1] 27 | * Fixed broken Table of Contents links in `README.md`. 28 | 29 | ## [1.9.1] 30 | * [#872](https://github.com/fluttercommunity/chewie/pull/872): feat: Add showSubtitles flag to control subtitles (#648). Thanks [floodoo](https://github.com/floodoo). 31 | * [#890](https://github.com/fluttercommunity/chewie/pull/890): Fix issue 888. Thanks [diegotori](https://github.com/diegotori). 32 | * **IMPORTANT**: Relaxed the minimum supported Flutter version to `3.24`. 33 | From now on, this library will make a best effort to support the latest `N-1` Flutter version at the minimum. 34 | 35 | ## [1.9.0] 36 | * **BREAKING CHANGE**: Library now requires at least Flutter version `3.27.0`. 37 | 38 | ## [1.8.7] 39 | * ⬆️ [#876](https://github.com/fluttercommunity/chewie/pull/876): Add keyboard controls seek forward and backward and fullscreen escape on desktop. Thanks [Ortes](https://github.com/Ortes). 40 | 41 | ## [1.8.6] 42 | * ⬆️ [#874](https://github.com/fluttercommunity/chewie/pull/874): Add `devtools_options.yaml` configuration files. Thanks [MoRmdn](https://github.com/MoRmdn). 43 | 44 | ## [1.8.5] 45 | * ⬆️ [#703](https://github.com/fluttercommunity/chewie/pull/703): Adding Seek buttons for Android. Thanks [GyanendroKh](https://github.com/GyanendroKh). 46 | * Upgraded `wakelock_plus` to version `1.2.8`, which uses `web` version `1.0.0`. Thanks [diegotori](https://github.com/diegotori). 47 | 48 | ## [1.8.4] 49 | * 🛠️ [#838](https://github.com/fluttercommunity/chewie/pull/838): Add bufferingBuilder. Thanks [daniellampl](https://github.com/daniellampl). 50 | 51 | ## [1.8.3] 52 | * 🛠️ [#828](https://github.com/fluttercommunity/chewie/pull/828): Fix the logic of the Center Play Button icon selection. Thanks [EmreDET](https://github.com/EmreDET). 53 | 54 | ## 1.8.2 55 | * ⬆️ [#842](https://github.com/fluttercommunity/chewie/pull/842): package upgrades. Thanks [vaishnavi-2301](https://github.com/vaishnavi-2301). 56 | 57 | ## 1.8.1 58 | * ⬆️ [#825](https://github.com/fluttercommunity/chewie/pull/825): Upgraded `wakelock_plus` to version `1.2.2`. Thanks [diegotori](https://github.com/diegotori). 59 | 60 | ## 1.8.0 61 | * 🛠️ [#814](https://github.com/fluttercommunity/chewie/pull/814): Refactor VideoPlayerController initialization to adhere to video_player ^2.8.2 guidelines. Thanks [ishworpanta10](https://github.com/ishworpanta10). 62 | * 🛠️ [#815](https://github.com/fluttercommunity/chewie/pull/815): Fix the Safe area conflict for material controls in Android. Thanks [MadGeorge](https://github.com/MadGeorge). 63 | * 🛠️ [#821](https://github.com/fluttercommunity/chewie/pull/821): Upgrade chewie's dependency package. Thanks [ycv005](https://github.com/ycv005). 64 | * 🛠️ [#824](https://github.com/fluttercommunity/chewie/pull/824): Flutter 3.19 enforcement. Thanks [diegotori](https://github.com/diegotori). 65 | * **BREAKING CHANGE**: Library now requires at least Flutter and Dart versions `3.19.0` and `3.3` respectively. 66 | 67 | 68 | ## 1.7.5 69 | * 🛠️ [#810](https://github.com/fluttercommunity/chewie/pull/810): Fixed : Web full screen issue (#790 #688). Thanks [ToddZeil](https://github.com/ToddZeil). 70 | * 🛠️ [#802](https://github.com/fluttercommunity/chewie/pull/802): Update chewie_player.dart. Thanks [B0yma](https://github.com/B0yma). 71 | 72 | ## 1.7.4 73 | * 🛠️ [#774](https://github.com/fluttercommunity/chewie/pull/774): Fixed : Playback speed reset on forwarding video. Thanks [Kronos-2701](https://github.com/Kronos-2701). 74 | 75 | ## 1.7.3 76 | * 🛠️ [#777](https://github.com/fluttercommunity/chewie/pull/777): fix display size while Chewie wrapped by some rotate widget. Thanks [bailyzheng](https://github.com/bailyzheng). 77 | 78 | ## 1.7.2 79 | * 🛠️ [#798](https://github.com/fluttercommunity/chewie/pull/798): Fix: Progress bar does not follow drag #789. Thanks [koutaro-masaki](https://github.com/koutaro-masaki). 80 | 81 | ## 1.7.1 82 | * 🛠️ [#772](https://github.com/fluttercommunity/chewie/pull/772): Stop force disabling wakelock. Thanks [jan-milovanovic](https://github.com/jan-milovanovic). 83 | * ⬆️ [#775](https://github.com/fluttercommunity/chewie/pull/775): Flutter `3.13` iOS example app upgrade. Thanks [diegotori](https://github.com/diegotori). 84 | 85 | ## 1.7.0 86 | * 🛠️ [#754](https://github.com/fluttercommunity/chewie/pull/754): Upgraded `wakelock_plus` to version `1.1.0`. Thanks [diegotori](https://github.com/diegotori). 87 | * **BREAKING CHANGE**: Library now requires at least Dart and Flutter versions `2.18` and `3.3.0` respectively. 88 | 89 | ## 1.6.0+1 90 | * Added Flutter Community Banner to `README.md`. Thanks [diegotori](https://github.com/diegotori). 91 | 92 | ## 1.6.0 93 | * [#747](https://github.com/fluttercommunity/chewie/pull/747): Migrated from `wakelock` to `wakelock_plus`. Thanks [diegotori](https://github.com/diegotori). 94 | * Also upgrades `video_player` from `2.4.7` to `2.7.0`. 95 | * **IMPORTANT**: Library now requires `Flutter`, version `2.11.0` or higher. 96 | 97 | ## 1.5.0 98 | * 🛠️ [#712](https://github.com/fluttercommunity/chewie/pull/712): Progress Bars can now be disabled by setting `ChewieController.draggableProgressBar` to `false`. Thanks [shiyiya](https://github.com/shiyiya). 99 | * ⬆️ Increased Dart SDK constraint to cover Dart `3.0.0` and higher. 100 | 101 | ## 1.4.1 102 | * 🛠️ [#719](https://github.com/fluttercommunity/chewie/pull/719): Fix overlay not visible. Thanks [jaripekkala](https://github.com/jaripekkala). 103 | 104 | ## 1.4.0 105 | * 🛠️ [#701](https://github.com/fluttercommunity/chewie/pull/701): Added Dart Analysis fixes due to Flutter 3.7. Thanks [diegotori](https://github.com/diegotori). 106 | 107 | ## 1.3.6 108 | * 🛠️ [#681](https://github.com/fluttercommunity/chewie/pull/681): Flutter `3.3` lint fixes. Thanks [diegotori](https://github.com/diegotori). 109 | 110 | * ⬆️ [#676](https://github.com/fluttercommunity/chewie/pull/676): Allow Chewie controls to be positioned to allow for a larger safe area. Thanks [jweidner-mbible](https://github.com/jweidner-mbible). 111 | 112 | ## 1.3.5 113 | 114 | * ⬆️ [#669](https://github.com/fluttercommunity/chewie/pull/669): Fix for CenterPlayButton UI bug when using Material 3. Thanks [luis901101](https://github.com/luis901101). 115 | * ⬆️ [#658](https://github.com/fluttercommunity/chewie/pull/658): Add transformationController to Interactive Viewer. Thanks [Geevies](https://github.com/Geevies). 116 | * ⬆️ update `video_player` to 2.4.7 117 | * ⬆️ update `wakelock` to 0.6.2 118 | * 🛠️ Fixed new linting issues 119 | * 💡 Library is now using `flutter_lints` for all of its linting needs. 120 | 121 | ## 1.3.4 122 | * ⬆️ [#646](https://github.com/fluttercommunity/chewie/pull/646): Fix to videos recorded with an orientation of 180° ( landscapeRight) being reversed on Android. Thanks [williamviktorsson](https://github.com/williamviktorsson). 123 | * ⬆️ [#623](https://github.com/fluttercommunity/chewie/pull/623): [Android] Add a delay before displaying progress indicator. Thanks [henri2h](https://github.com/henri2h). 124 | 125 | ## 1.3.3 126 | * ⬆️ [#634](https://github.com/fluttercommunity/chewie/pull/634): chore: Move very_good_analysis to dev_dependencies. Thanks [JCQuintas](https://github.com/JCQuintas). 127 | 128 | ## 1.3.2 129 | * ⬆️ [#626](https://github.com/fluttercommunity/chewie/pull/626): Added customizable timer to hide controls. Thanks [BuginRug](https://github.com/BuginRug). 130 | 131 | ## 1.3.1 132 | * ⬆️ [#617](https://github.com/fluttercommunity/chewie/pull/617): Allow video zooming with InteractiveViewer widget. Thanks [jmsanc](https://github.com/jmsanc). 133 | 134 | ## 1.3.0 135 | 136 | * ⬆️ [#598](https://github.com/fluttercommunity/chewie/pull/598): Update `wakelock` to `^0.6.1+1`. Thanks [fehernyul](https://github.com/fehernyul). 137 | * ⬆️ [#599](https://github.com/fluttercommunity/chewie/pull/599): Uniform controls. Thanks [BuginRug](https://github.com/BuginRug). 138 | 139 | **Slight Breaking Change**. Instead of: 140 | 141 | ```dart 142 | typedef ChewieRoutePageBuilder = Widget Function( 143 | BuildContext context, 144 | Animation animation, 145 | Animation secondaryAnimation, 146 | _ChewieControllerProvider controllerProvider, 147 | ); 148 | ``` 149 | 150 | It is now: 151 | 152 | ```dart 153 | typedef ChewieRoutePageBuilder = Widget Function( 154 | BuildContext context, 155 | Animation animation, 156 | Animation secondaryAnimation, 157 | ChewieControllerProvider controllerProvider, 158 | ); 159 | ``` 160 | 161 | TL;DR: We had to make `_ChewieControllerProvider` public. 162 | 163 | * 🛠️ Fixed lint and formatting problems 164 | * Under New Management under the auspices of [Flutter Community](https://github.com/fluttercommunity), and new maintainers [diegotori](https://github.com/diegotori) and [maherjaafar](https://github.com/maherjaafar). 165 | 166 | ## 1.2.3 167 | 168 | * ⬆️ Update 'provider' to 6.0.1 169 | - fixes [#568](https://github.com/brianegan/chewie/issues/568) 170 | * ⬆️ Update 'video_player' to 2.2.7 171 | * ⬆️ Update 'wakelock' to 0.5.6 172 | * ⬆️ Update 'lint' to 1.7.2 173 | * ⬆️ Update roadmap 174 | * 🛠️ Fix lint problems 175 | * 💡 Add very_good_analysis package 176 | * 💡 Add analysis_options.yaml for example app 177 | 178 | ## 1.2.2 179 | 180 | * 🛠️ Fix Incorrect use of ParentDataWidget. 181 | - Fixes: [#485](https://github.com/brianegan/chewie/issues/485) 182 | 183 | ## 1.2.1 184 | 185 | * 💡 add `showOptions` flag to show/hide the options-menu 186 | - Fixes: [#491](https://github.com/brianegan/chewie/issues/491) 187 | * ⬆️ update `video_player` to 2.1.5 188 | * 🛠️ fix MaterialUI duration text (RichText) 189 | 190 | ## 1.2.0 191 | 192 | * 🖥 __Desktop-UI__: Added `AdaptiveControls` where `MaterialDesktopControls` is now the default for Desktop-Platforms (start [ChewieDemo](https://github.com/brianegan/chewie/blob/master/example/lib/app/app.dart) for a preview) 193 | - Fixes: [#188](https://github.com/brianegan/chewie/issues/478) 194 | * Redesign `MaterialControls` (inspired by Youtube Mobile and Desktop) 195 | * Fix squeeze of `CenterPlayButton` 196 | * Add: `optionsTranslation`, `additionalOptions` and `optionsBuilder` to create and design your Video-Options like Playback speed, subtitles and other options you want to add (use here: `additionalOptions`!). Use `optionsTranslation` to provide your localized strings! 197 | 198 | > See [Options](https://github.com/brianegan/chewie#options) to customize your Chewie options 199 | 200 | ## 1.1.0 201 | 202 | * Add subtitle functionality 203 | - Thanks to kirill09: [#188](https://github.com/brianegan/chewie/pull/188) with which we've improved and optimized subtitles 204 | 205 | > See readme on how to create subtitles and provide your own subtitleBuilder: [Subtitles](https://github.com/brianegan/chewie#Subtitles) 206 | 207 | ## 1.0.0 208 | 209 | * Migrate to Null Safety 210 | - Thanks to miDeb: [#406](https://github.com/brianegan/chewie/pull/443) 211 | 212 | ## 0.12.1+1 213 | 214 | * Lint: Format to line length 80 for pub score 215 | 216 | ## 0.12.2 217 | 218 | * Fix: Deprecation of [`resizeToAvoidBottomPadding`](https://api.flutter.dev/flutter/material/Scaffold/resizeToAvoidBottomPadding.html). Replaced by `resizeToAvoidBottomInset` 219 | - Thanks to: [#423](https://github.com/brianegan/chewie/pull/423) 220 | 221 | ## 0.12.1 222 | 223 | * Fix: Duration called on null for cupertino controls 224 | - Thanks to: [#406](https://github.com/brianegan/chewie/pull/406) 225 | * Bump required Flutter version 1.20 -> 1.22 226 | - Thanks to: [#401](https://github.com/brianegan/chewie/pull/401) 227 | * Export controls in chewie.dart. 228 | - Thanks to: [#355](https://github.com/brianegan/chewie/pull/355) 229 | * Add `lint` linter 230 | * Add CI to analyze and check format 231 | 232 | ## 0.12.0 233 | 234 | * Add replay feature 235 | * Add Animated Play/Pause Button 236 | - Thanks to: [#228](https://github.com/brianegan/chewie/pull/228) 237 | 238 | ## 0.11.0 239 | 240 | * Add playback speed controls: 241 | - Thanks to: [#390](https://github.com/brianegan/chewie/pull/390) 242 | * Correct dependencies: 243 | - Thanks to: [#395](https://github.com/brianegan/chewie/pull/395) 244 | 245 | ## 0.10.4 246 | 247 | * Update Android example to latest support 248 | * Update Dart SDK 249 | * Update Flutter SDK 250 | * Update `wakelock` dependency 251 | 252 | ## 0.10.3+1 253 | 254 | * Format using `dartfmt -w .` for pub.dev 255 | 256 | ## 0.10.3 257 | 258 | * Bugfix: only `setState` if widget is mounted (cupertino + material) 259 | - Thanks to: [#309](https://github.com/brianegan/chewie/pull/309) 260 | 261 | ## 0.10.2 262 | 263 | * Replace `open_iconic_flutter` with `cupertino_icons` to resolve Apple App-Store rejection (ITMS-90853) 264 | - Fixes: [#381](https://github.com/brianegan/chewie/issues/381) 265 | 266 | ## 0.10.1 267 | 268 | * Update `video_player` dependecy (stable release) 269 | 270 | ## 0.10.0 271 | 272 | * Fix portrait mode 273 | * Add auto-detect orientation based on video aspect-ratio 274 | * Add optional parameters for `onEnterFullScreen` 275 | * Support iOS 14 with SafeArea in FullScreen 276 | 277 | ## 0.9.10 278 | 279 | * Remove `isInitialRoute` from full screen page route 280 | 281 | ## 0.9.9 282 | 283 | * Changed wakelock plugin from `flutter_screen` to `wakelock` due to lack of maintenance of `flutter_screen`. 284 | 285 | ## 0.9.8+1 286 | * Require latest flutter stable version 287 | 288 | ## 0.9.8 289 | 290 | * Hero Widget is no longer used (thanks @localpcguy) 291 | * Tap to hide controls (thanks @bostrot) 292 | * Replay on play when video is finished (thanks @VictorUvarov) 293 | 294 | ## 0.9.7 295 | 296 | * Errors are properly handled. You can provide the Widget to display when an error occurs by providing an `errorBuilder` function to the `ChewieController` constructor. 297 | * Add ability to override the fullscreen page builder. Allows folks to customize that functionality! 298 | 299 | ## 0.9.6 300 | 301 | * Update to work with `video_player: ">=0.7.0 <0.11.0"` 302 | 303 | ## 0.9.5 304 | 305 | * Cosmetic change -> remove unfinished fit property which slipped into the last release 306 | 307 | ## 0.9.4 308 | 309 | * Add overlay option to place a widget between the video and the controls 310 | * Update to work with `video_player: ">=0.7.0 <0.10.0"` 311 | 312 | ## 0.9.3 313 | 314 | * Absorb pointer when controls are hidden 315 | 316 | ## 0.9.2 317 | 318 | * Add options to define system overlays after exiting full screen 319 | * Add option to hide mute button 320 | 321 | ## 0.9.1 322 | 323 | * Add option to hide full screen button 324 | 325 | ## 0.9.0 326 | 327 | * **Breaking changes**: Add a `ChewieController` to make customizations and control from outside of the player easier. 328 | Refer to the [README](README.md) for details on how to upgrade from previous versions. 329 | 330 | ## 0.8.0 331 | 332 | * Update to work with `video_player: ">=0.7.0 <0.8.0` - Thanks @Sub6Resources 333 | * Preserves AspectRatio on FullScreen - Thanks @patrickb 334 | * Ability to start video in FullScreen - Thanks @miguelpruivo 335 | 336 | ## 0.7.0 337 | 338 | * Requires Dart 2 339 | * Updated dependencies that were not Dart 2 compatible 340 | 341 | ## 0.6.1 342 | 343 | * Fix time formatting 344 | * Fix skipping 345 | * Remove listener when disposed 346 | * Start video at certain position 347 | 348 | ## 0.6.0 349 | 350 | * Update to work with `video_player: ">=0.6.0 <0.7.0` 351 | 352 | ## 0.5.1 353 | 354 | * Update README to fix installation instructions 355 | 356 | ## 0.5.0 357 | 358 | * Update to work with `video_player: ">=0.5.0 <0.6.0` 359 | 360 | ## 0.3.0 361 | 362 | * Update to work with `video_player: ">=0.2.0 <0.3.0` 363 | * Add `showControls` option. You can use this to show / hide the controls 364 | * Move from `VideoProgressColors` to `ChewieProgressColors` for customization of the Chewie progress controls 365 | * Remove `progressColors` in favor of platform-specific customizations: `cupertinoProgressColors` and `materialProgressColors` to control 366 | * Add analysis options 367 | 368 | ## 0.2.0 369 | 370 | * Take a `controller` instead of a `String uri`. Allows for better control of playback outside the player if need be. 371 | 372 | ## 0.1.1 373 | 374 | * Fix images in docs for pub 375 | 376 | ## 0.1.0 377 | 378 | Initial version of Chewie, the video player with a heart of gold. 379 | 380 | * Hand a VideoPlayerController to Chewie, and let it do the rest. 381 | * Includes Material Player Controls 382 | * Includes Cupertino Player Controls 383 | * Spike version: Focus on good looking UI. Internal code is sloppy, needs a refactor and tests 384 | 385 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2017 Brian Egan 3 | 4 | Permission is hereby granted, free of charge, to any 5 | person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the 7 | Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, 9 | sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do 11 | so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall 14 | be included in all copies or substantial portions of 15 | the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 21 | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 22 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 23 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chewie 2 | 3 | [![Flutter Community: chewie](https://fluttercommunity.dev/_github/header/chewie)](https://github.com/fluttercommunity/community) 4 | 5 | [![Version](https://img.shields.io/pub/v/chewie.svg)](https://pub.dev/packages/chewie) 6 | ![CI](https://github.com/brianegan/chewie/workflows/CI/badge.svg) 7 | [![Generic badge](https://img.shields.io/badge/platform-android%20|%20ios%20|%20web%20-blue.svg)](https://pub.dev/packages/chewie) 8 | 9 | The video player for Flutter with a heart of gold. 10 | 11 | The [`video_player`](https://pub.dartlang.org/packages/video_player) plugin provides low-level 12 | access to video playback. 13 | 14 | Chewie uses the `video_player` under the hood and wraps it in a friendly Material or Cupertino UI! 15 | 16 | ## Table of Contents 17 | 1. 🚨 [IMPORTANT!!! (READ THIS FIRST)](#-important-read-this-first) 18 | 2. 🔀 [Flutter Version Compatibility](#-flutter-version-compatibility) 19 | 3. 🖼️ [Preview](#%EF%B8%8F-preview) 20 | 4. ⬇️ [Installation](#%EF%B8%8F-installation) 21 | 5. 🕹️ [Using it](#%EF%B8%8F-using-it) 22 | 6. ⚙️ [Options](#%EF%B8%8F-options) 23 | 7. 🔡 [Subtitles](#-subtitles) 24 | 8. 🧪 [Example](#-example) 25 | 9. ⏪ [Migrating from Chewie < 0.9.0](#-migrating-from-chewie--090) 26 | 10. 🗺️ [Roadmap](#%EF%B8%8F-roadmap) 27 | 11. ⚠️ [Android warning](#%EF%B8%8F-android-warning) 28 | 12. 📱 [iOS warning](#-ios-warning) 29 | 30 | 31 | ## 🚨 IMPORTANT!!! (READ THIS FIRST) 32 | This library is __NOT__ responsible for any issues caused by `video_player`, since it's merely a UI 33 | layer on top of it. 34 | 35 | In other words, if you see any `PlatformException`s being thrown in your app due to video playback, 36 | they are exclusive to the `video_player` library. 37 | 38 | Instead, please raise an issue related to it with the [Flutter Team](https://github.com/flutter/flutter/issues/new/choose). 39 | 40 | ## 🔀 Flutter Version Compatibility 41 | 42 | This library will at the very least make a solid effort to support the second most recent version 43 | of Flutter released. In other words, it will adopt `N-1` version support at 44 | the bare minimum. 45 | 46 | However, this cannot be guaranteed due to major changes between Flutter versions. Should that occur, 47 | future updates will be released as major or minor versions as needed. 48 | 49 | ## 🖼️ Preview 50 | 51 | | MaterialControls | MaterialDesktopControls | 52 | |:-------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------------:| 53 | | ![](https://github.com/brianegan/chewie/raw/master/assets/MaterialControls.png) | ![](https://github.com/brianegan/chewie/raw/master/assets/MaterialDesktopControls.png) | 54 | 55 | ### CupertinoControls 56 | ![](https://github.com/brianegan/chewie/raw/master/assets/CupertinoControls.png) 57 | 58 | ## ⬇️ Installation 59 | 60 | In your `pubspec.yaml` file within your Flutter Project add `chewie` and `video_player` under dependencies: 61 | 62 | ```yaml 63 | dependencies: 64 | chewie: 65 | video_player: 66 | ``` 67 | 68 | ## 🕹️ Using it 69 | 70 | ```dart 71 | import 'package:chewie/chewie.dart'; 72 | import 'package:video_player/video_player.dart'; 73 | 74 | final videoPlayerController = VideoPlayerController.networkUrl(Uri.parse( 75 | 'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4')); 76 | 77 | await videoPlayerController.initialize(); 78 | 79 | final chewieController = ChewieController( 80 | videoPlayerController: videoPlayerController, 81 | autoPlay: true, 82 | looping: true, 83 | ); 84 | 85 | final playerWidget = Chewie( 86 | controller: chewieController, 87 | ); 88 | ``` 89 | 90 | Please make sure to dispose both controller widgets after use. For example, by overriding the dispose method of the a `StatefulWidget`: 91 | ```dart 92 | @override 93 | void dispose() { 94 | videoPlayerController.dispose(); 95 | chewieController.dispose(); 96 | super.dispose(); 97 | } 98 | ``` 99 | 100 | ## ⚙️ Options 101 | 102 | ![](https://github.com/brianegan/chewie/raw/master/assets/Options.png) 103 | 104 | Chewie has some options which control the video. These options appear by default in a `showModalBottomSheet` (similar to YT). By default, Chewie passes `Playback speed` and `Subtitles` options as an `OptionItem`. 105 | 106 | To add additional options, just add these lines to your `ChewieController`: 107 | 108 | ```dart 109 | additionalOptions: (context) { 110 | return [ 111 | OptionItem( 112 | onTap: () => debugPrint('My option works!'), 113 | iconData: Icons.chat, 114 | title: 'My localized title', 115 | ), 116 | OptionItem( 117 | onTap: () => 118 | debugPrint('Another option that works!'), 119 | iconData: Icons.chat, 120 | title: 'Another localized title', 121 | ), 122 | ]; 123 | }, 124 | ``` 125 | 126 | ### Customizing the modal sheet 127 | 128 | If you don't like the default `showModalBottomSheet` for showing your options, you can override the View with the `optionsBuilder` method: 129 | 130 | ```dart 131 | optionsBuilder: (context, defaultOptions) async { 132 | await showDialog( 133 | context: context, 134 | builder: (ctx) { 135 | return AlertDialog( 136 | content: ListView.builder( 137 | itemCount: defaultOptions.length, 138 | itemBuilder: (_, i) => ActionChip( 139 | label: Text(defaultOptions[i].title), 140 | onPressed: () => 141 | defaultOptions[i].onTap!(), 142 | ), 143 | ), 144 | ); 145 | }, 146 | ); 147 | }, 148 | ``` 149 | 150 | Your `additionalOptions` are already included here (if you provided `additionalOptions`)! 151 | 152 | ### Translations 153 | 154 | What is an option without proper translation? 155 | 156 | To add your translation strings add: 157 | 158 | ```dart 159 | optionsTranslation: OptionsTranslation( 160 | playbackSpeedButtonText: 'Wiedergabegeschwindigkeit', 161 | subtitlesButtonText: 'Untertitel', 162 | cancelButtonText: 'Abbrechen', 163 | ), 164 | ``` 165 | 166 | ## 🔡 Subtitles 167 | 168 | > Since version 1.1.0, Chewie supports subtitles. 169 | 170 | Chewie allows you to enhance the video playback experience with text overlays. You can add a `List` to your `ChewieController` and fully customize their appearance using the `subtitleBuilder` function. 171 | 172 | ### Showing Subtitles by Default 173 | 174 | Chewie provides the `showSubtitles` flag, allowing you to control whether subtitles are displayed automatically when the video starts. By default, this flag is set to `false`. 175 | 176 | ### Adding Subtitles 177 | 178 | Here’s an example of how to add subtitles to your `ChewieController`: 179 | 180 | ```dart 181 | ChewieController( 182 | videoPlayerController: _videoPlayerController, 183 | autoPlay: true, 184 | looping: true, 185 | subtitle: Subtitles([ 186 | Subtitle( 187 | index: 0, 188 | start: Duration.zero, 189 | end: const Duration(seconds: 10), 190 | text: 'Hello from subtitles', 191 | ), 192 | Subtitle( 193 | index: 1, 194 | start: const Duration(seconds: 10), 195 | end: const Duration(seconds: 20), 196 | text: 'What’s up? :)', 197 | ), 198 | ]), 199 | showSubtitles: true, // Automatically display subtitles 200 | subtitleBuilder: (context, subtitle) => Container( 201 | padding: const EdgeInsets.all(10.0), 202 | child: Text( 203 | subtitle, 204 | style: const TextStyle(color: Colors.white), 205 | ), 206 | ), 207 | ); 208 | ``` 209 | 210 | ### Subtitle Structure 211 | 212 | The `Subtitle` model contains the following key attributes: 213 | 214 | - **`index`**: A unique identifier for the subtitle, useful for database integration. 215 | - **`start`**: The starting point of the subtitle, defined as a `Duration`. 216 | - **`end`**: The ending point of the subtitle, defined as a `Duration`. 217 | - **`text`**: The subtitle text that will be displayed. 218 | 219 | For example, if your video is 10 minutes long and you want to add a subtitle that appears between `00:00` and `00:10`, you can define it like this: 220 | 221 | ```dart 222 | Subtitle( 223 | index: 0, 224 | start: Duration.zero, 225 | end: const Duration(seconds: 10), 226 | text: 'Hello from subtitles', 227 | ), 228 | ``` 229 | 230 | ### Customizing Subtitles 231 | 232 | Use the `subtitleBuilder` function to customize how subtitles are rendered, allowing you to modify text styles, add padding, or apply other customizations to your subtitles. 233 | 234 | ## 🧪 Example 235 | 236 | Please run the app in the [`example/`](https://github.com/brianegan/chewie/tree/master/example) folder to start playing! 237 | 238 | ## ⏪ Migrating from Chewie < 0.9.0 239 | 240 | Instead of passing the `VideoPlayerController` and your options to the `Chewie` widget you now pass them to the `ChewieController` and pass that later to the `Chewie` widget. 241 | 242 | ```dart 243 | final playerWidget = Chewie( 244 | videoPlayerController, 245 | autoPlay: true, 246 | looping: true, 247 | ); 248 | ``` 249 | 250 | becomes 251 | 252 | ```dart 253 | final chewieController = ChewieController( 254 | videoPlayerController: videoPlayerController, 255 | autoPlay: true, 256 | looping: true, 257 | ); 258 | 259 | final playerWidget = Chewie( 260 | controller: chewieController, 261 | ); 262 | ``` 263 | 264 | ## 🗺️ Roadmap 265 | 266 | - [x] MaterialUI 267 | - [x] MaterialDesktopUI 268 | - [x] CupertinoUI 269 | - [x] Options with translations 270 | - [x] Subtitles 271 | - [x] CustomControls 272 | - [x] Auto-Rotate on FullScreen depending on Source Aspect-Ratio 273 | - [x] Live-Stream and UI 274 | - [x] AutoPlay 275 | - [x] Placeholder 276 | - [x] Looping 277 | - [x] Start video at 278 | - [x] Custom Progress-Bar colors 279 | - [x] Custom Overlay 280 | - [x] Allow Sleep (Wakelock) 281 | - [x] Playbackspeed Control 282 | - [x] Custom Route-Pagebuilder 283 | - [x] Custom Device-Orientation and SystemOverlay before and after fullscreen 284 | - [x] Custom ErrorBuilder 285 | - [ ] Support different resolutions of video 286 | - [ ] Re-design State-Manager with Provider 287 | - [ ] Screen-Mirroring / Casting (Google Chromecast) 288 | 289 | 290 | ## ⚠️ Android warning 291 | 292 | There is an open [issue](https://github.com/flutter/flutter/issues/165149) that the buffering state of a video is not reported correctly. With this, the loading state is always triggered, hiding controls to play, pause or seek the video. A workaround was implemented until this is fixed, however it can't be perfect and still hides controls if seeking backwards while the video is paused, as a result of lack of correct buffering information (see #912). 293 | 294 | Add the following to partly fix this behavior: 295 | 296 | ```dart 297 | // Your init code can be above 298 | videoController.addListener(yourListeningMethod); 299 | 300 | // ... 301 | 302 | bool wasPlayingBefore = false; 303 | void yourListeningMethod() { 304 | if (!videoController.value.isPlaying && !wasPlayingBefore) { 305 | // -> Workaround if seekTo another position while it was paused before. 306 | // On Android this might lead to infinite loading, so just play the 307 | // video again. 308 | videoController.play(); 309 | } 310 | 311 | wasPlayingBefore = videoController.value.isPlaying; 312 | 313 | // ... 314 | } 315 | ``` 316 | 317 | You can also disable the loading spinner entirely to fix this problem in a more _complete_ way, however will remove the loading indicator if a video is buffering. 318 | 319 | ```dart 320 | _chewieController = ChewieController( 321 | videoPlayerController: _videoPlayerController, 322 | progressIndicatorDelay: Platform.isAndroid ? const Duration(days: 1) : null, 323 | ); 324 | ``` 325 | 326 | ## 📱 iOS warning 327 | 328 | The video_player plugin used by chewie will only work in iOS simulators if you are on flutter 1.26.0 or above. You may need to switch to the beta channel `flutter channel beta` 329 | Please refer to this [issue](https://github.com/flutter/flutter/issues/14647). 330 | 331 | 332 | 333 | ``` 334 | 000000000000000KKKKKKKKKKKKXXXXXXXXXXXXXKKKKKKKKKKKKKKKKKKKKKKKKKKK00 335 | 000000000000000KKKKKKKKKKKKKXXXXXXXXXXKKKKKKKKKKKKKKKKKKKKKKKKKKKKK00 336 | 000000000000000KKKKKKKKKKKKKXXXXXXK0xdoddoclodxOKKKKKKKKKKKKKKKKKKK00 337 | 00000000000000KKKKKKKKKKKKKKKK0xoc:;;,;,,,,''';cldxO0KKKKKKKKKKKKK000 338 | 00000000000000KKKKKKKKKKKKKKx:'',,,'.,'...;,'''',;:clk0KKKKKKKKKKK000 339 | 00000000000000KKKKKKKKKKKKd;'',,,;;;'.,..,c;;,;;;;;:;;d0KKKKKKKKKK000 340 | 00000000000000KKKKKKKKKKx,',;:ccl;,c;';,,ol::coolc:;;,,x0KKKKKKKKK000 341 | 00000000000000KKKKKKKKOl;:;:clllll;;o;;;cooclddclllllc::kKKKKKKKKK000 342 | 00000000000000KKKKKK0o;:ccclccccooo:ooc:ddoddloddolc;;;:c0KKKKKKK0000 343 | 00000000000000KKKKKOccodolccclllooddddddxdxddxkkkkxxo;'';d0KKKKKK0000 344 | 00000000000000KKKKkcoddolllllclloodxxxxdddxdddxxxddool:'.;O0KKKKK0000 345 | 00000000000000000xloollcccc:cclclodkkxxxdddxxxkkxdlllolc,,x0KKKKK0000 346 | 0000000000000000xccllccccc:;,'',;:dxkxxddddxkkkxdollcc:cc;d0KKKKKK000 347 | 000000000000000kcc:::cllol:'......odxxdoccldxxxdollllc:;;:d0KKKKK0000 348 | 00000000000000klc;;;clcc::;'...';;;:cll..',cdddolccccccc;:x0KKKKK0000 349 | 0000000000000kdl;:cclllclllc::;,;.'.''o;,,'.;ccoooollllc:;x0KKKKK0000 350 | 000000000000kol;:;::coolcc:::,.....,..cd,....':lolclolllc;x0KKKK00000 351 | 00000000000Odl;:'cllol;''',;;;;::''.',:doc;,',::looc:lcol:x0K00000000 352 | 0000000000Oxl:c,:lolc,..',:clllollodoc;cllolccloolllcclollO0K00000000 353 | 0000000000xllc,:lool:'.,...o.;llxdo:loc;;ccodlolodldllolld00K0K000000 354 | 000000000Ooc::coooc,,.',;:lx,,...':;o;l;':o:oolccocdoldloO0000KK00000 355 | 00000000kol:clllc;;,.;::;:clllllolxc;.:c':ocldlccl;clldox000000000000 356 | 000000Odll:cccc;:;,';cllooodoollcloll;c:.:d:ooo;cl;oloddkO00000000000 357 | 0000OOddOdll;c,;;,,;;:cldodddoxdoodlcc:.,ox:o:lllocdlodx00O0000000000 358 | 000Oxdl:::ll,:,:;,';c,:oloddolkxddxolc.'coccocolcccoooc;oxO00KOOOO000 359 | dc;,'...';c,,:c:::'c:';cldoo;:odolxoc:.,o:oldlxol;lddl,.,lkO0KdlcckKO 360 | '.......,:''';cll:cc,,;:l:c,,;:oc;cdc,.;::dldoxd:ldol;,'..,:lo,,,,kOk 361 | .......';'.',:clcll,,;:l:;'..''c:,;cl'.';dxoooxlddl;',''..,,;'...,ool 362 | .......,,.'';;:cld;.;,do:..;:,':c',:c''';xxdldocol'..';,.......',;;,; 363 | .......'..'',,coxc'';:do'.clc:lco',o;',;cOxdol:cc:.....'..oxd;','.'.. 364 | '.......''..,:cxl;';;cx:''cll:clc'cl',:l:ko:c..;c:..';...,KNNl;:;ll:' 365 | .......''...;,ooc,,,:od'.':cccdd,,l''cl:co;;,..;;'..','..;d0O,;;:XXXK 366 | ............'cll;',,lo'.'.::codl,c..:c;doc.,:.',....'...'......'l0XKk 367 | '............c;;,':lc.'',.;ccol;:,.:c.:o,;'.;'......,...',,.'...'.,;; 368 | .............',;;,cc..;,'';:lc':;..c'.c:;.,......,'..'...'',:,,;;,... 369 | ..............',,;:'.';,',:c;.;;..';..,;,.........''..'...'kko.,,.... 370 | ...............;,:'..;''';:,..;''.''..''............'...'.lK0c';;c;'. 371 | ...............,,'...,.',;''...''....,......'............'dOx',;:dd,' 372 | ..............',.....'.,;..'..',..........'..............';:;',,ldo.' 373 | .............'''.'.....,'..',','..'...''..'............'.......,dx'.' 374 | .......................,...';,'..'.....,.'.............''.'......'..' 375 | ...........'......'...',..'';,'..'.....................',';,..'....'. 376 | ``` 377 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | analyzer: 4 | language: 5 | strict-raw-types: true 6 | exclude: 7 | - lib/generated_plugin_registrant.dart 8 | errors: 9 | # allow self-reference to deprecated members (we do this because otherwise we have 10 | # to annotate every member in every test, assert, etc, when we deprecate something) 11 | deprecated_member_use_from_same_package: ignore 12 | 13 | linter: 14 | rules: 15 | close_sinks: true 16 | sort_constructors_first: true 17 | sort_pub_dependencies: false 18 | -------------------------------------------------------------------------------- /assets/CupertinoControls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/chewie/4340bc1bfbdbba232f15879dff53d5d4e4617285/assets/CupertinoControls.png -------------------------------------------------------------------------------- /assets/MaterialControls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/chewie/4340bc1bfbdbba232f15879dff53d5d4e4617285/assets/MaterialControls.png -------------------------------------------------------------------------------- /assets/MaterialDesktopControls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/chewie/4340bc1bfbdbba232f15879dff53d5d4e4617285/assets/MaterialDesktopControls.png -------------------------------------------------------------------------------- /assets/Options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/chewie/4340bc1bfbdbba232f15879dff53d5d4e4617285/assets/Options.png -------------------------------------------------------------------------------- /devtools_options.yaml: -------------------------------------------------------------------------------- 1 | description: This file stores settings for Dart & Flutter DevTools. 2 | documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states 3 | extensions: 4 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.lock 4 | *.log 5 | *.pyc 6 | *.swp 7 | .DS_Store 8 | .atom/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # Visual Studio Code related 20 | .classpath 21 | .project 22 | .settings/ 23 | .vscode/ 24 | 25 | # Flutter repo-specific 26 | /bin/cache/ 27 | /bin/internal/bootstrap.bat 28 | /bin/internal/bootstrap.sh 29 | /bin/mingit/ 30 | /dev/benchmarks/mega_gallery/ 31 | /dev/bots/.recipe_deps 32 | /dev/bots/android_tools/ 33 | /dev/devicelab/ABresults*.json 34 | /dev/docs/doc/ 35 | /dev/docs/flutter.docs.zip 36 | /dev/docs/lib/ 37 | /dev/docs/pubspec.yaml 38 | /dev/integration_tests/**/xcuserdata 39 | /dev/integration_tests/**/Pods 40 | /packages/flutter/coverage/ 41 | version 42 | analysis_benchmark.json 43 | 44 | # packages file containing multi-root paths 45 | .packages.generated 46 | 47 | # Flutter/Dart/Pub related 48 | **/doc/api/ 49 | .dart_tool/ 50 | .flutter-plugins 51 | .flutter-plugins-dependencies 52 | **/generated_plugin_registrant.dart 53 | .packages 54 | .pub-cache/ 55 | .pub/ 56 | build/ 57 | flutter_*.png 58 | linked_*.ds 59 | unlinked.ds 60 | unlinked_spec.ds 61 | 62 | # Android related 63 | **/android/**/gradle-wrapper.jar 64 | **/android/.gradle 65 | **/android/captures/ 66 | **/android/gradlew 67 | **/android/gradlew.bat 68 | **/android/local.properties 69 | **/android/**/GeneratedPluginRegistrant.java 70 | **/android/key.properties 71 | *.jks 72 | 73 | # iOS/XCode related 74 | **/ios/**/*.mode1v3 75 | **/ios/**/*.mode2v3 76 | **/ios/**/*.moved-aside 77 | **/ios/**/*.pbxuser 78 | **/ios/**/*.perspectivev3 79 | **/ios/**/*sync/ 80 | **/ios/**/.sconsign.dblite 81 | **/ios/**/.tags* 82 | **/ios/**/.vagrant/ 83 | **/ios/**/DerivedData/ 84 | **/ios/**/Icon? 85 | **/ios/**/Pods/ 86 | **/ios/**/.symlinks/ 87 | **/ios/**/profile 88 | **/ios/**/xcuserdata 89 | **/ios/.generated/ 90 | **/ios/Flutter/.last_build_id 91 | **/ios/Flutter/App.framework 92 | **/ios/Flutter/Flutter.framework 93 | **/ios/Flutter/Flutter.podspec 94 | **/ios/Flutter/Generated.xcconfig 95 | **/ios/Flutter/app.flx 96 | **/ios/Flutter/app.zip 97 | **/ios/Flutter/flutter_assets/ 98 | **/ios/Flutter/flutter_export_environment.sh 99 | **/ios/ServiceDefinitions.json 100 | **/ios/Runner/GeneratedPluginRegistrant.* 101 | 102 | # macOS 103 | **/macos/Flutter/GeneratedPluginRegistrant.swift 104 | **/macos/Flutter/Flutter-Debug.xcconfig 105 | **/macos/Flutter/Flutter-Release.xcconfig 106 | **/macos/Flutter/Flutter-Profile.xcconfig 107 | 108 | # Coverage 109 | coverage/ 110 | 111 | # Symbols 112 | app.*.symbols 113 | 114 | # Exceptions to above rules. 115 | !**/ios/**/default.mode1v3 116 | !**/ios/**/default.mode2v3 117 | !**/ios/**/default.pbxuser 118 | !**/ios/**/default.perspectivev3 119 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 120 | !/dev/ci/**/Gemfile.lock -------------------------------------------------------------------------------- /example/.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: "b0850beeb25f6d5b10426284f506557f66181b36" 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: b0850beeb25f6d5b10426284f506557f66181b36 17 | base_revision: b0850beeb25f6d5b10426284f506557f66181b36 18 | - platform: ios 19 | create_revision: b0850beeb25f6d5b10426284f506557f66181b36 20 | base_revision: b0850beeb25f6d5b10426284f506557f66181b36 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Chewie Example 2 | 3 | An example of how to use the chewie for Flutter 4 | 5 | ## Getting Started 6 | 7 | For help getting started with Flutter, view our online 8 | [documentation](http://flutter.io/). 9 | -------------------------------------------------------------------------------- /example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | analyzer: 4 | language: 5 | strict-raw-types: true 6 | exclude: 7 | - lib/generated_plugin_registrant.dart 8 | 9 | linter: 10 | rules: 11 | close_sinks: true 12 | sort_constructors_first: true 13 | -------------------------------------------------------------------------------- /example/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 | -------------------------------------------------------------------------------- /example/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 | 8 | def localProperties = new Properties() 9 | def localPropertiesFile = rootProject.file('local.properties') 10 | if (localPropertiesFile.exists()) { 11 | localPropertiesFile.withReader('UTF-8') { reader -> 12 | localProperties.load(reader) 13 | } 14 | } 15 | 16 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 17 | if (flutterVersionCode == null) { 18 | flutterVersionCode = '1' 19 | } 20 | 21 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 22 | if (flutterVersionName == null) { 23 | flutterVersionName = '1.0' 24 | } 25 | 26 | def keystoreProperties = new Properties() 27 | def keystorePropertiesFile = rootProject.file('key.properties') 28 | if (keystorePropertiesFile.exists()) { 29 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 30 | } 31 | 32 | android { 33 | namespace "com.example.example" 34 | compileSdk 34 35 | 36 | compileOptions { 37 | sourceCompatibility JavaVersion.VERSION_17 38 | targetCompatibility JavaVersion.VERSION_17 39 | } 40 | 41 | kotlinOptions { 42 | jvmTarget = '17' 43 | } 44 | 45 | sourceSets { 46 | main.java.srcDirs += 'src/main/kotlin' 47 | } 48 | 49 | defaultConfig { 50 | applicationId "com.example.example" 51 | minSdk 21 52 | targetSdk 34 53 | versionCode flutterVersionCode.toInteger() 54 | versionName flutterVersionName 55 | } 56 | 57 | buildTypes { 58 | release { 59 | // TODO: Add your own signing config for the release build. 60 | // Signing with the debug keys for now, so `flutter run --release` works. 61 | signingConfig signingConfigs.debug 62 | } 63 | } 64 | } 65 | 66 | flutter { 67 | source '../..' 68 | } 69 | 70 | dependencies { 71 | } 72 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 14 | 18 | 22 | 27 | 31 | 32 | 33 | 34 | 35 | 36 | 38 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/example/example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.example 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/chewie/4340bc1bfbdbba232f15879dff53d5d4e4617285/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/chewie/4340bc1bfbdbba232f15879dff53d5d4e4617285/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/chewie/4340bc1bfbdbba232f15879dff53d5d4e4617285/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/chewie/4340bc1bfbdbba232f15879dff53d5d4e4617285/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/chewie/4340bc1bfbdbba232f15879dff53d5d4e4617285/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.buildDir = '../build' 9 | subprojects { 10 | project.buildDir = "${rootProject.buildDir}/${project.name}" 11 | project.evaluationDependsOn(':app') 12 | } 13 | 14 | tasks.register("clean", Delete) { 15 | delete rootProject.buildDir 16 | } -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip 7 | -------------------------------------------------------------------------------- /example/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 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id 'dev.flutter.flutter-plugin-loader' version '1.0.0' 21 | id 'com.android.application' version '8.3.1' apply false 22 | id 'org.jetbrains.kotlin.android' version '1.8.22' apply false 23 | } 24 | include ':app' -------------------------------------------------------------------------------- /example/devtools_options.yaml: -------------------------------------------------------------------------------- 1 | description: This file stores settings for Dart & Flutter DevTools. 2 | documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states 3 | extensions: 4 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/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 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | 4 | @main 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/chewie/4340bc1bfbdbba232f15879dff53d5d4e4617285/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/chewie/4340bc1bfbdbba232f15879dff53d5d4e4617285/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/chewie/4340bc1bfbdbba232f15879dff53d5d4e4617285/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/chewie/4340bc1bfbdbba232f15879dff53d5d4e4617285/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/chewie/4340bc1bfbdbba232f15879dff53d5d4e4617285/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/chewie/4340bc1bfbdbba232f15879dff53d5d4e4617285/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/chewie/4340bc1bfbdbba232f15879dff53d5d4e4617285/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/chewie/4340bc1bfbdbba232f15879dff53d5d4e4617285/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/chewie/4340bc1bfbdbba232f15879dff53d5d4e4617285/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/chewie/4340bc1bfbdbba232f15879dff53d5d4e4617285/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/chewie/4340bc1bfbdbba232f15879dff53d5d4e4617285/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/chewie/4340bc1bfbdbba232f15879dff53d5d4e4617285/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/chewie/4340bc1bfbdbba232f15879dff53d5d4e4617285/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/chewie/4340bc1bfbdbba232f15879dff53d5d4e4617285/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/chewie/4340bc1bfbdbba232f15879dff53d5d4e4617285/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/chewie/4340bc1bfbdbba232f15879dff53d5d4e4617285/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/chewie/4340bc1bfbdbba232f15879dff53d5d4e4617285/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/chewie/4340bc1bfbdbba232f15879dff53d5d4e4617285/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Example 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | example 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | CADisableMinimumFrameDurationOnPhone 47 | 48 | UIApplicationSupportsIndirectInputEvents 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /example/ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 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 | -------------------------------------------------------------------------------- /example/lib/app/app.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:chewie/chewie.dart'; 3 | import 'package:chewie_example/app/theme.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:video_player/video_player.dart'; 6 | 7 | class ChewieDemo extends StatefulWidget { 8 | const ChewieDemo({super.key, this.title = 'Chewie Demo'}); 9 | 10 | final String title; 11 | 12 | @override 13 | State createState() { 14 | return _ChewieDemoState(); 15 | } 16 | } 17 | 18 | class _ChewieDemoState extends State { 19 | TargetPlatform? _platform; 20 | late VideoPlayerController _videoPlayerController1; 21 | late VideoPlayerController _videoPlayerController2; 22 | ChewieController? _chewieController; 23 | int? bufferDelay; 24 | 25 | @override 26 | void initState() { 27 | super.initState(); 28 | initializePlayer(); 29 | } 30 | 31 | @override 32 | void dispose() { 33 | _videoPlayerController1.dispose(); 34 | _videoPlayerController2.dispose(); 35 | _chewieController?.dispose(); 36 | super.dispose(); 37 | } 38 | 39 | List srcs = [ 40 | "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", 41 | "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", 42 | "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4", 43 | ]; 44 | 45 | Future initializePlayer() async { 46 | _videoPlayerController1 = VideoPlayerController.networkUrl( 47 | Uri.parse(srcs[currPlayIndex]), 48 | ); 49 | _videoPlayerController2 = VideoPlayerController.networkUrl( 50 | Uri.parse(srcs[currPlayIndex]), 51 | ); 52 | await Future.wait([ 53 | _videoPlayerController1.initialize(), 54 | _videoPlayerController2.initialize(), 55 | ]); 56 | _createChewieController(); 57 | setState(() {}); 58 | } 59 | 60 | void _createChewieController() { 61 | // final subtitles = [ 62 | // Subtitle( 63 | // index: 0, 64 | // start: Duration.zero, 65 | // end: const Duration(seconds: 10), 66 | // text: 'Hello from subtitles', 67 | // ), 68 | // Subtitle( 69 | // index: 0, 70 | // start: const Duration(seconds: 10), 71 | // end: const Duration(seconds: 20), 72 | // text: 'Whats up? :)', 73 | // ), 74 | // ]; 75 | 76 | final subtitles = [ 77 | Subtitle( 78 | index: 0, 79 | start: Duration.zero, 80 | end: const Duration(seconds: 10), 81 | text: const TextSpan( 82 | children: [ 83 | TextSpan( 84 | text: 'Hello', 85 | style: TextStyle(color: Colors.red, fontSize: 22), 86 | ), 87 | TextSpan( 88 | text: ' from ', 89 | style: TextStyle(color: Colors.green, fontSize: 20), 90 | ), 91 | TextSpan( 92 | text: 'subtitles', 93 | style: TextStyle(color: Colors.blue, fontSize: 18), 94 | ), 95 | ], 96 | ), 97 | ), 98 | Subtitle( 99 | index: 0, 100 | start: const Duration(seconds: 10), 101 | end: const Duration(seconds: 20), 102 | text: 'Whats up? :)', 103 | // text: const TextSpan( 104 | // text: 'Whats up? :)', 105 | // style: TextStyle(color: Colors.amber, fontSize: 22, fontStyle: FontStyle.italic), 106 | // ), 107 | ), 108 | ]; 109 | 110 | _chewieController = ChewieController( 111 | videoPlayerController: _videoPlayerController1, 112 | autoPlay: true, 113 | looping: true, 114 | progressIndicatorDelay: 115 | bufferDelay != null ? Duration(milliseconds: bufferDelay!) : null, 116 | 117 | additionalOptions: (context) { 118 | return [ 119 | OptionItem( 120 | onTap: (context) => toggleVideo(), 121 | iconData: Icons.live_tv_sharp, 122 | title: 'Toggle Video Src', 123 | ), 124 | ]; 125 | }, 126 | subtitle: Subtitles(subtitles), 127 | showSubtitles: true, 128 | subtitleBuilder: (context, dynamic subtitle) => Container( 129 | padding: const EdgeInsets.all(10.0), 130 | child: subtitle is InlineSpan 131 | ? RichText(text: subtitle) 132 | : Text( 133 | subtitle.toString(), 134 | style: const TextStyle(color: Colors.black), 135 | ), 136 | ), 137 | 138 | hideControlsTimer: const Duration(seconds: 1), 139 | 140 | // Try playing around with some of these other options: 141 | 142 | // showControls: false, 143 | // materialProgressColors: ChewieProgressColors( 144 | // playedColor: Colors.red, 145 | // handleColor: Colors.blue, 146 | // backgroundColor: Colors.grey, 147 | // bufferedColor: Colors.lightGreen, 148 | // ), 149 | // placeholder: Container( 150 | // color: Colors.grey, 151 | // ), 152 | // autoInitialize: true, 153 | ); 154 | } 155 | 156 | int currPlayIndex = 0; 157 | 158 | Future toggleVideo() async { 159 | await _videoPlayerController1.pause(); 160 | currPlayIndex += 1; 161 | if (currPlayIndex >= srcs.length) { 162 | currPlayIndex = 0; 163 | } 164 | await initializePlayer(); 165 | } 166 | 167 | @override 168 | Widget build(BuildContext context) { 169 | return MaterialApp( 170 | title: widget.title, 171 | theme: AppTheme.light.copyWith( 172 | platform: _platform ?? Theme.of(context).platform, 173 | ), 174 | home: Scaffold( 175 | appBar: AppBar(title: Text(widget.title)), 176 | body: Column( 177 | children: [ 178 | Expanded( 179 | child: Center( 180 | child: _chewieController != null && 181 | _chewieController! 182 | .videoPlayerController.value.isInitialized 183 | ? Chewie(controller: _chewieController!) 184 | : const Column( 185 | mainAxisAlignment: MainAxisAlignment.center, 186 | children: [ 187 | CircularProgressIndicator(), 188 | SizedBox(height: 20), 189 | Text('Loading'), 190 | ], 191 | ), 192 | ), 193 | ), 194 | TextButton( 195 | onPressed: () { 196 | _chewieController?.enterFullScreen(); 197 | }, 198 | child: const Text('Fullscreen'), 199 | ), 200 | Row( 201 | children: [ 202 | Expanded( 203 | child: TextButton( 204 | onPressed: () { 205 | setState(() { 206 | _videoPlayerController1.pause(); 207 | _videoPlayerController1.seekTo(Duration.zero); 208 | _createChewieController(); 209 | }); 210 | }, 211 | child: const Padding( 212 | padding: EdgeInsets.symmetric(vertical: 16.0), 213 | child: Text("Landscape Video"), 214 | ), 215 | ), 216 | ), 217 | Expanded( 218 | child: TextButton( 219 | onPressed: () { 220 | setState(() { 221 | _videoPlayerController2.pause(); 222 | _videoPlayerController2.seekTo(Duration.zero); 223 | _chewieController = _chewieController!.copyWith( 224 | videoPlayerController: _videoPlayerController2, 225 | autoPlay: true, 226 | looping: true, 227 | /* subtitle: Subtitles([ 228 | Subtitle( 229 | index: 0, 230 | start: Duration.zero, 231 | end: const Duration(seconds: 10), 232 | text: 'Hello from subtitles', 233 | ), 234 | Subtitle( 235 | index: 0, 236 | start: const Duration(seconds: 10), 237 | end: const Duration(seconds: 20), 238 | text: 'Whats up? :)', 239 | ), 240 | ]), 241 | subtitleBuilder: (context, subtitle) => Container( 242 | padding: const EdgeInsets.all(10.0), 243 | child: Text( 244 | subtitle, 245 | style: const TextStyle(color: Colors.white), 246 | ), 247 | ), */ 248 | ); 249 | }); 250 | }, 251 | child: const Padding( 252 | padding: EdgeInsets.symmetric(vertical: 16.0), 253 | child: Text("Portrait Video"), 254 | ), 255 | ), 256 | ), 257 | ], 258 | ), 259 | Row( 260 | children: [ 261 | Expanded( 262 | child: TextButton( 263 | onPressed: () { 264 | setState(() { 265 | _platform = TargetPlatform.android; 266 | }); 267 | }, 268 | child: const Padding( 269 | padding: EdgeInsets.symmetric(vertical: 16.0), 270 | child: Text("Android controls"), 271 | ), 272 | ), 273 | ), 274 | Expanded( 275 | child: TextButton( 276 | onPressed: () { 277 | setState(() { 278 | _platform = TargetPlatform.iOS; 279 | }); 280 | }, 281 | child: const Padding( 282 | padding: EdgeInsets.symmetric(vertical: 16.0), 283 | child: Text("iOS controls"), 284 | ), 285 | ), 286 | ), 287 | ], 288 | ), 289 | Row( 290 | children: [ 291 | Expanded( 292 | child: TextButton( 293 | onPressed: () { 294 | setState(() { 295 | _platform = TargetPlatform.windows; 296 | }); 297 | }, 298 | child: const Padding( 299 | padding: EdgeInsets.symmetric(vertical: 16.0), 300 | child: Text("Desktop controls"), 301 | ), 302 | ), 303 | ), 304 | ], 305 | ), 306 | if (Theme.of(context).platform == TargetPlatform.android) 307 | ListTile( 308 | title: const Text("Delay"), 309 | subtitle: DelaySlider( 310 | delay: 311 | _chewieController?.progressIndicatorDelay?.inMilliseconds, 312 | onSave: (delay) async { 313 | if (delay != null) { 314 | bufferDelay = delay == 0 ? null : delay; 315 | await initializePlayer(); 316 | } 317 | }, 318 | ), 319 | ), 320 | ], 321 | ), 322 | ), 323 | ); 324 | } 325 | } 326 | 327 | class DelaySlider extends StatefulWidget { 328 | const DelaySlider({super.key, required this.delay, required this.onSave}); 329 | 330 | final int? delay; 331 | final void Function(int?) onSave; 332 | @override 333 | State createState() => _DelaySliderState(); 334 | } 335 | 336 | class _DelaySliderState extends State { 337 | int? delay; 338 | bool saved = false; 339 | 340 | @override 341 | void initState() { 342 | super.initState(); 343 | delay = widget.delay; 344 | } 345 | 346 | @override 347 | Widget build(BuildContext context) { 348 | const int max = 1000; 349 | return ListTile( 350 | title: Text( 351 | "Progress indicator delay ${delay != null ? "${delay.toString()} MS" : ""}", 352 | ), 353 | subtitle: Slider( 354 | value: delay != null ? (delay! / max) : 0, 355 | onChanged: (value) async { 356 | delay = (value * max).toInt(); 357 | setState(() { 358 | saved = false; 359 | }); 360 | }, 361 | ), 362 | trailing: IconButton( 363 | icon: const Icon(Icons.save), 364 | onPressed: saved 365 | ? null 366 | : () { 367 | widget.onSave(delay); 368 | setState(() { 369 | saved = true; 370 | }); 371 | }, 372 | ), 373 | ); 374 | } 375 | } 376 | -------------------------------------------------------------------------------- /example/lib/app/theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | // ignore: avoid_classes_with_only_static_members 4 | class AppTheme { 5 | static final light = ThemeData( 6 | brightness: Brightness.light, 7 | useMaterial3: true, 8 | colorScheme: const ColorScheme.light(secondary: Colors.red), 9 | disabledColor: Colors.grey.shade400, 10 | visualDensity: VisualDensity.adaptivePlatformDensity, 11 | ); 12 | 13 | static final dark = ThemeData( 14 | brightness: Brightness.dark, 15 | colorScheme: const ColorScheme.dark(secondary: Colors.red), 16 | disabledColor: Colors.grey.shade400, 17 | useMaterial3: true, 18 | visualDensity: VisualDensity.adaptivePlatformDensity, 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:chewie_example/app/app.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | void main() { 5 | runApp(const ChewieDemo()); 6 | } 7 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: chewie_example 2 | description: An example of how to use the chewie for Flutter 3 | version: 1.0.0 4 | publish_to: none 5 | 6 | environment: 7 | sdk: '>=3.6.0 <4.0.0' 8 | flutter: ">=3.27.0" 9 | 10 | dependencies: 11 | chewie: 12 | path: ../ 13 | flutter: 14 | sdk: flutter 15 | video_player: ^2.9.3 16 | 17 | dev_dependencies: 18 | flutter_test: 19 | sdk: flutter 20 | flutter_lints: ^5.0.0 21 | 22 | # For information on the generic Dart part of this file, see the 23 | # following page: https://www.dartlang.org/tools/pub/pubspec 24 | 25 | # The following section is specific to Flutter. 26 | flutter: 27 | # The following line ensures that the Material Icons font is 28 | # included with your application, so that you can use the icons in 29 | # the material Icons class. 30 | uses-material-design: true 31 | 32 | # To add assets to your application, add an assets section, like this: 33 | # assets: 34 | # - images/a_dot_burr.jpeg 35 | # - images/a_dot_ham.jpeg 36 | 37 | # An image asset can refer to one or more resolution-specific "variants", see 38 | # https://flutter.io/assets-and-images/#resolution-aware. 39 | 40 | # For details regarding adding assets from package dependencies, see 41 | # https://flutter.io/assets-and-images/#from-packages 42 | 43 | # To add custom fonts to your application, add a fonts section here, 44 | # in this "flutter" section. Each entry in this list should have a 45 | # "family" key with the font family name, and a "fonts" key with a 46 | # list giving the asset and other descriptors for the font. For 47 | # example: 48 | # fonts: 49 | # - family: Schyler 50 | # fonts: 51 | # - asset: fonts/Schyler-Regular.ttf 52 | # - asset: fonts/Schyler-Italic.ttf 53 | # style: italic 54 | # - family: Trajan Pro 55 | # fonts: 56 | # - asset: fonts/TrajanPro.ttf 57 | # - asset: fonts/TrajanPro_Bold.ttf 58 | # weight: 700 59 | # 60 | # For details regarding fonts from package dependencies, 61 | # see https://flutter.io/custom-fonts/#from-packages 62 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:chewie_example/app/app.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | void main() { 12 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 13 | // Build our app and trigger a frame. 14 | await tester.pumpWidget(const ChewieDemo()); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /example/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/chewie/4340bc1bfbdbba232f15879dff53d5d4e4617285/example/web/favicon.png -------------------------------------------------------------------------------- /example/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/chewie/4340bc1bfbdbba232f15879dff53d5d4e4617285/example/web/icons/Icon-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/chewie/4340bc1bfbdbba232f15879dff53d5d4e4617285/example/web/icons/Icon-512.png -------------------------------------------------------------------------------- /example/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | example 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "short_name": "example", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /lib/chewie.dart: -------------------------------------------------------------------------------- 1 | library; 2 | 3 | export 'src/chewie_player.dart'; 4 | export 'src/chewie_progress_colors.dart'; 5 | export 'src/cupertino/cupertino_controls.dart'; 6 | export 'src/material/material_controls.dart'; 7 | export 'src/material/material_desktop_controls.dart'; 8 | export 'src/material/material_progress_bar.dart'; 9 | export 'src/models/index.dart'; 10 | -------------------------------------------------------------------------------- /lib/src/animated_play_pause.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// A widget that animates implicitly between a play and a pause icon. 4 | class AnimatedPlayPause extends StatefulWidget { 5 | const AnimatedPlayPause({ 6 | super.key, 7 | required this.playing, 8 | this.size, 9 | this.color, 10 | }); 11 | 12 | final double? size; 13 | final bool playing; 14 | final Color? color; 15 | 16 | @override 17 | State createState() => AnimatedPlayPauseState(); 18 | } 19 | 20 | class AnimatedPlayPauseState extends State 21 | with SingleTickerProviderStateMixin { 22 | late final animationController = AnimationController( 23 | vsync: this, 24 | value: widget.playing ? 1 : 0, 25 | duration: const Duration(milliseconds: 400), 26 | ); 27 | 28 | @override 29 | void didUpdateWidget(AnimatedPlayPause oldWidget) { 30 | super.didUpdateWidget(oldWidget); 31 | if (widget.playing != oldWidget.playing) { 32 | if (widget.playing) { 33 | animationController.forward(); 34 | } else { 35 | animationController.reverse(); 36 | } 37 | } 38 | } 39 | 40 | @override 41 | void dispose() { 42 | animationController.dispose(); 43 | super.dispose(); 44 | } 45 | 46 | @override 47 | Widget build(BuildContext context) { 48 | return Center( 49 | child: AnimatedIcon( 50 | color: widget.color, 51 | size: widget.size, 52 | icon: AnimatedIcons.play_pause, 53 | progress: animationController, 54 | ), 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/src/center_play_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:chewie/src/animated_play_pause.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class CenterPlayButton extends StatelessWidget { 5 | const CenterPlayButton({ 6 | super.key, 7 | required this.backgroundColor, 8 | this.iconColor, 9 | required this.show, 10 | required this.isPlaying, 11 | required this.isFinished, 12 | this.onPressed, 13 | }); 14 | 15 | final Color backgroundColor; 16 | final Color? iconColor; 17 | final bool show; 18 | final bool isPlaying; 19 | final bool isFinished; 20 | final VoidCallback? onPressed; 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | return ColoredBox( 25 | color: Colors.transparent, 26 | child: Center( 27 | child: UnconstrainedBox( 28 | child: AnimatedOpacity( 29 | opacity: show ? 1.0 : 0.0, 30 | duration: const Duration(milliseconds: 300), 31 | child: DecoratedBox( 32 | decoration: BoxDecoration( 33 | color: backgroundColor, 34 | shape: BoxShape.circle, 35 | ), 36 | // Always set the iconSize on the IconButton, not on the Icon itself: 37 | // https://github.com/flutter/flutter/issues/52980 38 | child: IconButton( 39 | iconSize: 32, 40 | padding: const EdgeInsets.all(12.0), 41 | icon: isFinished 42 | ? Icon(Icons.replay, color: iconColor) 43 | : AnimatedPlayPause( 44 | color: iconColor, 45 | playing: isPlaying, 46 | ), 47 | onPressed: onPressed, 48 | ), 49 | ), 50 | ), 51 | ), 52 | ), 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/src/center_seek_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CenterSeekButton extends StatelessWidget { 4 | const CenterSeekButton({ 5 | super.key, 6 | required this.iconData, 7 | required this.backgroundColor, 8 | this.iconColor, 9 | required this.show, 10 | this.fadeDuration = const Duration(milliseconds: 300), 11 | this.iconSize = 26, 12 | this.onPressed, 13 | }); 14 | 15 | final IconData iconData; 16 | final Color backgroundColor; 17 | final Color? iconColor; 18 | final bool show; 19 | final VoidCallback? onPressed; 20 | final Duration fadeDuration; 21 | final double iconSize; 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return ColoredBox( 26 | color: Colors.transparent, 27 | child: Center( 28 | child: UnconstrainedBox( 29 | child: AnimatedOpacity( 30 | opacity: show ? 1.0 : 0.0, 31 | duration: fadeDuration, 32 | child: DecoratedBox( 33 | decoration: BoxDecoration( 34 | color: backgroundColor, 35 | shape: BoxShape.circle, 36 | ), 37 | // Always set the iconSize on the IconButton, not on the Icon itself: 38 | // https://github.com/flutter/flutter/issues/52980 39 | child: IconButton( 40 | iconSize: iconSize, 41 | padding: const EdgeInsets.all(8.0), 42 | icon: Icon(iconData, color: iconColor), 43 | onPressed: onPressed, 44 | ), 45 | ), 46 | ), 47 | ), 48 | ), 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/src/chewie_player.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:chewie/src/chewie_progress_colors.dart'; 4 | import 'package:chewie/src/models/option_item.dart'; 5 | import 'package:chewie/src/models/options_translation.dart'; 6 | import 'package:chewie/src/models/subtitle_model.dart'; 7 | import 'package:chewie/src/notifiers/player_notifier.dart'; 8 | import 'package:chewie/src/player_with_controls.dart'; 9 | import 'package:flutter/foundation.dart'; 10 | import 'package:flutter/material.dart'; 11 | import 'package:flutter/services.dart'; 12 | import 'package:provider/provider.dart'; 13 | import 'package:video_player/video_player.dart'; 14 | import 'package:wakelock_plus/wakelock_plus.dart'; 15 | 16 | typedef ChewieRoutePageBuilder = Widget Function( 17 | BuildContext context, 18 | Animation animation, 19 | Animation secondaryAnimation, 20 | ChewieControllerProvider controllerProvider, 21 | ); 22 | 23 | /// A Video Player with Material and Cupertino skins. 24 | /// 25 | /// `video_player` is pretty low level. Chewie wraps it in a friendly skin to 26 | /// make it easy to use! 27 | class Chewie extends StatefulWidget { 28 | const Chewie({ 29 | super.key, 30 | required this.controller, 31 | }); 32 | 33 | /// The [ChewieController] 34 | final ChewieController controller; 35 | 36 | @override 37 | ChewieState createState() { 38 | return ChewieState(); 39 | } 40 | } 41 | 42 | class ChewieState extends State { 43 | bool _isFullScreen = false; 44 | 45 | bool get isControllerFullScreen => widget.controller.isFullScreen; 46 | late PlayerNotifier notifier; 47 | 48 | @override 49 | void initState() { 50 | super.initState(); 51 | widget.controller.addListener(listener); 52 | notifier = PlayerNotifier.init(); 53 | } 54 | 55 | @override 56 | void dispose() { 57 | widget.controller.removeListener(listener); 58 | notifier.dispose(); 59 | super.dispose(); 60 | } 61 | 62 | @override 63 | void didUpdateWidget(Chewie oldWidget) { 64 | if (oldWidget.controller != widget.controller) { 65 | widget.controller.addListener(listener); 66 | } 67 | super.didUpdateWidget(oldWidget); 68 | if (_isFullScreen != isControllerFullScreen) { 69 | widget.controller._isFullScreen = _isFullScreen; 70 | } 71 | } 72 | 73 | Future listener() async { 74 | if (isControllerFullScreen && !_isFullScreen) { 75 | _isFullScreen = isControllerFullScreen; 76 | await _pushFullScreenWidget(context); 77 | } else if (_isFullScreen) { 78 | Navigator.of( 79 | context, 80 | rootNavigator: widget.controller.useRootNavigator, 81 | ).pop(); 82 | _isFullScreen = false; 83 | } 84 | } 85 | 86 | @override 87 | Widget build(BuildContext context) { 88 | return ChewieControllerProvider( 89 | controller: widget.controller, 90 | child: ChangeNotifierProvider.value( 91 | value: notifier, 92 | builder: (context, w) => const PlayerWithControls(), 93 | ), 94 | ); 95 | } 96 | 97 | Widget _buildFullScreenVideo( 98 | BuildContext context, 99 | Animation animation, 100 | ChewieControllerProvider controllerProvider, 101 | ) { 102 | return Scaffold( 103 | resizeToAvoidBottomInset: false, 104 | body: Container( 105 | alignment: Alignment.center, 106 | color: Colors.black, 107 | child: controllerProvider, 108 | ), 109 | ); 110 | } 111 | 112 | AnimatedWidget _defaultRoutePageBuilder( 113 | BuildContext context, 114 | Animation animation, 115 | Animation secondaryAnimation, 116 | ChewieControllerProvider controllerProvider, 117 | ) { 118 | return AnimatedBuilder( 119 | animation: animation, 120 | builder: (BuildContext context, Widget? child) { 121 | return _buildFullScreenVideo(context, animation, controllerProvider); 122 | }, 123 | ); 124 | } 125 | 126 | Widget _fullScreenRoutePageBuilder( 127 | BuildContext context, 128 | Animation animation, 129 | Animation secondaryAnimation, 130 | ) { 131 | final controllerProvider = ChewieControllerProvider( 132 | controller: widget.controller, 133 | child: ChangeNotifierProvider.value( 134 | value: notifier, 135 | builder: (context, w) => const PlayerWithControls(), 136 | ), 137 | ); 138 | 139 | if (widget.controller.routePageBuilder == null) { 140 | return _defaultRoutePageBuilder( 141 | context, 142 | animation, 143 | secondaryAnimation, 144 | controllerProvider, 145 | ); 146 | } 147 | return widget.controller.routePageBuilder!( 148 | context, 149 | animation, 150 | secondaryAnimation, 151 | controllerProvider, 152 | ); 153 | } 154 | 155 | Future _pushFullScreenWidget(BuildContext context) async { 156 | final TransitionRoute route = PageRouteBuilder( 157 | pageBuilder: _fullScreenRoutePageBuilder, 158 | ); 159 | 160 | onEnterFullScreen(); 161 | 162 | if (!widget.controller.allowedScreenSleep) { 163 | WakelockPlus.enable(); 164 | } 165 | 166 | await Navigator.of( 167 | context, 168 | rootNavigator: widget.controller.useRootNavigator, 169 | ).push(route); 170 | 171 | if (kIsWeb) { 172 | _reInitializeControllers(); 173 | } 174 | 175 | _isFullScreen = false; 176 | widget.controller.exitFullScreen(); 177 | 178 | if (!widget.controller.allowedScreenSleep) { 179 | WakelockPlus.disable(); 180 | } 181 | 182 | SystemChrome.setEnabledSystemUIMode( 183 | SystemUiMode.manual, 184 | overlays: widget.controller.systemOverlaysAfterFullScreen, 185 | ); 186 | SystemChrome.setPreferredOrientations( 187 | widget.controller.deviceOrientationsAfterFullScreen, 188 | ); 189 | } 190 | 191 | void onEnterFullScreen() { 192 | final videoWidth = widget.controller.videoPlayerController.value.size.width; 193 | final videoHeight = 194 | widget.controller.videoPlayerController.value.size.height; 195 | 196 | SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); 197 | 198 | // if (widget.controller.systemOverlaysOnEnterFullScreen != null) { 199 | // /// Optional user preferred settings 200 | // SystemChrome.setEnabledSystemUIMode( 201 | // SystemUiMode.manual, 202 | // overlays: widget.controller.systemOverlaysOnEnterFullScreen, 203 | // ); 204 | // } else { 205 | // /// Default behavior 206 | // SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); 207 | // } 208 | 209 | if (widget.controller.deviceOrientationsOnEnterFullScreen != null) { 210 | /// Optional user preferred settings 211 | SystemChrome.setPreferredOrientations( 212 | widget.controller.deviceOrientationsOnEnterFullScreen!, 213 | ); 214 | } else { 215 | final isLandscapeVideo = videoWidth > videoHeight; 216 | final isPortraitVideo = videoWidth < videoHeight; 217 | 218 | /// Default behavior 219 | /// Video w > h means we force landscape 220 | if (isLandscapeVideo) { 221 | SystemChrome.setPreferredOrientations([ 222 | DeviceOrientation.landscapeLeft, 223 | DeviceOrientation.landscapeRight, 224 | ]); 225 | } 226 | 227 | /// Video h > w means we force portrait 228 | else if (isPortraitVideo) { 229 | SystemChrome.setPreferredOrientations([ 230 | DeviceOrientation.portraitUp, 231 | DeviceOrientation.portraitDown, 232 | ]); 233 | } 234 | 235 | /// Otherwise if h == w (square video) 236 | else { 237 | SystemChrome.setPreferredOrientations(DeviceOrientation.values); 238 | } 239 | } 240 | } 241 | 242 | ///When viewing full screen on web, returning from full screen causes original video to lose the picture. 243 | ///We re initialise controllers for web only when returning from full screen 244 | void _reInitializeControllers() { 245 | final prevPosition = widget.controller.videoPlayerController.value.position; 246 | widget.controller.videoPlayerController.initialize().then((_) async { 247 | widget.controller._initialize(); 248 | widget.controller.videoPlayerController.seekTo(prevPosition); 249 | await widget.controller.videoPlayerController.play(); 250 | widget.controller.videoPlayerController.pause(); 251 | }); 252 | } 253 | } 254 | 255 | /// The ChewieController is used to configure and drive the Chewie Player 256 | /// Widgets. It provides methods to control playback, such as [pause] and 257 | /// [play], as well as methods that control the visual appearance of the player, 258 | /// such as [enterFullScreen] or [exitFullScreen]. 259 | /// 260 | /// In addition, you can listen to the ChewieController for presentational 261 | /// changes, such as entering and exiting full screen mode. To listen for 262 | /// changes to the playback, such as a change to the seek position of the 263 | /// player, please use the standard information provided by the 264 | /// `VideoPlayerController`. 265 | class ChewieController extends ChangeNotifier { 266 | ChewieController({ 267 | required this.videoPlayerController, 268 | this.optionsTranslation, 269 | this.aspectRatio, 270 | this.autoInitialize = false, 271 | this.autoPlay = false, 272 | this.draggableProgressBar = true, 273 | this.startAt, 274 | this.looping = false, 275 | this.fullScreenByDefault = false, 276 | this.cupertinoProgressColors, 277 | this.materialProgressColors, 278 | this.materialSeekButtonFadeDuration = const Duration(milliseconds: 300), 279 | this.materialSeekButtonSize = 26, 280 | this.placeholder, 281 | this.overlay, 282 | this.showControlsOnInitialize = true, 283 | this.showOptions = true, 284 | this.optionsBuilder, 285 | this.additionalOptions, 286 | this.showControls = true, 287 | this.transformationController, 288 | this.zoomAndPan = false, 289 | this.maxScale = 2.5, 290 | this.subtitle, 291 | this.showSubtitles = false, 292 | this.subtitleBuilder, 293 | this.customControls, 294 | this.errorBuilder, 295 | this.bufferingBuilder, 296 | this.allowedScreenSleep = true, 297 | this.isLive = false, 298 | this.allowFullScreen = true, 299 | this.allowMuting = true, 300 | this.allowPlaybackSpeedChanging = true, 301 | this.useRootNavigator = true, 302 | this.playbackSpeeds = const [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2], 303 | this.systemOverlaysOnEnterFullScreen, 304 | this.deviceOrientationsOnEnterFullScreen, 305 | this.systemOverlaysAfterFullScreen = SystemUiOverlay.values, 306 | this.deviceOrientationsAfterFullScreen = DeviceOrientation.values, 307 | this.routePageBuilder, 308 | this.progressIndicatorDelay, 309 | this.hideControlsTimer = defaultHideControlsTimer, 310 | this.controlsSafeAreaMinimum = EdgeInsets.zero, 311 | this.pauseOnBackgroundTap = false, 312 | }) : assert( 313 | playbackSpeeds.every((speed) => speed > 0), 314 | 'The playbackSpeeds values must all be greater than 0', 315 | ) { 316 | _initialize(); 317 | } 318 | 319 | ChewieController copyWith({ 320 | VideoPlayerController? videoPlayerController, 321 | OptionsTranslation? optionsTranslation, 322 | double? aspectRatio, 323 | bool? autoInitialize, 324 | bool? autoPlay, 325 | bool? draggableProgressBar, 326 | Duration? startAt, 327 | bool? looping, 328 | bool? fullScreenByDefault, 329 | ChewieProgressColors? cupertinoProgressColors, 330 | ChewieProgressColors? materialProgressColors, 331 | Duration? materialSeekButtonFadeDuration, 332 | double? materialSeekButtonSize, 333 | Widget? placeholder, 334 | Widget? overlay, 335 | bool? showControlsOnInitialize, 336 | bool? showOptions, 337 | Future Function(BuildContext, List)? optionsBuilder, 338 | List Function(BuildContext)? additionalOptions, 339 | bool? showControls, 340 | TransformationController? transformationController, 341 | bool? zoomAndPan, 342 | double? maxScale, 343 | Subtitles? subtitle, 344 | bool? showSubtitles, 345 | Widget Function(BuildContext, dynamic)? subtitleBuilder, 346 | Widget? customControls, 347 | WidgetBuilder? bufferingBuilder, 348 | Widget Function(BuildContext, String)? errorBuilder, 349 | bool? allowedScreenSleep, 350 | bool? isLive, 351 | bool? allowFullScreen, 352 | bool? allowMuting, 353 | bool? allowPlaybackSpeedChanging, 354 | bool? useRootNavigator, 355 | Duration? hideControlsTimer, 356 | EdgeInsets? controlsSafeAreaMinimum, 357 | List? playbackSpeeds, 358 | List? systemOverlaysOnEnterFullScreen, 359 | List? deviceOrientationsOnEnterFullScreen, 360 | List? systemOverlaysAfterFullScreen, 361 | List? deviceOrientationsAfterFullScreen, 362 | Duration? progressIndicatorDelay, 363 | Widget Function( 364 | BuildContext, 365 | Animation, 366 | Animation, 367 | ChewieControllerProvider, 368 | )? routePageBuilder, 369 | bool? pauseOnBackgroundTap, 370 | }) { 371 | return ChewieController( 372 | draggableProgressBar: draggableProgressBar ?? this.draggableProgressBar, 373 | videoPlayerController: 374 | videoPlayerController ?? this.videoPlayerController, 375 | optionsTranslation: optionsTranslation ?? this.optionsTranslation, 376 | aspectRatio: aspectRatio ?? this.aspectRatio, 377 | autoInitialize: autoInitialize ?? this.autoInitialize, 378 | autoPlay: autoPlay ?? this.autoPlay, 379 | startAt: startAt ?? this.startAt, 380 | looping: looping ?? this.looping, 381 | fullScreenByDefault: fullScreenByDefault ?? this.fullScreenByDefault, 382 | cupertinoProgressColors: 383 | cupertinoProgressColors ?? this.cupertinoProgressColors, 384 | materialProgressColors: 385 | materialProgressColors ?? this.materialProgressColors, 386 | materialSeekButtonFadeDuration: 387 | materialSeekButtonFadeDuration ?? this.materialSeekButtonFadeDuration, 388 | materialSeekButtonSize: 389 | materialSeekButtonSize ?? this.materialSeekButtonSize, 390 | placeholder: placeholder ?? this.placeholder, 391 | overlay: overlay ?? this.overlay, 392 | showControlsOnInitialize: 393 | showControlsOnInitialize ?? this.showControlsOnInitialize, 394 | showOptions: showOptions ?? this.showOptions, 395 | optionsBuilder: optionsBuilder ?? this.optionsBuilder, 396 | additionalOptions: additionalOptions ?? this.additionalOptions, 397 | showControls: showControls ?? this.showControls, 398 | showSubtitles: showSubtitles ?? this.showSubtitles, 399 | subtitle: subtitle ?? this.subtitle, 400 | subtitleBuilder: subtitleBuilder ?? this.subtitleBuilder, 401 | customControls: customControls ?? this.customControls, 402 | errorBuilder: errorBuilder ?? this.errorBuilder, 403 | bufferingBuilder: bufferingBuilder ?? this.bufferingBuilder, 404 | allowedScreenSleep: allowedScreenSleep ?? this.allowedScreenSleep, 405 | isLive: isLive ?? this.isLive, 406 | allowFullScreen: allowFullScreen ?? this.allowFullScreen, 407 | allowMuting: allowMuting ?? this.allowMuting, 408 | allowPlaybackSpeedChanging: 409 | allowPlaybackSpeedChanging ?? this.allowPlaybackSpeedChanging, 410 | useRootNavigator: useRootNavigator ?? this.useRootNavigator, 411 | playbackSpeeds: playbackSpeeds ?? this.playbackSpeeds, 412 | systemOverlaysOnEnterFullScreen: systemOverlaysOnEnterFullScreen ?? 413 | this.systemOverlaysOnEnterFullScreen, 414 | deviceOrientationsOnEnterFullScreen: 415 | deviceOrientationsOnEnterFullScreen ?? 416 | this.deviceOrientationsOnEnterFullScreen, 417 | systemOverlaysAfterFullScreen: 418 | systemOverlaysAfterFullScreen ?? this.systemOverlaysAfterFullScreen, 419 | deviceOrientationsAfterFullScreen: deviceOrientationsAfterFullScreen ?? 420 | this.deviceOrientationsAfterFullScreen, 421 | routePageBuilder: routePageBuilder ?? this.routePageBuilder, 422 | hideControlsTimer: hideControlsTimer ?? this.hideControlsTimer, 423 | progressIndicatorDelay: 424 | progressIndicatorDelay ?? this.progressIndicatorDelay, 425 | pauseOnBackgroundTap: pauseOnBackgroundTap ?? this.pauseOnBackgroundTap, 426 | ); 427 | } 428 | 429 | static const defaultHideControlsTimer = Duration(seconds: 3); 430 | 431 | /// If false, the options button in MaterialUI and MaterialDesktopUI 432 | /// won't be shown. 433 | final bool showOptions; 434 | 435 | /// Pass your translations for the options like: 436 | /// - PlaybackSpeed 437 | /// - Subtitles 438 | /// - Cancel 439 | /// 440 | /// Buttons 441 | /// 442 | /// These are required for the default `OptionItem`'s 443 | final OptionsTranslation? optionsTranslation; 444 | 445 | /// Build your own options with default chewieOptions shiped through 446 | /// the builder method. Just add your own options to the Widget 447 | /// you'll build. If you want to hide the chewieOptions, just leave them 448 | /// out from your Widget. 449 | final Future Function( 450 | BuildContext context, 451 | List chewieOptions, 452 | )? optionsBuilder; 453 | 454 | /// Add your own additional options on top of chewie options 455 | final List Function(BuildContext context)? additionalOptions; 456 | 457 | /// Define here your own Widget on how your n'th subtitle will look like 458 | Widget Function(BuildContext context, dynamic subtitle)? subtitleBuilder; 459 | 460 | /// Add a List of Subtitles here in `Subtitles.subtitle` 461 | Subtitles? subtitle; 462 | 463 | /// Determines whether subtitles should be shown by default when the video starts. 464 | /// 465 | /// If set to `true`, subtitles will be displayed automatically when the video 466 | /// begins playing. If set to `false`, subtitles will be hidden by default. 467 | bool showSubtitles; 468 | 469 | /// The controller for the video you want to play 470 | final VideoPlayerController videoPlayerController; 471 | 472 | /// Initialize the Video on Startup. This will prep the video for playback. 473 | final bool autoInitialize; 474 | 475 | /// Play the video as soon as it's displayed 476 | final bool autoPlay; 477 | 478 | /// Non-Draggable Progress Bar 479 | final bool draggableProgressBar; 480 | 481 | /// Start video at a certain position 482 | final Duration? startAt; 483 | 484 | /// Whether or not the video should loop 485 | final bool looping; 486 | 487 | /// Wether or not to show the controls when initializing the widget. 488 | final bool showControlsOnInitialize; 489 | 490 | /// Whether or not to show the controls at all 491 | final bool showControls; 492 | 493 | /// Controller to pass into the [InteractiveViewer] component 494 | final TransformationController? transformationController; 495 | 496 | /// Whether or not to allow zooming and panning 497 | final bool zoomAndPan; 498 | 499 | /// Max scale when zooming 500 | final double maxScale; 501 | 502 | /// Defines customised controls. Check [MaterialControls] or 503 | /// [CupertinoControls] for reference. 504 | final Widget? customControls; 505 | 506 | /// When the video playback runs into an error, you can build a custom 507 | /// error message. 508 | final Widget Function(BuildContext context, String errorMessage)? 509 | errorBuilder; 510 | 511 | /// When the video is buffering, you can build a custom widget. 512 | final WidgetBuilder? bufferingBuilder; 513 | 514 | /// The Aspect Ratio of the Video. Important to get the correct size of the 515 | /// video! 516 | /// 517 | /// Will fallback to fitting within the space allowed. 518 | final double? aspectRatio; 519 | 520 | /// The colors to use for controls on iOS. By default, the iOS player uses 521 | /// colors sampled from the original iOS 11 designs. 522 | final ChewieProgressColors? cupertinoProgressColors; 523 | 524 | /// The colors to use for the Material Progress Bar. By default, the Material 525 | /// player uses the colors from your Theme. 526 | final ChewieProgressColors? materialProgressColors; 527 | 528 | // The duration of the fade animation for the seek button (Material Player only) 529 | final Duration materialSeekButtonFadeDuration; 530 | 531 | // The size of the seek button for the Material Player only 532 | final double materialSeekButtonSize; 533 | 534 | /// The placeholder is displayed underneath the Video before it is initialized 535 | /// or played. 536 | final Widget? placeholder; 537 | 538 | /// A widget which is placed between the video and the controls 539 | final Widget? overlay; 540 | 541 | /// Defines if the player will start in fullscreen when play is pressed 542 | final bool fullScreenByDefault; 543 | 544 | /// Defines if the player will sleep in fullscreen or not 545 | final bool allowedScreenSleep; 546 | 547 | /// Defines if the controls should be shown for live stream video 548 | final bool isLive; 549 | 550 | /// Defines if the fullscreen control should be shown 551 | final bool allowFullScreen; 552 | 553 | /// Defines if the mute control should be shown 554 | final bool allowMuting; 555 | 556 | /// Defines if the playback speed control should be shown 557 | final bool allowPlaybackSpeedChanging; 558 | 559 | /// Defines if push/pop navigations use the rootNavigator 560 | final bool useRootNavigator; 561 | 562 | /// Defines the [Duration] before the video controls are hidden. By default, this is set to three seconds. 563 | final Duration hideControlsTimer; 564 | 565 | /// Defines the set of allowed playback speeds user can change 566 | final List playbackSpeeds; 567 | 568 | /// Defines the system overlays visible on entering fullscreen 569 | final List? systemOverlaysOnEnterFullScreen; 570 | 571 | /// Defines the set of allowed device orientations on entering fullscreen 572 | final List? deviceOrientationsOnEnterFullScreen; 573 | 574 | /// Defines the system overlays visible after exiting fullscreen 575 | final List systemOverlaysAfterFullScreen; 576 | 577 | /// Defines the set of allowed device orientations after exiting fullscreen 578 | final List deviceOrientationsAfterFullScreen; 579 | 580 | /// Defines a custom RoutePageBuilder for the fullscreen 581 | final ChewieRoutePageBuilder? routePageBuilder; 582 | 583 | /// Defines a delay in milliseconds between entering buffering state and displaying the loading spinner. Set null (default) to disable it. 584 | final Duration? progressIndicatorDelay; 585 | 586 | /// Adds additional padding to the controls' [SafeArea] as desired. 587 | /// Defaults to [EdgeInsets.zero]. 588 | final EdgeInsets controlsSafeAreaMinimum; 589 | 590 | /// Defines if the player should pause when the background is tapped 591 | final bool pauseOnBackgroundTap; 592 | 593 | static ChewieController of(BuildContext context) { 594 | final chewieControllerProvider = 595 | context.dependOnInheritedWidgetOfExactType()!; 596 | 597 | return chewieControllerProvider.controller; 598 | } 599 | 600 | bool _isFullScreen = false; 601 | 602 | bool get isFullScreen => _isFullScreen; 603 | 604 | bool get isPlaying => videoPlayerController.value.isPlaying; 605 | 606 | Future _initialize() async { 607 | await videoPlayerController.setLooping(looping); 608 | 609 | if ((autoInitialize || autoPlay) && 610 | !videoPlayerController.value.isInitialized) { 611 | await videoPlayerController.initialize(); 612 | } 613 | 614 | if (autoPlay) { 615 | if (fullScreenByDefault) { 616 | enterFullScreen(); 617 | } 618 | 619 | await videoPlayerController.play(); 620 | } 621 | 622 | if (startAt != null) { 623 | await videoPlayerController.seekTo(startAt!); 624 | } 625 | 626 | if (fullScreenByDefault) { 627 | videoPlayerController.addListener(_fullScreenListener); 628 | } 629 | } 630 | 631 | Future _fullScreenListener() async { 632 | if (videoPlayerController.value.isPlaying && !_isFullScreen) { 633 | enterFullScreen(); 634 | videoPlayerController.removeListener(_fullScreenListener); 635 | } 636 | } 637 | 638 | void enterFullScreen() { 639 | _isFullScreen = true; 640 | notifyListeners(); 641 | } 642 | 643 | void exitFullScreen() { 644 | _isFullScreen = false; 645 | notifyListeners(); 646 | } 647 | 648 | void toggleFullScreen() { 649 | _isFullScreen = !_isFullScreen; 650 | notifyListeners(); 651 | } 652 | 653 | void togglePause() { 654 | isPlaying ? pause() : play(); 655 | } 656 | 657 | Future play() async { 658 | await videoPlayerController.play(); 659 | } 660 | 661 | // ignore: avoid_positional_boolean_parameters 662 | Future setLooping(bool looping) async { 663 | await videoPlayerController.setLooping(looping); 664 | } 665 | 666 | Future pause() async { 667 | await videoPlayerController.pause(); 668 | } 669 | 670 | Future seekTo(Duration moment) async { 671 | await videoPlayerController.seekTo(moment); 672 | } 673 | 674 | Future setVolume(double volume) async { 675 | await videoPlayerController.setVolume(volume); 676 | } 677 | 678 | void setSubtitle(List newSubtitle) { 679 | subtitle = Subtitles(newSubtitle); 680 | } 681 | } 682 | 683 | class ChewieControllerProvider extends InheritedWidget { 684 | const ChewieControllerProvider({ 685 | super.key, 686 | required this.controller, 687 | required super.child, 688 | }); 689 | 690 | final ChewieController controller; 691 | 692 | @override 693 | bool updateShouldNotify(ChewieControllerProvider oldWidget) => 694 | controller != oldWidget.controller; 695 | } 696 | -------------------------------------------------------------------------------- /lib/src/chewie_progress_colors.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/rendering.dart'; 2 | 3 | class ChewieProgressColors { 4 | ChewieProgressColors({ 5 | Color playedColor = const Color.fromRGBO(255, 0, 0, 0.7), 6 | Color bufferedColor = const Color.fromRGBO(30, 30, 200, 0.2), 7 | Color handleColor = const Color.fromRGBO(200, 200, 200, 1.0), 8 | Color backgroundColor = const Color.fromRGBO(200, 200, 200, 0.5), 9 | }) : playedPaint = Paint()..color = playedColor, 10 | bufferedPaint = Paint()..color = bufferedColor, 11 | handlePaint = Paint()..color = handleColor, 12 | backgroundPaint = Paint()..color = backgroundColor; 13 | 14 | final Paint playedPaint; 15 | final Paint bufferedPaint; 16 | final Paint handlePaint; 17 | final Paint backgroundPaint; 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/cupertino/cupertino_progress_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:chewie/src/chewie_progress_colors.dart'; 2 | import 'package:chewie/src/progress_bar.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/widgets.dart'; 5 | import 'package:video_player/video_player.dart'; 6 | 7 | class CupertinoVideoProgressBar extends StatelessWidget { 8 | CupertinoVideoProgressBar( 9 | this.controller, { 10 | ChewieProgressColors? colors, 11 | this.onDragEnd, 12 | this.onDragStart, 13 | this.onDragUpdate, 14 | super.key, 15 | this.draggableProgressBar = true, 16 | }) : colors = colors ?? ChewieProgressColors(); 17 | 18 | final VideoPlayerController controller; 19 | final ChewieProgressColors colors; 20 | final Function()? onDragStart; 21 | final Function()? onDragEnd; 22 | final Function()? onDragUpdate; 23 | final bool draggableProgressBar; 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return VideoProgressBar( 28 | controller, 29 | barHeight: 5, 30 | handleHeight: 6, 31 | drawShadow: true, 32 | colors: colors, 33 | onDragEnd: onDragEnd, 34 | onDragStart: onDragStart, 35 | onDragUpdate: onDragUpdate, 36 | draggableProgressBar: draggableProgressBar, 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/cupertino/widgets/cupertino_options_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:chewie/src/models/option_item.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | 4 | class CupertinoOptionsDialog extends StatefulWidget { 5 | const CupertinoOptionsDialog({ 6 | super.key, 7 | required this.options, 8 | this.cancelButtonText, 9 | }); 10 | 11 | final List options; 12 | final String? cancelButtonText; 13 | 14 | @override 15 | // ignore: library_private_types_in_public_api 16 | _CupertinoOptionsDialogState createState() => _CupertinoOptionsDialogState(); 17 | } 18 | 19 | class _CupertinoOptionsDialogState extends State { 20 | @override 21 | Widget build(BuildContext context) { 22 | return SafeArea( 23 | child: CupertinoActionSheet( 24 | actions: widget.options 25 | .map( 26 | (option) => CupertinoActionSheetAction( 27 | onPressed: () => option.onTap(context), 28 | child: Text(option.title), 29 | ), 30 | ) 31 | .toList(), 32 | cancelButton: CupertinoActionSheetAction( 33 | onPressed: () => Navigator.pop(context), 34 | isDestructiveAction: true, 35 | child: Text(widget.cancelButtonText ?? 'Cancel'), 36 | ), 37 | ), 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/src/helpers/adaptive_controls.dart: -------------------------------------------------------------------------------- 1 | import 'package:chewie/chewie.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class AdaptiveControls extends StatelessWidget { 5 | const AdaptiveControls({ 6 | super.key, 7 | }); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | switch (Theme.of(context).platform) { 12 | case TargetPlatform.android: 13 | case TargetPlatform.fuchsia: 14 | return const MaterialControls(); 15 | 16 | case TargetPlatform.macOS: 17 | case TargetPlatform.windows: 18 | case TargetPlatform.linux: 19 | return const MaterialDesktopControls(); 20 | 21 | case TargetPlatform.iOS: 22 | return const CupertinoControls( 23 | backgroundColor: Color.fromRGBO(41, 41, 41, 0.7), 24 | iconColor: Color.fromARGB(255, 200, 200, 200), 25 | ); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/src/helpers/utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:video_player/video_player.dart'; 3 | 4 | String formatDuration(Duration position) { 5 | final ms = position.inMilliseconds; 6 | 7 | int seconds = ms ~/ 1000; 8 | final int hours = seconds ~/ 3600; 9 | seconds = seconds % 3600; 10 | final minutes = seconds ~/ 60; 11 | seconds = seconds % 60; 12 | 13 | final hoursString = hours >= 10 14 | ? '$hours' 15 | : hours == 0 16 | ? '00' 17 | : '0$hours'; 18 | 19 | final minutesString = minutes >= 10 20 | ? '$minutes' 21 | : minutes == 0 22 | ? '00' 23 | : '0$minutes'; 24 | 25 | final secondsString = seconds >= 10 26 | ? '$seconds' 27 | : seconds == 0 28 | ? '00' 29 | : '0$seconds'; 30 | 31 | final formattedTime = 32 | '${hoursString == '00' ? '' : '$hoursString:'}$minutesString:$secondsString'; 33 | 34 | return formattedTime; 35 | } 36 | 37 | /// Gets the current buffering state of the video player. 38 | /// 39 | /// For Android, it will use a workaround due to a [bug](https://github.com/flutter/flutter/issues/165149) 40 | /// affecting the `video_player` plugin, preventing it from getting the 41 | /// actual buffering state. This currently results in the `VideoPlayerController` always buffering, 42 | /// thus breaking UI elements. 43 | /// 44 | /// For this, the actual buffer position is used to determine if the video is 45 | /// buffering or not. See Issue [#912](https://github.com/fluttercommunity/chewie/pull/912) for more details. 46 | bool getIsBuffering(VideoPlayerController controller) { 47 | final VideoPlayerValue value = controller.value; 48 | 49 | if (defaultTargetPlatform == TargetPlatform.android) { 50 | if (value.isBuffering) { 51 | // -> Check if we actually buffer, as android has a bug preventing to 52 | // get the correct buffering state from this single bool. 53 | final int position = value.position.inMilliseconds; 54 | 55 | // Special case, if the video is finished, we don't want to show the 56 | // buffering indicator anymore 57 | if (position >= value.duration.inMilliseconds) { 58 | return false; 59 | } else { 60 | final int buffer = value.buffered.lastOrNull?.end.inMilliseconds ?? -1; 61 | 62 | return position >= buffer; 63 | } 64 | } else { 65 | // -> No buffering 66 | return false; 67 | } 68 | } 69 | 70 | return value.isBuffering; 71 | } 72 | -------------------------------------------------------------------------------- /lib/src/material/color_compat_extensions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | //ignore_for_file: deprecated_member_use 4 | extension ColorCompatExtensions on Color { 5 | /// Returns a new color that matches this color with the given opacity. 6 | /// 7 | /// This is a compatibility layer that ensures compatibility with Flutter 8 | /// versions below 3.27. In Flutter 3.27 and later, `Color.withOpacity` 9 | /// has been deprecated in favor of `Color.withValues`. 10 | /// 11 | /// This method bridges the gap by providing a consistent way to adjust 12 | /// the opacity of a color across different Flutter versions. 13 | /// 14 | /// **Important:** Once the minimum supported Flutter version is bumped 15 | /// to 3.27 or higher, this method should be removed and replaced with 16 | /// `withValues(alpha: opacity)`. 17 | /// 18 | /// See also: 19 | /// * [Color.withOpacity], which is deprecated in Flutter 3.27 and later. 20 | /// * [Color.withValues], the recommended replacement for `withOpacity`. 21 | Color withOpacityCompat(double opacity) { 22 | // Compatibility layer that uses the legacy withOpacity method, while 23 | // ignoring the deprecation for now (in order to guarantee N-1 minimum 24 | // version compatibility). 25 | // Once it's removed from a future update, we'll have to replace uses of 26 | // this method with withValues(alpha: opacity). 27 | // TODO: Replace this bridge method once the above holds true. 28 | return withOpacity(opacity); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/material/material_controls.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:chewie/src/center_play_button.dart'; 4 | import 'package:chewie/src/center_seek_button.dart'; 5 | import 'package:chewie/src/chewie_player.dart'; 6 | import 'package:chewie/src/chewie_progress_colors.dart'; 7 | import 'package:chewie/src/helpers/utils.dart'; 8 | import 'package:chewie/src/material/color_compat_extensions.dart'; 9 | import 'package:chewie/src/material/material_progress_bar.dart'; 10 | import 'package:chewie/src/material/widgets/options_dialog.dart'; 11 | import 'package:chewie/src/material/widgets/playback_speed_dialog.dart'; 12 | import 'package:chewie/src/models/option_item.dart'; 13 | import 'package:chewie/src/models/subtitle_model.dart'; 14 | import 'package:chewie/src/notifiers/index.dart'; 15 | import 'package:flutter/material.dart'; 16 | import 'package:provider/provider.dart'; 17 | import 'package:video_player/video_player.dart'; 18 | 19 | class MaterialControls extends StatefulWidget { 20 | const MaterialControls({ 21 | this.showPlayButton = true, 22 | super.key, 23 | }); 24 | 25 | final bool showPlayButton; 26 | 27 | @override 28 | State createState() { 29 | return _MaterialControlsState(); 30 | } 31 | } 32 | 33 | class _MaterialControlsState extends State 34 | with SingleTickerProviderStateMixin { 35 | late PlayerNotifier notifier; 36 | late VideoPlayerValue _latestValue; 37 | double? _latestVolume; 38 | Timer? _hideTimer; 39 | Timer? _initTimer; 40 | late var _subtitlesPosition = Duration.zero; 41 | bool _subtitleOn = false; 42 | Timer? _showAfterExpandCollapseTimer; 43 | bool _dragging = false; 44 | bool _displayTapped = false; 45 | Timer? _bufferingDisplayTimer; 46 | bool _displayBufferingIndicator = false; 47 | 48 | final barHeight = 48.0 * 1.5; 49 | final marginSize = 5.0; 50 | 51 | late VideoPlayerController controller; 52 | ChewieController? _chewieController; 53 | 54 | // We know that _chewieController is set in didChangeDependencies 55 | ChewieController get chewieController => _chewieController!; 56 | 57 | @override 58 | void initState() { 59 | super.initState(); 60 | notifier = Provider.of(context, listen: false); 61 | } 62 | 63 | @override 64 | Widget build(BuildContext context) { 65 | if (_latestValue.hasError) { 66 | return chewieController.errorBuilder?.call( 67 | context, 68 | chewieController.videoPlayerController.value.errorDescription!, 69 | ) ?? 70 | const Center( 71 | child: Icon( 72 | Icons.error, 73 | color: Colors.white, 74 | size: 42, 75 | ), 76 | ); 77 | } 78 | 79 | return MouseRegion( 80 | onHover: (_) { 81 | _cancelAndRestartTimer(); 82 | }, 83 | child: GestureDetector( 84 | onTap: () => _cancelAndRestartTimer(), 85 | child: AbsorbPointer( 86 | absorbing: notifier.hideStuff, 87 | child: Stack( 88 | children: [ 89 | if (_displayBufferingIndicator) 90 | _chewieController?.bufferingBuilder?.call(context) ?? 91 | const Center( 92 | child: CircularProgressIndicator(), 93 | ) 94 | else 95 | _buildHitArea(), 96 | _buildActionBar(), 97 | Column( 98 | mainAxisAlignment: MainAxisAlignment.end, 99 | children: [ 100 | if (_subtitleOn) 101 | Transform.translate( 102 | offset: Offset( 103 | 0.0, 104 | notifier.hideStuff ? barHeight * 0.8 : 0.0, 105 | ), 106 | child: 107 | _buildSubtitles(context, chewieController.subtitle!), 108 | ), 109 | _buildBottomBar(context), 110 | ], 111 | ), 112 | ], 113 | ), 114 | ), 115 | ), 116 | ); 117 | } 118 | 119 | @override 120 | void dispose() { 121 | _dispose(); 122 | super.dispose(); 123 | } 124 | 125 | void _dispose() { 126 | controller.removeListener(_updateState); 127 | _hideTimer?.cancel(); 128 | _initTimer?.cancel(); 129 | _showAfterExpandCollapseTimer?.cancel(); 130 | } 131 | 132 | @override 133 | void didChangeDependencies() { 134 | final oldController = _chewieController; 135 | _chewieController = ChewieController.of(context); 136 | controller = chewieController.videoPlayerController; 137 | 138 | if (oldController != chewieController) { 139 | _dispose(); 140 | _initialize(); 141 | } 142 | 143 | super.didChangeDependencies(); 144 | } 145 | 146 | Widget _buildActionBar() { 147 | return Positioned( 148 | top: 0, 149 | right: 0, 150 | child: SafeArea( 151 | child: AnimatedOpacity( 152 | opacity: notifier.hideStuff ? 0.0 : 1.0, 153 | duration: const Duration(milliseconds: 250), 154 | child: Row( 155 | children: [ 156 | _buildSubtitleToggle(), 157 | if (chewieController.showOptions) _buildOptionsButton(), 158 | ], 159 | ), 160 | ), 161 | ), 162 | ); 163 | } 164 | 165 | List _buildOptions(BuildContext context) { 166 | final options = [ 167 | OptionItem( 168 | onTap: (context) async { 169 | Navigator.pop(context); 170 | _onSpeedButtonTap(); 171 | }, 172 | iconData: Icons.speed, 173 | title: chewieController.optionsTranslation?.playbackSpeedButtonText ?? 174 | 'Playback speed', 175 | ) 176 | ]; 177 | 178 | if (chewieController.additionalOptions != null && 179 | chewieController.additionalOptions!(context).isNotEmpty) { 180 | options.addAll(chewieController.additionalOptions!(context)); 181 | } 182 | return options; 183 | } 184 | 185 | Widget _buildOptionsButton() { 186 | return AnimatedOpacity( 187 | opacity: notifier.hideStuff ? 0.0 : 1.0, 188 | duration: const Duration(milliseconds: 250), 189 | child: IconButton( 190 | onPressed: () async { 191 | _hideTimer?.cancel(); 192 | 193 | if (chewieController.optionsBuilder != null) { 194 | await chewieController.optionsBuilder!( 195 | context, _buildOptions(context)); 196 | } else { 197 | await showModalBottomSheet( 198 | context: context, 199 | isScrollControlled: true, 200 | useRootNavigator: chewieController.useRootNavigator, 201 | builder: (context) => OptionsDialog( 202 | options: _buildOptions(context), 203 | cancelButtonText: 204 | chewieController.optionsTranslation?.cancelButtonText, 205 | ), 206 | ); 207 | } 208 | 209 | if (_latestValue.isPlaying) { 210 | _startHideTimer(); 211 | } 212 | }, 213 | icon: const Icon( 214 | Icons.more_vert, 215 | color: Colors.white, 216 | ), 217 | ), 218 | ); 219 | } 220 | 221 | Widget _buildSubtitles(BuildContext context, Subtitles subtitles) { 222 | if (!_subtitleOn) { 223 | return const SizedBox(); 224 | } 225 | final currentSubtitle = subtitles.getByPosition(_subtitlesPosition); 226 | if (currentSubtitle.isEmpty) { 227 | return const SizedBox(); 228 | } 229 | 230 | if (chewieController.subtitleBuilder != null) { 231 | return chewieController.subtitleBuilder!( 232 | context, 233 | currentSubtitle.first!.text, 234 | ); 235 | } 236 | 237 | return Padding( 238 | padding: EdgeInsets.all(marginSize), 239 | child: Container( 240 | padding: const EdgeInsets.all(5), 241 | decoration: BoxDecoration( 242 | color: const Color(0x96000000), 243 | borderRadius: BorderRadius.circular(10.0), 244 | ), 245 | child: Text( 246 | currentSubtitle.first!.text.toString(), 247 | style: const TextStyle( 248 | fontSize: 18, 249 | ), 250 | textAlign: TextAlign.center, 251 | ), 252 | ), 253 | ); 254 | } 255 | 256 | AnimatedOpacity _buildBottomBar( 257 | BuildContext context, 258 | ) { 259 | final iconColor = Theme.of(context).textTheme.labelLarge!.color; 260 | 261 | return AnimatedOpacity( 262 | opacity: notifier.hideStuff ? 0.0 : 1.0, 263 | duration: const Duration(milliseconds: 300), 264 | child: Container( 265 | height: barHeight + (chewieController.isFullScreen ? 10.0 : 0), 266 | padding: EdgeInsets.only( 267 | left: 20, 268 | right: 20, 269 | bottom: !chewieController.isFullScreen ? 10.0 : 0, 270 | ), 271 | child: SafeArea( 272 | top: false, 273 | bottom: chewieController.isFullScreen, 274 | minimum: chewieController.controlsSafeAreaMinimum, 275 | child: Column( 276 | mainAxisSize: MainAxisSize.min, 277 | mainAxisAlignment: MainAxisAlignment.center, 278 | children: [ 279 | Flexible( 280 | child: Row( 281 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 282 | children: [ 283 | if (chewieController.isLive) 284 | const Expanded(child: Text('LIVE')) 285 | else 286 | _buildPosition(iconColor), 287 | if (chewieController.allowMuting) 288 | _buildMuteButton(controller), 289 | const Spacer(), 290 | if (chewieController.allowFullScreen) _buildExpandButton(), 291 | ], 292 | ), 293 | ), 294 | SizedBox( 295 | height: chewieController.isFullScreen ? 15.0 : 0, 296 | ), 297 | if (!chewieController.isLive) 298 | Expanded( 299 | child: Container( 300 | padding: const EdgeInsets.symmetric(horizontal: 20), 301 | child: Row( 302 | children: [ 303 | _buildProgressBar(), 304 | ], 305 | ), 306 | ), 307 | ), 308 | ], 309 | ), 310 | ), 311 | ), 312 | ); 313 | } 314 | 315 | GestureDetector _buildMuteButton( 316 | VideoPlayerController controller, 317 | ) { 318 | return GestureDetector( 319 | onTap: () { 320 | _cancelAndRestartTimer(); 321 | 322 | if (_latestValue.volume == 0) { 323 | controller.setVolume(_latestVolume ?? 0.5); 324 | } else { 325 | _latestVolume = controller.value.volume; 326 | controller.setVolume(0.0); 327 | } 328 | }, 329 | child: AnimatedOpacity( 330 | opacity: notifier.hideStuff ? 0.0 : 1.0, 331 | duration: const Duration(milliseconds: 300), 332 | child: ClipRect( 333 | child: Container( 334 | height: barHeight, 335 | padding: const EdgeInsets.only( 336 | left: 6.0, 337 | ), 338 | child: Icon( 339 | _latestValue.volume > 0 ? Icons.volume_up : Icons.volume_off, 340 | color: Colors.white, 341 | ), 342 | ), 343 | ), 344 | ), 345 | ); 346 | } 347 | 348 | GestureDetector _buildExpandButton() { 349 | return GestureDetector( 350 | onTap: _onExpandCollapse, 351 | child: AnimatedOpacity( 352 | opacity: notifier.hideStuff ? 0.0 : 1.0, 353 | duration: const Duration(milliseconds: 300), 354 | child: Container( 355 | height: barHeight + (chewieController.isFullScreen ? 15.0 : 0), 356 | margin: const EdgeInsets.only(right: 12.0), 357 | padding: const EdgeInsets.only( 358 | left: 8.0, 359 | right: 8.0, 360 | ), 361 | child: Center( 362 | child: Icon( 363 | chewieController.isFullScreen 364 | ? Icons.fullscreen_exit 365 | : Icons.fullscreen, 366 | color: Colors.white, 367 | ), 368 | ), 369 | ), 370 | ), 371 | ); 372 | } 373 | 374 | Widget _buildHitArea() { 375 | final bool isFinished = (_latestValue.position >= _latestValue.duration) && 376 | _latestValue.duration.inSeconds > 0; 377 | final bool showPlayButton = 378 | widget.showPlayButton && !_dragging && !notifier.hideStuff; 379 | 380 | return GestureDetector( 381 | onTap: () { 382 | if (_latestValue.isPlaying) { 383 | if (_chewieController?.pauseOnBackgroundTap ?? false) { 384 | _playPause(); 385 | _cancelAndRestartTimer(); 386 | } else { 387 | if (_displayTapped) { 388 | setState(() { 389 | notifier.hideStuff = true; 390 | }); 391 | } else { 392 | _cancelAndRestartTimer(); 393 | } 394 | } 395 | } else { 396 | _playPause(); 397 | 398 | setState(() { 399 | notifier.hideStuff = true; 400 | }); 401 | } 402 | }, 403 | child: Container( 404 | alignment: Alignment.center, 405 | color: Colors 406 | .transparent, // The Gesture Detector doesn't expand to the full size of the container without this; Not sure why! 407 | child: Row( 408 | mainAxisAlignment: MainAxisAlignment.center, 409 | children: [ 410 | if (!isFinished && !chewieController.isLive) 411 | CenterSeekButton( 412 | iconData: Icons.replay_10, 413 | backgroundColor: Colors.black54, 414 | iconColor: Colors.white, 415 | show: showPlayButton, 416 | fadeDuration: chewieController.materialSeekButtonFadeDuration, 417 | iconSize: chewieController.materialSeekButtonSize, 418 | onPressed: _seekBackward, 419 | ), 420 | Container( 421 | margin: EdgeInsets.symmetric( 422 | horizontal: marginSize, 423 | ), 424 | child: CenterPlayButton( 425 | backgroundColor: Colors.black54, 426 | iconColor: Colors.white, 427 | isFinished: isFinished, 428 | isPlaying: controller.value.isPlaying, 429 | show: showPlayButton, 430 | onPressed: _playPause, 431 | ), 432 | ), 433 | if (!isFinished && !chewieController.isLive) 434 | CenterSeekButton( 435 | iconData: Icons.forward_10, 436 | backgroundColor: Colors.black54, 437 | iconColor: Colors.white, 438 | show: showPlayButton, 439 | fadeDuration: chewieController.materialSeekButtonFadeDuration, 440 | iconSize: chewieController.materialSeekButtonSize, 441 | onPressed: _seekForward, 442 | ), 443 | ], 444 | ), 445 | ), 446 | ); 447 | } 448 | 449 | Future _onSpeedButtonTap() async { 450 | _hideTimer?.cancel(); 451 | 452 | final chosenSpeed = await showModalBottomSheet( 453 | context: context, 454 | isScrollControlled: true, 455 | useRootNavigator: chewieController.useRootNavigator, 456 | builder: (context) => PlaybackSpeedDialog( 457 | speeds: chewieController.playbackSpeeds, 458 | selected: _latestValue.playbackSpeed, 459 | ), 460 | ); 461 | 462 | if (chosenSpeed != null) { 463 | controller.setPlaybackSpeed(chosenSpeed); 464 | } 465 | 466 | if (_latestValue.isPlaying) { 467 | _startHideTimer(); 468 | } 469 | } 470 | 471 | Widget _buildPosition(Color? iconColor) { 472 | final position = _latestValue.position; 473 | final duration = _latestValue.duration; 474 | 475 | return RichText( 476 | text: TextSpan( 477 | text: '${formatDuration(position)} ', 478 | children: [ 479 | TextSpan( 480 | text: '/ ${formatDuration(duration)}', 481 | style: TextStyle( 482 | fontSize: 14.0, 483 | color: Colors.white.withOpacityCompat(.75), 484 | fontWeight: FontWeight.normal, 485 | ), 486 | ) 487 | ], 488 | style: const TextStyle( 489 | fontSize: 14.0, 490 | color: Colors.white, 491 | fontWeight: FontWeight.bold, 492 | ), 493 | ), 494 | ); 495 | } 496 | 497 | Widget _buildSubtitleToggle() { 498 | //if don't have subtitle hiden button 499 | if (chewieController.subtitle?.isEmpty ?? true) { 500 | return const SizedBox(); 501 | } 502 | return GestureDetector( 503 | onTap: _onSubtitleTap, 504 | child: Container( 505 | height: barHeight, 506 | color: Colors.transparent, 507 | padding: const EdgeInsets.only( 508 | left: 12.0, 509 | right: 12.0, 510 | ), 511 | child: Icon( 512 | _subtitleOn 513 | ? Icons.closed_caption 514 | : Icons.closed_caption_off_outlined, 515 | color: _subtitleOn ? Colors.white : Colors.grey[700], 516 | ), 517 | ), 518 | ); 519 | } 520 | 521 | void _onSubtitleTap() { 522 | setState(() { 523 | _subtitleOn = !_subtitleOn; 524 | }); 525 | } 526 | 527 | void _cancelAndRestartTimer() { 528 | _hideTimer?.cancel(); 529 | _startHideTimer(); 530 | 531 | setState(() { 532 | notifier.hideStuff = false; 533 | _displayTapped = true; 534 | }); 535 | } 536 | 537 | Future _initialize() async { 538 | _subtitleOn = chewieController.showSubtitles && 539 | (chewieController.subtitle?.isNotEmpty ?? false); 540 | controller.addListener(_updateState); 541 | 542 | _updateState(); 543 | 544 | if (controller.value.isPlaying || chewieController.autoPlay) { 545 | _startHideTimer(); 546 | } 547 | 548 | if (chewieController.showControlsOnInitialize) { 549 | _initTimer = Timer(const Duration(milliseconds: 200), () { 550 | setState(() { 551 | notifier.hideStuff = false; 552 | }); 553 | }); 554 | } 555 | } 556 | 557 | void _onExpandCollapse() { 558 | setState(() { 559 | notifier.hideStuff = true; 560 | 561 | chewieController.toggleFullScreen(); 562 | _showAfterExpandCollapseTimer = 563 | Timer(const Duration(milliseconds: 300), () { 564 | setState(() { 565 | _cancelAndRestartTimer(); 566 | }); 567 | }); 568 | }); 569 | } 570 | 571 | void _playPause() { 572 | final bool isFinished = (_latestValue.position >= _latestValue.duration) && 573 | _latestValue.duration.inSeconds > 0; 574 | 575 | setState(() { 576 | if (controller.value.isPlaying) { 577 | notifier.hideStuff = false; 578 | _hideTimer?.cancel(); 579 | controller.pause(); 580 | } else { 581 | _cancelAndRestartTimer(); 582 | 583 | if (!controller.value.isInitialized) { 584 | controller.initialize().then((_) { 585 | controller.play(); 586 | }); 587 | } else { 588 | if (isFinished) { 589 | controller.seekTo(Duration.zero); 590 | } 591 | controller.play(); 592 | } 593 | } 594 | }); 595 | } 596 | 597 | void _seekRelative(Duration relativeSeek) { 598 | _cancelAndRestartTimer(); 599 | final position = _latestValue.position + relativeSeek; 600 | final duration = _latestValue.duration; 601 | 602 | if (position < Duration.zero) { 603 | controller.seekTo(Duration.zero); 604 | } else if (position > duration) { 605 | controller.seekTo(duration); 606 | } else { 607 | controller.seekTo(position); 608 | } 609 | } 610 | 611 | void _seekBackward() { 612 | _seekRelative( 613 | const Duration( 614 | seconds: -10, 615 | ), 616 | ); 617 | } 618 | 619 | void _seekForward() { 620 | _seekRelative( 621 | const Duration( 622 | seconds: 10, 623 | ), 624 | ); 625 | } 626 | 627 | void _startHideTimer() { 628 | final hideControlsTimer = chewieController.hideControlsTimer.isNegative 629 | ? ChewieController.defaultHideControlsTimer 630 | : chewieController.hideControlsTimer; 631 | _hideTimer = Timer(hideControlsTimer, () { 632 | setState(() { 633 | notifier.hideStuff = true; 634 | }); 635 | }); 636 | } 637 | 638 | void _bufferingTimerTimeout() { 639 | _displayBufferingIndicator = true; 640 | if (mounted) { 641 | setState(() {}); 642 | } 643 | } 644 | 645 | void _updateState() { 646 | if (!mounted) return; 647 | 648 | final bool buffering = getIsBuffering(controller); 649 | 650 | // display the progress bar indicator only after the buffering delay if it has been set 651 | if (chewieController.progressIndicatorDelay != null) { 652 | if (buffering) { 653 | _bufferingDisplayTimer ??= Timer( 654 | chewieController.progressIndicatorDelay!, 655 | _bufferingTimerTimeout, 656 | ); 657 | } else { 658 | _bufferingDisplayTimer?.cancel(); 659 | _bufferingDisplayTimer = null; 660 | _displayBufferingIndicator = false; 661 | } 662 | } else { 663 | _displayBufferingIndicator = buffering; 664 | } 665 | 666 | setState(() { 667 | _latestValue = controller.value; 668 | _subtitlesPosition = controller.value.position; 669 | }); 670 | } 671 | 672 | Widget _buildProgressBar() { 673 | return Expanded( 674 | child: MaterialVideoProgressBar( 675 | controller, 676 | onDragStart: () { 677 | setState(() { 678 | _dragging = true; 679 | }); 680 | 681 | _hideTimer?.cancel(); 682 | }, 683 | onDragUpdate: () { 684 | _hideTimer?.cancel(); 685 | }, 686 | onDragEnd: () { 687 | setState(() { 688 | _dragging = false; 689 | }); 690 | 691 | _startHideTimer(); 692 | }, 693 | colors: chewieController.materialProgressColors ?? 694 | ChewieProgressColors( 695 | playedColor: Theme.of(context).colorScheme.secondary, 696 | handleColor: Theme.of(context).colorScheme.secondary, 697 | bufferedColor: 698 | Theme.of(context).colorScheme.surface.withOpacityCompat(0.5), 699 | backgroundColor: 700 | Theme.of(context).disabledColor.withOpacityCompat(.5), 701 | ), 702 | draggableProgressBar: chewieController.draggableProgressBar, 703 | ), 704 | ); 705 | } 706 | } 707 | -------------------------------------------------------------------------------- /lib/src/material/material_desktop_controls.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:chewie/src/animated_play_pause.dart'; 4 | import 'package:chewie/src/center_play_button.dart'; 5 | import 'package:chewie/src/chewie_player.dart'; 6 | import 'package:chewie/src/chewie_progress_colors.dart'; 7 | import 'package:chewie/src/helpers/utils.dart'; 8 | import 'package:chewie/src/material/color_compat_extensions.dart'; 9 | import 'package:chewie/src/material/material_progress_bar.dart'; 10 | import 'package:chewie/src/material/widgets/options_dialog.dart'; 11 | import 'package:chewie/src/material/widgets/playback_speed_dialog.dart'; 12 | import 'package:chewie/src/models/option_item.dart'; 13 | import 'package:chewie/src/models/subtitle_model.dart'; 14 | import 'package:chewie/src/notifiers/index.dart'; 15 | import 'package:flutter/material.dart'; 16 | import 'package:flutter/services.dart'; 17 | import 'package:provider/provider.dart'; 18 | import 'package:video_player/video_player.dart'; 19 | 20 | class MaterialDesktopControls extends StatefulWidget { 21 | const MaterialDesktopControls({ 22 | this.showPlayButton = true, 23 | super.key, 24 | }); 25 | 26 | final bool showPlayButton; 27 | 28 | @override 29 | State createState() { 30 | return _MaterialDesktopControlsState(); 31 | } 32 | } 33 | 34 | class _MaterialDesktopControlsState extends State 35 | with SingleTickerProviderStateMixin { 36 | late PlayerNotifier notifier; 37 | late VideoPlayerValue _latestValue; 38 | double? _latestVolume; 39 | Timer? _hideTimer; 40 | Timer? _initTimer; 41 | late var _subtitlesPosition = Duration.zero; 42 | bool _subtitleOn = false; 43 | Timer? _showAfterExpandCollapseTimer; 44 | bool _dragging = false; 45 | bool _displayTapped = false; 46 | Timer? _bufferingDisplayTimer; 47 | bool _displayBufferingIndicator = false; 48 | 49 | final barHeight = 48.0 * 1.5; 50 | final marginSize = 5.0; 51 | 52 | late VideoPlayerController controller; 53 | ChewieController? _chewieController; 54 | late final FocusNode _focusNode; 55 | 56 | // We know that _chewieController is set in didChangeDependencies 57 | ChewieController get chewieController => _chewieController!; 58 | 59 | @override 60 | void initState() { 61 | super.initState(); 62 | _focusNode = FocusNode(); 63 | _focusNode.requestFocus(); 64 | notifier = Provider.of(context, listen: false); 65 | } 66 | 67 | void _handleKeyPress(event) { 68 | if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.space) { 69 | _playPause(); 70 | } else if (event is KeyDownEvent && 71 | event.logicalKey == LogicalKeyboardKey.arrowRight) { 72 | _seekForward(); 73 | } else if (event is KeyDownEvent && 74 | event.logicalKey == LogicalKeyboardKey.arrowLeft) { 75 | _seekBackward(); 76 | } else if (event is KeyDownEvent && 77 | event.logicalKey == LogicalKeyboardKey.escape) { 78 | if (chewieController.isFullScreen) { 79 | _onExpandCollapse(); 80 | } 81 | } 82 | } 83 | 84 | @override 85 | Widget build(BuildContext context) { 86 | if (_latestValue.hasError) { 87 | return chewieController.errorBuilder?.call( 88 | context, 89 | chewieController.videoPlayerController.value.errorDescription!, 90 | ) ?? 91 | const Center( 92 | child: Icon( 93 | Icons.error, 94 | color: Colors.white, 95 | size: 42, 96 | ), 97 | ); 98 | } 99 | 100 | return KeyboardListener( 101 | focusNode: _focusNode, 102 | onKeyEvent: _handleKeyPress, 103 | child: MouseRegion( 104 | onHover: (_) { 105 | _focusNode.requestFocus(); 106 | _cancelAndRestartTimer(); 107 | }, 108 | child: GestureDetector( 109 | onTap: () { 110 | _playPause(); 111 | _cancelAndRestartTimer(); 112 | }, 113 | child: AbsorbPointer( 114 | absorbing: notifier.hideStuff, 115 | child: Stack( 116 | children: [ 117 | if (_displayBufferingIndicator) 118 | _chewieController?.bufferingBuilder?.call(context) ?? 119 | const Center( 120 | child: CircularProgressIndicator(), 121 | ) 122 | else 123 | _buildHitArea(), 124 | Column( 125 | mainAxisAlignment: MainAxisAlignment.end, 126 | children: [ 127 | if (_subtitleOn) 128 | Transform.translate( 129 | offset: Offset( 130 | 0.0, 131 | notifier.hideStuff ? barHeight * 0.8 : 0.0, 132 | ), 133 | child: _buildSubtitles( 134 | context, chewieController.subtitle!), 135 | ), 136 | _buildBottomBar(context), 137 | ], 138 | ), 139 | ], 140 | ), 141 | ), 142 | ), 143 | ), 144 | ); 145 | } 146 | 147 | @override 148 | void dispose() { 149 | _dispose(); 150 | _focusNode.dispose(); 151 | super.dispose(); 152 | } 153 | 154 | void _dispose() { 155 | controller.removeListener(_updateState); 156 | _hideTimer?.cancel(); 157 | _initTimer?.cancel(); 158 | _showAfterExpandCollapseTimer?.cancel(); 159 | } 160 | 161 | @override 162 | void didChangeDependencies() { 163 | final oldController = _chewieController; 164 | _chewieController = ChewieController.of(context); 165 | controller = chewieController.videoPlayerController; 166 | 167 | if (oldController != chewieController) { 168 | _dispose(); 169 | _initialize(); 170 | } 171 | 172 | super.didChangeDependencies(); 173 | } 174 | 175 | Widget _buildSubtitleToggle({IconData? icon, bool isPadded = false}) { 176 | return IconButton( 177 | padding: isPadded ? const EdgeInsets.all(8.0) : EdgeInsets.zero, 178 | icon: Icon(icon, color: _subtitleOn ? Colors.white : Colors.grey[700]), 179 | onPressed: _onSubtitleTap, 180 | ); 181 | } 182 | 183 | Widget _buildOptionsButton({ 184 | IconData? icon, 185 | bool isPadded = false, 186 | }) { 187 | final options = [ 188 | OptionItem( 189 | onTap: (context) async { 190 | Navigator.pop(context); 191 | _onSpeedButtonTap(); 192 | }, 193 | iconData: Icons.speed, 194 | title: chewieController.optionsTranslation?.playbackSpeedButtonText ?? 195 | 'Playback speed', 196 | ) 197 | ]; 198 | 199 | if (chewieController.additionalOptions != null && 200 | chewieController.additionalOptions!(context).isNotEmpty) { 201 | options.addAll(chewieController.additionalOptions!(context)); 202 | } 203 | 204 | return AnimatedOpacity( 205 | opacity: notifier.hideStuff ? 0.0 : 1.0, 206 | duration: const Duration(milliseconds: 250), 207 | child: IconButton( 208 | padding: isPadded ? const EdgeInsets.all(8.0) : EdgeInsets.zero, 209 | onPressed: () async { 210 | _hideTimer?.cancel(); 211 | 212 | if (chewieController.optionsBuilder != null) { 213 | await chewieController.optionsBuilder!(context, options); 214 | } else { 215 | await showModalBottomSheet( 216 | context: context, 217 | isScrollControlled: true, 218 | useRootNavigator: chewieController.useRootNavigator, 219 | builder: (context) => OptionsDialog( 220 | options: options, 221 | cancelButtonText: 222 | chewieController.optionsTranslation?.cancelButtonText, 223 | ), 224 | ); 225 | } 226 | 227 | if (_latestValue.isPlaying) { 228 | _startHideTimer(); 229 | } 230 | }, 231 | icon: Icon( 232 | icon ?? Icons.more_vert, 233 | color: Colors.white, 234 | ), 235 | ), 236 | ); 237 | } 238 | 239 | Widget _buildSubtitles(BuildContext context, Subtitles subtitles) { 240 | if (!_subtitleOn) { 241 | return const SizedBox(); 242 | } 243 | final currentSubtitle = subtitles.getByPosition(_subtitlesPosition); 244 | if (currentSubtitle.isEmpty) { 245 | return const SizedBox(); 246 | } 247 | 248 | if (chewieController.subtitleBuilder != null) { 249 | return chewieController.subtitleBuilder!( 250 | context, 251 | currentSubtitle.first!.text, 252 | ); 253 | } 254 | 255 | return Padding( 256 | padding: EdgeInsets.all(marginSize), 257 | child: Container( 258 | padding: const EdgeInsets.all(5), 259 | decoration: BoxDecoration( 260 | color: const Color(0x96000000), 261 | borderRadius: BorderRadius.circular(10.0), 262 | ), 263 | child: Text( 264 | currentSubtitle.first!.text.toString(), 265 | style: const TextStyle( 266 | fontSize: 18, 267 | ), 268 | textAlign: TextAlign.center, 269 | ), 270 | ), 271 | ); 272 | } 273 | 274 | AnimatedOpacity _buildBottomBar( 275 | BuildContext context, 276 | ) { 277 | final iconColor = Theme.of(context).textTheme.labelLarge!.color; 278 | 279 | return AnimatedOpacity( 280 | opacity: notifier.hideStuff ? 0.0 : 1.0, 281 | duration: const Duration(milliseconds: 300), 282 | child: Container( 283 | height: barHeight + (chewieController.isFullScreen ? 20.0 : 0), 284 | padding: 285 | EdgeInsets.only(bottom: chewieController.isFullScreen ? 10.0 : 15), 286 | child: SafeArea( 287 | bottom: chewieController.isFullScreen, 288 | child: Column( 289 | mainAxisSize: MainAxisSize.min, 290 | mainAxisAlignment: MainAxisAlignment.center, 291 | verticalDirection: VerticalDirection.up, 292 | children: [ 293 | Flexible( 294 | child: Row( 295 | children: [ 296 | _buildPlayPause(controller), 297 | if (chewieController.allowMuting) 298 | _buildMuteButton(controller), 299 | if (chewieController.isLive) 300 | const Expanded(child: Text('LIVE')) 301 | else 302 | _buildPosition(iconColor), 303 | const Spacer(), 304 | if (chewieController.showControls && 305 | chewieController.subtitle != null && 306 | chewieController.subtitle!.isNotEmpty) 307 | _buildSubtitleToggle(icon: Icons.subtitles), 308 | if (chewieController.showOptions) 309 | _buildOptionsButton(icon: Icons.settings), 310 | if (chewieController.allowFullScreen) _buildExpandButton(), 311 | ], 312 | ), 313 | ), 314 | if (!chewieController.isLive) 315 | Expanded( 316 | child: Container( 317 | padding: EdgeInsets.only( 318 | right: 20, 319 | left: 20, 320 | bottom: chewieController.isFullScreen ? 5.0 : 0, 321 | ), 322 | child: Row( 323 | children: [ 324 | _buildProgressBar(), 325 | ], 326 | ), 327 | ), 328 | ), 329 | ], 330 | ), 331 | ), 332 | ), 333 | ); 334 | } 335 | 336 | GestureDetector _buildExpandButton() { 337 | return GestureDetector( 338 | onTap: _onExpandCollapse, 339 | child: AnimatedOpacity( 340 | opacity: notifier.hideStuff ? 0.0 : 1.0, 341 | duration: const Duration(milliseconds: 300), 342 | child: Container( 343 | height: barHeight + (chewieController.isFullScreen ? 15.0 : 0), 344 | margin: const EdgeInsets.only(right: 12.0), 345 | padding: const EdgeInsets.only( 346 | left: 8.0, 347 | right: 8.0, 348 | ), 349 | child: Center( 350 | child: Icon( 351 | chewieController.isFullScreen 352 | ? Icons.fullscreen_exit 353 | : Icons.fullscreen, 354 | color: Colors.white, 355 | ), 356 | ), 357 | ), 358 | ), 359 | ); 360 | } 361 | 362 | Widget _buildHitArea() { 363 | final bool isFinished = _latestValue.position >= _latestValue.duration && 364 | _latestValue.duration.inSeconds > 0; 365 | final bool showPlayButton = 366 | widget.showPlayButton && !_dragging && !notifier.hideStuff; 367 | 368 | return GestureDetector( 369 | onTap: () { 370 | if (_latestValue.isPlaying) { 371 | if (_chewieController?.pauseOnBackgroundTap ?? false) { 372 | _playPause(); 373 | _cancelAndRestartTimer(); 374 | } else { 375 | if (_displayTapped) { 376 | setState(() { 377 | notifier.hideStuff = true; 378 | }); 379 | } else { 380 | _cancelAndRestartTimer(); 381 | } 382 | } 383 | } else { 384 | _playPause(); 385 | 386 | setState(() { 387 | notifier.hideStuff = true; 388 | }); 389 | } 390 | }, 391 | child: CenterPlayButton( 392 | backgroundColor: Colors.black54, 393 | iconColor: Colors.white, 394 | isFinished: isFinished, 395 | isPlaying: controller.value.isPlaying, 396 | show: showPlayButton, 397 | onPressed: _playPause, 398 | ), 399 | ); 400 | } 401 | 402 | Future _onSpeedButtonTap() async { 403 | _hideTimer?.cancel(); 404 | 405 | final chosenSpeed = await showModalBottomSheet( 406 | context: context, 407 | isScrollControlled: true, 408 | useRootNavigator: chewieController.useRootNavigator, 409 | builder: (context) => PlaybackSpeedDialog( 410 | speeds: chewieController.playbackSpeeds, 411 | selected: _latestValue.playbackSpeed, 412 | ), 413 | ); 414 | 415 | if (chosenSpeed != null) { 416 | controller.setPlaybackSpeed(chosenSpeed); 417 | } 418 | 419 | if (_latestValue.isPlaying) { 420 | _startHideTimer(); 421 | } 422 | } 423 | 424 | GestureDetector _buildMuteButton( 425 | VideoPlayerController controller, 426 | ) { 427 | return GestureDetector( 428 | onTap: () { 429 | _cancelAndRestartTimer(); 430 | 431 | if (_latestValue.volume == 0) { 432 | controller.setVolume(_latestVolume ?? 0.5); 433 | } else { 434 | _latestVolume = controller.value.volume; 435 | controller.setVolume(0.0); 436 | } 437 | }, 438 | child: AnimatedOpacity( 439 | opacity: notifier.hideStuff ? 0.0 : 1.0, 440 | duration: const Duration(milliseconds: 300), 441 | child: ClipRect( 442 | child: Container( 443 | height: barHeight, 444 | padding: const EdgeInsets.only( 445 | right: 15.0, 446 | ), 447 | child: Icon( 448 | _latestValue.volume > 0 ? Icons.volume_up : Icons.volume_off, 449 | color: Colors.white, 450 | ), 451 | ), 452 | ), 453 | ), 454 | ); 455 | } 456 | 457 | GestureDetector _buildPlayPause(VideoPlayerController controller) { 458 | return GestureDetector( 459 | onTap: _playPause, 460 | child: Container( 461 | height: barHeight, 462 | color: Colors.transparent, 463 | margin: const EdgeInsets.only(left: 8.0, right: 4.0), 464 | padding: const EdgeInsets.only( 465 | left: 12.0, 466 | right: 12.0, 467 | ), 468 | child: AnimatedPlayPause( 469 | playing: controller.value.isPlaying, 470 | color: Colors.white, 471 | ), 472 | ), 473 | ); 474 | } 475 | 476 | Widget _buildPosition(Color? iconColor) { 477 | final position = _latestValue.position; 478 | final duration = _latestValue.duration; 479 | 480 | return Text( 481 | '${formatDuration(position)} / ${formatDuration(duration)}', 482 | style: const TextStyle( 483 | fontSize: 14.0, 484 | color: Colors.white, 485 | ), 486 | ); 487 | } 488 | 489 | void _onSubtitleTap() { 490 | setState(() { 491 | _subtitleOn = !_subtitleOn; 492 | }); 493 | } 494 | 495 | void _cancelAndRestartTimer() { 496 | _hideTimer?.cancel(); 497 | _startHideTimer(); 498 | 499 | setState(() { 500 | notifier.hideStuff = false; 501 | _displayTapped = true; 502 | }); 503 | } 504 | 505 | Future _initialize() async { 506 | _subtitleOn = chewieController.showSubtitles && 507 | (chewieController.subtitle?.isNotEmpty ?? false); 508 | controller.addListener(_updateState); 509 | 510 | _updateState(); 511 | 512 | if (controller.value.isPlaying || chewieController.autoPlay) { 513 | _startHideTimer(); 514 | } 515 | 516 | if (chewieController.showControlsOnInitialize) { 517 | _initTimer = Timer(const Duration(milliseconds: 200), () { 518 | setState(() { 519 | notifier.hideStuff = false; 520 | }); 521 | }); 522 | } 523 | } 524 | 525 | void _onExpandCollapse() { 526 | setState(() { 527 | notifier.hideStuff = true; 528 | }); 529 | 530 | chewieController.toggleFullScreen(); 531 | 532 | _showAfterExpandCollapseTimer = 533 | Timer(const Duration(milliseconds: 300), () { 534 | setState(() { 535 | _cancelAndRestartTimer(); 536 | }); 537 | }); 538 | } 539 | 540 | void _playPause() { 541 | if (controller.value.isPlaying) { 542 | setState(() { 543 | notifier.hideStuff = false; 544 | }); 545 | 546 | _hideTimer?.cancel(); 547 | controller.pause(); 548 | } else { 549 | _cancelAndRestartTimer(); 550 | 551 | if (!controller.value.isInitialized) { 552 | controller.initialize().then((_) { 553 | //[VideoPlayerController.play] If the video is at the end, this method starts playing from the beginning 554 | controller.play(); 555 | }); 556 | } else { 557 | //[VideoPlayerController.play] If the video is at the end, this method starts playing from the beginning 558 | controller.play(); 559 | } 560 | } 561 | } 562 | 563 | void _startHideTimer() { 564 | final hideControlsTimer = chewieController.hideControlsTimer.isNegative 565 | ? ChewieController.defaultHideControlsTimer 566 | : chewieController.hideControlsTimer; 567 | _hideTimer = Timer(hideControlsTimer, () { 568 | setState(() { 569 | notifier.hideStuff = true; 570 | }); 571 | }); 572 | } 573 | 574 | void _bufferingTimerTimeout() { 575 | _displayBufferingIndicator = true; 576 | if (mounted) { 577 | setState(() {}); 578 | } 579 | } 580 | 581 | void _updateState() { 582 | if (!mounted) return; 583 | 584 | final bool buffering = getIsBuffering(controller); 585 | 586 | // display the progress bar indicator only after the buffering delay if it has been set 587 | if (chewieController.progressIndicatorDelay != null) { 588 | if (buffering) { 589 | _bufferingDisplayTimer ??= Timer( 590 | chewieController.progressIndicatorDelay!, 591 | _bufferingTimerTimeout, 592 | ); 593 | } else { 594 | _bufferingDisplayTimer?.cancel(); 595 | _bufferingDisplayTimer = null; 596 | _displayBufferingIndicator = false; 597 | } 598 | } else { 599 | _displayBufferingIndicator = buffering; 600 | } 601 | 602 | setState(() { 603 | _latestValue = controller.value; 604 | _subtitlesPosition = controller.value.position; 605 | }); 606 | } 607 | 608 | void _seekBackward() { 609 | _seekRelative( 610 | const Duration( 611 | seconds: -10, 612 | ), 613 | ); 614 | } 615 | 616 | void _seekForward() { 617 | _seekRelative( 618 | const Duration( 619 | seconds: 10, 620 | ), 621 | ); 622 | } 623 | 624 | void _seekRelative(Duration relativeSeek) { 625 | _cancelAndRestartTimer(); 626 | final position = _latestValue.position + relativeSeek; 627 | final duration = _latestValue.duration; 628 | 629 | if (position < Duration.zero) { 630 | controller.seekTo(Duration.zero); 631 | } else if (position > duration) { 632 | controller.seekTo(duration); 633 | } else { 634 | controller.seekTo(position); 635 | } 636 | } 637 | 638 | Widget _buildProgressBar() { 639 | return Expanded( 640 | child: MaterialVideoProgressBar( 641 | controller, 642 | onDragStart: () { 643 | setState(() { 644 | _dragging = true; 645 | }); 646 | 647 | _hideTimer?.cancel(); 648 | }, 649 | onDragUpdate: () { 650 | _hideTimer?.cancel(); 651 | }, 652 | onDragEnd: () { 653 | setState(() { 654 | _dragging = false; 655 | }); 656 | 657 | _startHideTimer(); 658 | }, 659 | colors: chewieController.materialProgressColors ?? 660 | ChewieProgressColors( 661 | playedColor: Theme.of(context).colorScheme.secondary, 662 | handleColor: Theme.of(context).colorScheme.secondary, 663 | bufferedColor: 664 | Theme.of(context).colorScheme.surface.withOpacityCompat(0.5), 665 | backgroundColor: 666 | Theme.of(context).disabledColor.withOpacityCompat(0.5), 667 | ), 668 | draggableProgressBar: chewieController.draggableProgressBar, 669 | ), 670 | ); 671 | } 672 | } 673 | -------------------------------------------------------------------------------- /lib/src/material/material_progress_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:chewie/src/chewie_progress_colors.dart'; 2 | import 'package:chewie/src/progress_bar.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:video_player/video_player.dart'; 5 | 6 | class MaterialVideoProgressBar extends StatelessWidget { 7 | MaterialVideoProgressBar( 8 | this.controller, { 9 | this.height = kToolbarHeight, 10 | this.barHeight = 10, 11 | this.handleHeight = 6, 12 | ChewieProgressColors? colors, 13 | this.onDragEnd, 14 | this.onDragStart, 15 | this.onDragUpdate, 16 | super.key, 17 | this.draggableProgressBar = true, 18 | }) : colors = colors ?? ChewieProgressColors(); 19 | 20 | final double height; 21 | final double barHeight; 22 | final double handleHeight; 23 | final VideoPlayerController controller; 24 | final ChewieProgressColors colors; 25 | final Function()? onDragStart; 26 | final Function()? onDragEnd; 27 | final Function()? onDragUpdate; 28 | final bool draggableProgressBar; 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | return VideoProgressBar( 33 | controller, 34 | barHeight: barHeight, 35 | handleHeight: handleHeight, 36 | drawShadow: true, 37 | colors: colors, 38 | onDragEnd: onDragEnd, 39 | onDragStart: onDragStart, 40 | onDragUpdate: onDragUpdate, 41 | draggableProgressBar: draggableProgressBar, 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/src/material/widgets/options_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:chewie/src/models/option_item.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class OptionsDialog extends StatefulWidget { 5 | const OptionsDialog({ 6 | super.key, 7 | required this.options, 8 | this.cancelButtonText, 9 | }); 10 | 11 | final List options; 12 | final String? cancelButtonText; 13 | 14 | @override 15 | // ignore: library_private_types_in_public_api 16 | _OptionsDialogState createState() => _OptionsDialogState(); 17 | } 18 | 19 | class _OptionsDialogState extends State { 20 | @override 21 | Widget build(BuildContext context) { 22 | return SafeArea( 23 | child: Column( 24 | mainAxisSize: MainAxisSize.min, 25 | children: [ 26 | ListView.builder( 27 | shrinkWrap: true, 28 | itemCount: widget.options.length, 29 | itemBuilder: (context, i) { 30 | return ListTile( 31 | onTap: () => widget.options[i].onTap(context), 32 | leading: Icon(widget.options[i].iconData), 33 | title: Text(widget.options[i].title), 34 | subtitle: widget.options[i].subtitle != null 35 | ? Text(widget.options[i].subtitle!) 36 | : null, 37 | ); 38 | }, 39 | ), 40 | const Padding( 41 | padding: EdgeInsets.symmetric(horizontal: 16), 42 | child: Divider( 43 | thickness: 1.0, 44 | ), 45 | ), 46 | ListTile( 47 | onTap: () => Navigator.pop(context), 48 | leading: const Icon(Icons.close), 49 | title: Text( 50 | widget.cancelButtonText ?? 'Cancel', 51 | ), 52 | ), 53 | ], 54 | ), 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/src/material/widgets/playback_speed_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class PlaybackSpeedDialog extends StatelessWidget { 4 | const PlaybackSpeedDialog({ 5 | super.key, 6 | required List speeds, 7 | required double selected, 8 | }) : _speeds = speeds, 9 | _selected = selected; 10 | 11 | final List _speeds; 12 | final double _selected; 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | final Color selectedColor = Theme.of(context).primaryColor; 17 | 18 | return ListView.builder( 19 | shrinkWrap: true, 20 | physics: const ScrollPhysics(), 21 | itemBuilder: (context, index) { 22 | final speed = _speeds[index]; 23 | return ListTile( 24 | dense: true, 25 | title: Row( 26 | children: [ 27 | if (speed == _selected) 28 | Icon( 29 | Icons.check, 30 | size: 20.0, 31 | color: selectedColor, 32 | ) 33 | else 34 | Container(width: 20.0), 35 | const SizedBox(width: 16.0), 36 | Text(speed.toString()), 37 | ], 38 | ), 39 | selected: speed == _selected, 40 | onTap: () { 41 | Navigator.of(context).pop(speed); 42 | }, 43 | ); 44 | }, 45 | itemCount: _speeds.length, 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/src/models/index.dart: -------------------------------------------------------------------------------- 1 | export 'option_item.dart'; 2 | export 'options_translation.dart'; 3 | export 'subtitle_model.dart'; 4 | -------------------------------------------------------------------------------- /lib/src/models/option_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class OptionItem { 4 | OptionItem({ 5 | required this.onTap, 6 | required this.iconData, 7 | required this.title, 8 | this.subtitle, 9 | }); 10 | 11 | final void Function(BuildContext context) onTap; 12 | final IconData iconData; 13 | final String title; 14 | final String? subtitle; 15 | 16 | OptionItem copyWith({ 17 | Function(BuildContext context)? onTap, 18 | IconData? iconData, 19 | String? title, 20 | String? subtitle, 21 | }) { 22 | return OptionItem( 23 | onTap: onTap ?? this.onTap, 24 | iconData: iconData ?? this.iconData, 25 | title: title ?? this.title, 26 | subtitle: subtitle ?? this.subtitle, 27 | ); 28 | } 29 | 30 | @override 31 | String toString() => 32 | 'OptionItem(onTap: $onTap, iconData: $iconData, title: $title, subtitle: $subtitle)'; 33 | 34 | @override 35 | bool operator ==(Object other) { 36 | if (identical(this, other)) return true; 37 | 38 | return other is OptionItem && 39 | other.onTap == onTap && 40 | other.iconData == iconData && 41 | other.title == title && 42 | other.subtitle == subtitle; 43 | } 44 | 45 | @override 46 | int get hashCode => 47 | onTap.hashCode ^ iconData.hashCode ^ title.hashCode ^ subtitle.hashCode; 48 | } 49 | -------------------------------------------------------------------------------- /lib/src/models/options_translation.dart: -------------------------------------------------------------------------------- 1 | class OptionsTranslation { 2 | OptionsTranslation({ 3 | this.playbackSpeedButtonText, 4 | this.subtitlesButtonText, 5 | this.cancelButtonText, 6 | }); 7 | 8 | String? playbackSpeedButtonText; 9 | String? subtitlesButtonText; 10 | String? cancelButtonText; 11 | 12 | OptionsTranslation copyWith({ 13 | String? playbackSpeedButtonText, 14 | String? subtitlesButtonText, 15 | String? cancelButtonText, 16 | }) { 17 | return OptionsTranslation( 18 | playbackSpeedButtonText: 19 | playbackSpeedButtonText ?? this.playbackSpeedButtonText, 20 | subtitlesButtonText: subtitlesButtonText ?? this.subtitlesButtonText, 21 | cancelButtonText: cancelButtonText ?? this.cancelButtonText, 22 | ); 23 | } 24 | 25 | @override 26 | String toString() => 27 | 'OptionsTranslation(playbackSpeedButtonText: $playbackSpeedButtonText, subtitlesButtonText: $subtitlesButtonText, cancelButtonText: $cancelButtonText)'; 28 | 29 | @override 30 | bool operator ==(Object other) { 31 | if (identical(this, other)) return true; 32 | 33 | return other is OptionsTranslation && 34 | other.playbackSpeedButtonText == playbackSpeedButtonText && 35 | other.subtitlesButtonText == subtitlesButtonText && 36 | other.cancelButtonText == cancelButtonText; 37 | } 38 | 39 | @override 40 | int get hashCode => 41 | playbackSpeedButtonText.hashCode ^ 42 | subtitlesButtonText.hashCode ^ 43 | cancelButtonText.hashCode; 44 | } 45 | -------------------------------------------------------------------------------- /lib/src/models/subtitle_model.dart: -------------------------------------------------------------------------------- 1 | class Subtitles { 2 | Subtitles(this.subtitle); 3 | 4 | final List subtitle; 5 | 6 | bool get isEmpty => subtitle.isEmpty; 7 | 8 | bool get isNotEmpty => !isEmpty; 9 | 10 | List getByPosition(Duration position) { 11 | final found = subtitle.where((item) { 12 | if (item != null) return position >= item.start && position <= item.end; 13 | return false; 14 | }).toList(); 15 | 16 | return found; 17 | } 18 | } 19 | 20 | class Subtitle { 21 | Subtitle({ 22 | required this.index, 23 | required this.start, 24 | required this.end, 25 | required this.text, 26 | }); 27 | 28 | Subtitle copyWith({ 29 | int? index, 30 | Duration? start, 31 | Duration? end, 32 | dynamic text, 33 | }) { 34 | return Subtitle( 35 | index: index ?? this.index, 36 | start: start ?? this.start, 37 | end: end ?? this.end, 38 | text: text ?? this.text, 39 | ); 40 | } 41 | 42 | final int index; 43 | final Duration start; 44 | final Duration end; 45 | final dynamic text; 46 | 47 | @override 48 | String toString() { 49 | return 'Subtitle(index: $index, start: $start, end: $end, text: $text)'; 50 | } 51 | 52 | @override 53 | bool operator ==(Object other) { 54 | if (identical(this, other)) return true; 55 | 56 | return other is Subtitle && 57 | other.index == index && 58 | other.start == start && 59 | other.end == end && 60 | other.text == text; 61 | } 62 | 63 | @override 64 | int get hashCode { 65 | return index.hashCode ^ start.hashCode ^ end.hashCode ^ text.hashCode; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/src/notifiers/index.dart: -------------------------------------------------------------------------------- 1 | export 'player_notifier.dart'; 2 | -------------------------------------------------------------------------------- /lib/src/notifiers/player_notifier.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// 4 | /// The new State-Manager for Chewie! 5 | /// Has to be an instance of Singleton to survive 6 | /// over all State-Changes inside chewie 7 | /// 8 | class PlayerNotifier extends ChangeNotifier { 9 | PlayerNotifier._( 10 | bool hideStuff, 11 | ) : _hideStuff = hideStuff; 12 | 13 | bool _hideStuff; 14 | 15 | bool get hideStuff => _hideStuff; 16 | 17 | set hideStuff(bool value) { 18 | _hideStuff = value; 19 | notifyListeners(); 20 | } 21 | 22 | // ignore: prefer_constructors_over_static_methods 23 | static PlayerNotifier init() { 24 | return PlayerNotifier._( 25 | true, 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/src/player_with_controls.dart: -------------------------------------------------------------------------------- 1 | import 'package:chewie/src/chewie_player.dart'; 2 | import 'package:chewie/src/helpers/adaptive_controls.dart'; 3 | import 'package:chewie/src/notifiers/index.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:provider/provider.dart'; 6 | import 'package:video_player/video_player.dart'; 7 | 8 | class PlayerWithControls extends StatelessWidget { 9 | const PlayerWithControls({super.key}); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | final ChewieController chewieController = ChewieController.of(context); 14 | 15 | double calculateAspectRatio(BuildContext context) { 16 | final size = MediaQuery.of(context).size; 17 | final width = size.width; 18 | final height = size.height; 19 | 20 | return width > height ? width / height : height / width; 21 | } 22 | 23 | Widget buildControls( 24 | BuildContext context, 25 | ChewieController chewieController, 26 | ) { 27 | return chewieController.showControls 28 | ? chewieController.customControls ?? const AdaptiveControls() 29 | : const SizedBox(); 30 | } 31 | 32 | Widget buildPlayerWithControls( 33 | ChewieController chewieController, 34 | BuildContext context, 35 | ) { 36 | return Stack( 37 | children: [ 38 | if (chewieController.placeholder != null) 39 | chewieController.placeholder!, 40 | InteractiveViewer( 41 | transformationController: chewieController.transformationController, 42 | maxScale: chewieController.maxScale, 43 | panEnabled: chewieController.zoomAndPan, 44 | scaleEnabled: chewieController.zoomAndPan, 45 | child: Center( 46 | child: AspectRatio( 47 | aspectRatio: chewieController.aspectRatio ?? 48 | chewieController.videoPlayerController.value.aspectRatio, 49 | child: VideoPlayer(chewieController.videoPlayerController), 50 | ), 51 | ), 52 | ), 53 | if (chewieController.overlay != null) chewieController.overlay!, 54 | if (Theme.of(context).platform != TargetPlatform.iOS) 55 | Consumer( 56 | builder: ( 57 | BuildContext context, 58 | PlayerNotifier notifier, 59 | Widget? widget, 60 | ) => 61 | Visibility( 62 | visible: !notifier.hideStuff, 63 | child: AnimatedOpacity( 64 | opacity: notifier.hideStuff ? 0.0 : 0.8, 65 | duration: const Duration( 66 | milliseconds: 250, 67 | ), 68 | child: const DecoratedBox( 69 | decoration: BoxDecoration(color: Colors.black54), 70 | child: SizedBox.expand(), 71 | ), 72 | ), 73 | ), 74 | ), 75 | if (!chewieController.isFullScreen) 76 | buildControls(context, chewieController) 77 | else 78 | SafeArea( 79 | bottom: false, 80 | child: buildControls(context, chewieController), 81 | ), 82 | ], 83 | ); 84 | } 85 | 86 | return LayoutBuilder( 87 | builder: (BuildContext context, BoxConstraints constraints) { 88 | return Center( 89 | child: SizedBox( 90 | height: constraints.maxHeight, 91 | width: constraints.maxWidth, 92 | child: AspectRatio( 93 | aspectRatio: calculateAspectRatio(context), 94 | child: buildPlayerWithControls(chewieController, context), 95 | ), 96 | ), 97 | ); 98 | }); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/src/progress_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:chewie/chewie.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:video_player/video_player.dart'; 4 | 5 | class VideoProgressBar extends StatefulWidget { 6 | VideoProgressBar( 7 | this.controller, { 8 | ChewieProgressColors? colors, 9 | this.onDragEnd, 10 | this.onDragStart, 11 | this.onDragUpdate, 12 | this.draggableProgressBar = true, 13 | super.key, 14 | required this.barHeight, 15 | required this.handleHeight, 16 | required this.drawShadow, 17 | }) : colors = colors ?? ChewieProgressColors(); 18 | 19 | final VideoPlayerController controller; 20 | final ChewieProgressColors colors; 21 | final Function()? onDragStart; 22 | final Function()? onDragEnd; 23 | final Function()? onDragUpdate; 24 | 25 | final double barHeight; 26 | final double handleHeight; 27 | final bool drawShadow; 28 | final bool draggableProgressBar; 29 | 30 | @override 31 | // ignore: library_private_types_in_public_api 32 | _VideoProgressBarState createState() { 33 | return _VideoProgressBarState(); 34 | } 35 | } 36 | 37 | class _VideoProgressBarState extends State { 38 | void listener() { 39 | if (!mounted) return; 40 | setState(() {}); 41 | } 42 | 43 | bool _controllerWasPlaying = false; 44 | 45 | Offset? _latestDraggableOffset; 46 | 47 | VideoPlayerController get controller => widget.controller; 48 | 49 | @override 50 | void initState() { 51 | super.initState(); 52 | controller.addListener(listener); 53 | } 54 | 55 | @override 56 | void deactivate() { 57 | controller.removeListener(listener); 58 | super.deactivate(); 59 | } 60 | 61 | void _seekToRelativePosition(Offset globalPosition) { 62 | controller.seekTo(context.calcRelativePosition( 63 | controller.value.duration, 64 | globalPosition, 65 | )); 66 | } 67 | 68 | @override 69 | Widget build(BuildContext context) { 70 | final child = Center( 71 | child: StaticProgressBar( 72 | value: controller.value, 73 | colors: widget.colors, 74 | barHeight: widget.barHeight, 75 | handleHeight: widget.handleHeight, 76 | drawShadow: widget.drawShadow, 77 | latestDraggableOffset: _latestDraggableOffset, 78 | ), 79 | ); 80 | 81 | return widget.draggableProgressBar 82 | ? GestureDetector( 83 | onHorizontalDragStart: (DragStartDetails details) { 84 | if (!controller.value.isInitialized) { 85 | return; 86 | } 87 | _controllerWasPlaying = controller.value.isPlaying; 88 | if (_controllerWasPlaying) { 89 | controller.pause(); 90 | } 91 | 92 | widget.onDragStart?.call(); 93 | }, 94 | onHorizontalDragUpdate: (DragUpdateDetails details) { 95 | if (!controller.value.isInitialized) { 96 | return; 97 | } 98 | _latestDraggableOffset = details.globalPosition; 99 | listener(); 100 | 101 | widget.onDragUpdate?.call(); 102 | }, 103 | onHorizontalDragEnd: (DragEndDetails details) { 104 | if (_controllerWasPlaying) { 105 | controller.play(); 106 | } 107 | 108 | if (_latestDraggableOffset != null) { 109 | _seekToRelativePosition(_latestDraggableOffset!); 110 | _latestDraggableOffset = null; 111 | } 112 | 113 | widget.onDragEnd?.call(); 114 | }, 115 | onTapDown: (TapDownDetails details) { 116 | if (!controller.value.isInitialized) { 117 | return; 118 | } 119 | _seekToRelativePosition(details.globalPosition); 120 | }, 121 | child: child, 122 | ) 123 | : child; 124 | } 125 | } 126 | 127 | class StaticProgressBar extends StatelessWidget { 128 | const StaticProgressBar({ 129 | super.key, 130 | required this.value, 131 | required this.colors, 132 | required this.barHeight, 133 | required this.handleHeight, 134 | required this.drawShadow, 135 | this.latestDraggableOffset, 136 | }); 137 | 138 | final Offset? latestDraggableOffset; 139 | final VideoPlayerValue value; 140 | final ChewieProgressColors colors; 141 | 142 | final double barHeight; 143 | final double handleHeight; 144 | final bool drawShadow; 145 | 146 | @override 147 | Widget build(BuildContext context) { 148 | return Container( 149 | height: MediaQuery.of(context).size.height, 150 | width: MediaQuery.of(context).size.width, 151 | color: Colors.transparent, 152 | child: CustomPaint( 153 | painter: _ProgressBarPainter( 154 | value: value, 155 | draggableValue: latestDraggableOffset != null 156 | ? context.calcRelativePosition( 157 | value.duration, 158 | latestDraggableOffset!, 159 | ) 160 | : null, 161 | colors: colors, 162 | barHeight: barHeight, 163 | handleHeight: handleHeight, 164 | drawShadow: drawShadow, 165 | ), 166 | ), 167 | ); 168 | } 169 | } 170 | 171 | class _ProgressBarPainter extends CustomPainter { 172 | _ProgressBarPainter({ 173 | required this.value, 174 | required this.colors, 175 | required this.barHeight, 176 | required this.handleHeight, 177 | required this.drawShadow, 178 | required this.draggableValue, 179 | }); 180 | 181 | VideoPlayerValue value; 182 | ChewieProgressColors colors; 183 | 184 | final double barHeight; 185 | final double handleHeight; 186 | final bool drawShadow; 187 | 188 | /// The value of the draggable progress bar. 189 | /// If null, the progress bar is not being dragged. 190 | final Duration? draggableValue; 191 | 192 | @override 193 | bool shouldRepaint(CustomPainter painter) { 194 | return true; 195 | } 196 | 197 | @override 198 | void paint(Canvas canvas, Size size) { 199 | final baseOffset = size.height / 2 - barHeight / 2; 200 | 201 | canvas.drawRRect( 202 | RRect.fromRectAndRadius( 203 | Rect.fromPoints( 204 | Offset(0.0, baseOffset), 205 | Offset(size.width, baseOffset + barHeight), 206 | ), 207 | const Radius.circular(4.0), 208 | ), 209 | colors.backgroundPaint, 210 | ); 211 | if (!value.isInitialized) { 212 | return; 213 | } 214 | final double playedPartPercent = (draggableValue != null 215 | ? draggableValue!.inMilliseconds 216 | : value.position.inMilliseconds) / 217 | value.duration.inMilliseconds; 218 | final double playedPart = 219 | playedPartPercent > 1 ? size.width : playedPartPercent * size.width; 220 | for (final DurationRange range in value.buffered) { 221 | final double start = range.startFraction(value.duration) * size.width; 222 | final double end = range.endFraction(value.duration) * size.width; 223 | canvas.drawRRect( 224 | RRect.fromRectAndRadius( 225 | Rect.fromPoints( 226 | Offset(start, baseOffset), 227 | Offset(end, baseOffset + barHeight), 228 | ), 229 | const Radius.circular(4.0), 230 | ), 231 | colors.bufferedPaint, 232 | ); 233 | } 234 | canvas.drawRRect( 235 | RRect.fromRectAndRadius( 236 | Rect.fromPoints( 237 | Offset(0.0, baseOffset), 238 | Offset(playedPart, baseOffset + barHeight), 239 | ), 240 | const Radius.circular(4.0), 241 | ), 242 | colors.playedPaint, 243 | ); 244 | 245 | if (drawShadow) { 246 | final Path shadowPath = Path() 247 | ..addOval( 248 | Rect.fromCircle( 249 | center: Offset(playedPart, baseOffset + barHeight / 2), 250 | radius: handleHeight, 251 | ), 252 | ); 253 | 254 | canvas.drawShadow(shadowPath, Colors.black, 0.2, false); 255 | } 256 | 257 | canvas.drawCircle( 258 | Offset(playedPart, baseOffset + barHeight / 2), 259 | handleHeight, 260 | colors.handlePaint, 261 | ); 262 | } 263 | } 264 | 265 | extension RelativePositionExtensions on BuildContext { 266 | Duration calcRelativePosition( 267 | Duration videoDuration, 268 | Offset globalPosition, 269 | ) { 270 | final box = findRenderObject()! as RenderBox; 271 | final Offset tapPos = box.globalToLocal(globalPosition); 272 | final double relative = (tapPos.dx / box.size.width).clamp(0, 1); 273 | final Duration position = videoDuration * relative; 274 | return position; 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: chewie 2 | description: A video player for Flutter with Cupertino and Material play controls 3 | version: 1.11.3 4 | homepage: https://github.com/fluttercommunity/chewie 5 | 6 | environment: 7 | sdk: '>=3.6.0 <4.0.0' 8 | flutter: ">=3.27.0" 9 | 10 | dependencies: 11 | cupertino_icons: ^1.0.8 12 | flutter: 13 | sdk: flutter 14 | provider: ^6.1.2 15 | video_player: ^2.9.3 16 | wakelock_plus: ^1.2.10 17 | 18 | dev_dependencies: 19 | flutter_test: 20 | sdk: flutter 21 | flutter_lints: ^5.0.0 22 | 23 | flutter: 24 | uses-material-design: true 25 | -------------------------------------------------------------------------------- /test/uninitialized_controls_state_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:chewie/chewie.dart'; 2 | import 'package:chewie/src/center_play_button.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | import 'package:video_player/video_player.dart'; 6 | 7 | List srcs = [ 8 | "https://assets.mixkit.co/videos/preview/mixkit-spinning-around-the-earth-29351-large.mp4", 9 | "https://assets.mixkit.co/videos/preview/mixkit-daytime-city-traffic-aerial-view-56-large.mp4", 10 | "https://assets.mixkit.co/videos/preview/mixkit-a-girl-blowing-a-bubble-gum-at-an-amusement-park-1226-large.mp4" 11 | ]; 12 | 13 | main() { 14 | testWidgets("MaterialControls state test", (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | var videoPlayerController = VideoPlayerController.networkUrl( 17 | Uri.parse(srcs[0]), 18 | ); 19 | var materialControlsKey = GlobalKey(); 20 | var chewieController = ChewieController( 21 | videoPlayerController: videoPlayerController, 22 | autoPlay: false, 23 | looping: false, 24 | customControls: MaterialControls( 25 | key: materialControlsKey, 26 | ), 27 | ); 28 | await tester.pumpWidget( 29 | MaterialApp( 30 | home: Scaffold( 31 | body: Chewie( 32 | controller: chewieController, 33 | ), 34 | ), 35 | ), 36 | ); 37 | 38 | await tester.pump(); 39 | 40 | var playButton = find.byType(CenterPlayButton); 41 | expect(playButton, findsOneWidget); 42 | var btn = playButton.first; 43 | var playButtonWidget = tester.widget(btn); 44 | expect(playButtonWidget.isFinished, false); 45 | }); 46 | 47 | testWidgets("CupertinoControls state test", (WidgetTester tester) async { 48 | // Build our app and trigger a frame. 49 | var videoPlayerController = VideoPlayerController.networkUrl( 50 | Uri.parse(srcs[0]), 51 | ); 52 | var materialControlsKey = GlobalKey(); 53 | var chewieController = ChewieController( 54 | videoPlayerController: videoPlayerController, 55 | autoPlay: false, 56 | looping: false, 57 | customControls: CupertinoControls( 58 | key: materialControlsKey, 59 | backgroundColor: Colors.black, 60 | iconColor: Colors.white, 61 | ), 62 | ); 63 | await tester.pumpWidget( 64 | MaterialApp( 65 | home: Scaffold( 66 | body: Chewie( 67 | controller: chewieController, 68 | ), 69 | ), 70 | ), 71 | ); 72 | 73 | await tester.pump(); 74 | 75 | var playButton = find.byType(CenterPlayButton); 76 | expect(playButton, findsOneWidget); 77 | var btn = playButton.first; 78 | var playButtonWidget = tester.widget(btn); 79 | expect(playButtonWidget.isFinished, false); 80 | }); 81 | 82 | testWidgets("MaterialDesktopControls state test", 83 | (WidgetTester tester) async { 84 | // Build our app and trigger a frame. 85 | var videoPlayerController = VideoPlayerController.networkUrl( 86 | Uri.parse(srcs[0]), 87 | ); 88 | var materialControlsKey = GlobalKey(); 89 | var chewieController = ChewieController( 90 | videoPlayerController: videoPlayerController, 91 | autoPlay: false, 92 | looping: false, 93 | customControls: MaterialDesktopControls( 94 | key: materialControlsKey, 95 | ), 96 | ); 97 | await tester.pumpWidget( 98 | MaterialApp( 99 | home: Scaffold( 100 | body: Chewie( 101 | controller: chewieController, 102 | ), 103 | ), 104 | ), 105 | ); 106 | 107 | await tester.pump(); 108 | 109 | var playButton = find.byType(CenterPlayButton); 110 | expect(playButton, findsOneWidget); 111 | var btn = playButton.first; 112 | var playButtonWidget = tester.widget(btn); 113 | expect(playButtonWidget.isFinished, false); 114 | }); 115 | } 116 | --------------------------------------------------------------------------------