diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..9f29d8253 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,61 @@ +name: CI + +on: + pull_request: + paths-ignore: + - '**.md' + push: + branches: + - master + paths-ignore: + - '**.md' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + # Does a sanity check that packages at least pass analysis on the N-1 + # versions of Flutter stable if the package claims to support that version. + # This is to minimize accidentally making changes that break old versions + # (which we don't commit to supporting, but don't want to actively break) + # without updating the constraints. + lint_and_build: + name: Flutter Version ${{ matrix.flutter-version }} Lint and Build. + runs-on: ubuntu-latest + strategy: + matrix: + flutter-version: + # The version of Flutter to use should use the minimum Dart SDK version supported by the package, + # refer to https://docs.flutter.dev/development/tools/sdk/releases. + # Note: The version below should be manually updated to the latest second most recent version + # after a new stable version comes out. + - "3.27.4" + - "3.x" + steps: + - name: 📚 Git Checkout + uses: actions/checkout@v4 + + - name: 🐦 Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: ${{ matrix.flutter-version }} + channel: stable + cache: true + cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} + + - name: 📦 Install Dependencies + run: flutter packages get + + - name: ✨ Check Formatting + run: dart format --set-exit-if-changed lib + + - name: 🕵️ Analyze + run: flutter analyze lib + + - name: 🧪 Run Tests + run: flutter test --no-pub --coverage --test-randomize-ordering-seed random + + - name: 📁 Upload coverage to Codecov + uses: codecov/codecov-action@v5 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..52f6f6f98 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,28 @@ +name: Create release + +on: + workflow_dispatch: + +jobs: + changelog: + name: Create changelog + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Conventional Changelog Action + id: changelog + uses: TriPSs/conventional-changelog-action@v3 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + version-file: ./pubspec.yaml + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ steps.changelog.outputs.tag }} + release_name: ${{ steps.changelog.outputs.tag }} + body: ${{ steps.changelog.outputs.clean_changelog }} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 3672146a2..8991527a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,247 @@ +## [1.11.3] +* 🛠️ [#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). + +## [1.11.2] +* 🛠️ [#912](https://github.com/fluttercommunity/chewie/pull/912): Add workaround for invalid buffering info on android. Thanks [timoxd7](https://github.com/timoxd7). + +## [1.11.1] +* ⬆️ [#875](https://github.com/fluttercommunity/chewie/pull/875): Add background tap to pause video feature. Thanks [Ortes](https://github.com/Ortes). +* 🛠️ [#896](https://github.com/fluttercommunity/chewie/pull/896): Fixed allowMute being ignored on Desktop. Thanks [mpoimer](https://github.com/mpoimer). +* 🛠️ [#910](https://github.com/fluttercommunity/chewie/pull/910): Fix example on web. Thanks [Ortes](https://github.com/Ortes). + +## [1.11.0] +* ⬆️ [#900](https://github.com/fluttercommunity/chewie/pull/900): Flutter `3.29` upgrade. Thanks [diegotori](https://github.com/diegotori). +* **BREAKING CHANGE**: Library now requires at least Flutter version `3.27.0`, for real this time. + +## [1.10.0] +* 🛠️ [#871](https://github.com/fluttercommunity/chewie/pull/871): Fixed pop the wrong page when changing the speed. Thanks [akmalova](https://github.com/akmalova). +* **BREAKING CHANGES**: + * `OptionItem.onTap` now takes in a `BuildContext`. + * `OptionItem`'s properties are now marked as `final`. Use `copyWith` to mutate its properties into + a new instance. + +## [1.9.2] +* Fixed broken Table of Contents links in `README.md`, take two. + +## [1.9.1+1] +* Fixed broken Table of Contents links in `README.md`. + +## [1.9.1] +* [#872](https://github.com/fluttercommunity/chewie/pull/872): feat: Add showSubtitles flag to control subtitles (#648). Thanks [floodoo](https://github.com/floodoo). +* [#890](https://github.com/fluttercommunity/chewie/pull/890): Fix issue 888. Thanks [diegotori](https://github.com/diegotori). +* **IMPORTANT**: Relaxed the minimum supported Flutter version to `3.24`. + From now on, this library will make a best effort to support the latest `N-1` Flutter version at the minimum. + +## [1.9.0] +* **BREAKING CHANGE**: Library now requires at least Flutter version `3.27.0`. + +## [1.8.7] +* ⬆️ [#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). + +## [1.8.6] +* ⬆️ [#874](https://github.com/fluttercommunity/chewie/pull/874): Add `devtools_options.yaml` configuration files. Thanks [MoRmdn](https://github.com/MoRmdn). + +## [1.8.5] +* ⬆️ [#703](https://github.com/fluttercommunity/chewie/pull/703): Adding Seek buttons for Android. Thanks [GyanendroKh](https://github.com/GyanendroKh). +* Upgraded `wakelock_plus` to version `1.2.8`, which uses `web` version `1.0.0`. Thanks [diegotori](https://github.com/diegotori). + +## [1.8.4] +* 🛠️ [#838](https://github.com/fluttercommunity/chewie/pull/838): Add bufferingBuilder. Thanks [daniellampl](https://github.com/daniellampl). + +## [1.8.3] +* 🛠️ [#828](https://github.com/fluttercommunity/chewie/pull/828): Fix the logic of the Center Play Button icon selection. Thanks [EmreDET](https://github.com/EmreDET). + +## 1.8.2 +* ⬆️ [#842](https://github.com/fluttercommunity/chewie/pull/842): package upgrades. Thanks [vaishnavi-2301](https://github.com/vaishnavi-2301). + +## 1.8.1 +* ⬆️ [#825](https://github.com/fluttercommunity/chewie/pull/825): Upgraded `wakelock_plus` to version `1.2.2`. Thanks [diegotori](https://github.com/diegotori). + +## 1.8.0 +* 🛠️ [#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). +* 🛠️ [#815](https://github.com/fluttercommunity/chewie/pull/815): Fix the Safe area conflict for material controls in Android. Thanks [MadGeorge](https://github.com/MadGeorge). +* 🛠️ [#821](https://github.com/fluttercommunity/chewie/pull/821): Upgrade chewie's dependency package. Thanks [ycv005](https://github.com/ycv005). +* 🛠️ [#824](https://github.com/fluttercommunity/chewie/pull/824): Flutter 3.19 enforcement. Thanks [diegotori](https://github.com/diegotori). +* **BREAKING CHANGE**: Library now requires at least Flutter and Dart versions `3.19.0` and `3.3` respectively. + + +## 1.7.5 +* 🛠️ [#810](https://github.com/fluttercommunity/chewie/pull/810): Fixed : Web full screen issue (#790 #688). Thanks [ToddZeil](https://github.com/ToddZeil). +* 🛠️ [#802](https://github.com/fluttercommunity/chewie/pull/802): Update chewie_player.dart. Thanks [B0yma](https://github.com/B0yma). + +## 1.7.4 +* 🛠️ [#774](https://github.com/fluttercommunity/chewie/pull/774): Fixed : Playback speed reset on forwarding video. Thanks [Kronos-2701](https://github.com/Kronos-2701). + +## 1.7.3 +* 🛠️ [#777](https://github.com/fluttercommunity/chewie/pull/777): fix display size while Chewie wrapped by some rotate widget. Thanks [bailyzheng](https://github.com/bailyzheng). + +## 1.7.2 +* 🛠️ [#798](https://github.com/fluttercommunity/chewie/pull/798): Fix: Progress bar does not follow drag #789. Thanks [koutaro-masaki](https://github.com/koutaro-masaki). + +## 1.7.1 +* 🛠️ [#772](https://github.com/fluttercommunity/chewie/pull/772): Stop force disabling wakelock. Thanks [jan-milovanovic](https://github.com/jan-milovanovic). +* ⬆️ [#775](https://github.com/fluttercommunity/chewie/pull/775): Flutter `3.13` iOS example app upgrade. Thanks [diegotori](https://github.com/diegotori). + +## 1.7.0 +* 🛠️ [#754](https://github.com/fluttercommunity/chewie/pull/754): Upgraded `wakelock_plus` to version `1.1.0`. Thanks [diegotori](https://github.com/diegotori). +* **BREAKING CHANGE**: Library now requires at least Dart and Flutter versions `2.18` and `3.3.0` respectively. + +## 1.6.0+1 +* Added Flutter Community Banner to `README.md`. Thanks [diegotori](https://github.com/diegotori). + +## 1.6.0 +* [#747](https://github.com/fluttercommunity/chewie/pull/747): Migrated from `wakelock` to `wakelock_plus`. Thanks [diegotori](https://github.com/diegotori). +* Also upgrades `video_player` from `2.4.7` to `2.7.0`. +* **IMPORTANT**: Library now requires `Flutter`, version `2.11.0` or higher. + +## 1.5.0 +* 🛠️ [#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). +* ⬆️ Increased Dart SDK constraint to cover Dart `3.0.0` and higher. + +## 1.4.1 +* 🛠️ [#719](https://github.com/fluttercommunity/chewie/pull/719): Fix overlay not visible. Thanks [jaripekkala](https://github.com/jaripekkala). + +## 1.4.0 +* 🛠️ [#701](https://github.com/fluttercommunity/chewie/pull/701): Added Dart Analysis fixes due to Flutter 3.7. Thanks [diegotori](https://github.com/diegotori). + +## 1.3.6 +* 🛠️ [#681](https://github.com/fluttercommunity/chewie/pull/681): Flutter `3.3` lint fixes. Thanks [diegotori](https://github.com/diegotori). + +* ⬆️ [#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). + +## 1.3.5 + +* ⬆️ [#669](https://github.com/fluttercommunity/chewie/pull/669): Fix for CenterPlayButton UI bug when using Material 3. Thanks [luis901101](https://github.com/luis901101). +* ⬆️ [#658](https://github.com/fluttercommunity/chewie/pull/658): Add transformationController to Interactive Viewer. Thanks [Geevies](https://github.com/Geevies). +* ⬆️ update `video_player` to 2.4.7 +* ⬆️ update `wakelock` to 0.6.2 +* 🛠️ Fixed new linting issues +* 💡 Library is now using `flutter_lints` for all of its linting needs. + +## 1.3.4 +* ⬆️ [#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). +* ⬆️ [#623](https://github.com/fluttercommunity/chewie/pull/623): [Android] Add a delay before displaying progress indicator. Thanks [henri2h](https://github.com/henri2h). + +## 1.3.3 +* ⬆️ [#634](https://github.com/fluttercommunity/chewie/pull/634): chore: Move very_good_analysis to dev_dependencies. Thanks [JCQuintas](https://github.com/JCQuintas). + +## 1.3.2 +* ⬆️ [#626](https://github.com/fluttercommunity/chewie/pull/626): Added customizable timer to hide controls. Thanks [BuginRug](https://github.com/BuginRug). + +## 1.3.1 +* ⬆️ [#617](https://github.com/fluttercommunity/chewie/pull/617): Allow video zooming with InteractiveViewer widget. Thanks [jmsanc](https://github.com/jmsanc). + +## 1.3.0 + +* ⬆️ [#598](https://github.com/fluttercommunity/chewie/pull/598): Update `wakelock` to `^0.6.1+1`. Thanks [fehernyul](https://github.com/fehernyul). +* ⬆️ [#599](https://github.com/fluttercommunity/chewie/pull/599): Uniform controls. Thanks [BuginRug](https://github.com/BuginRug). + + **Slight Breaking Change**. Instead of: + + ```dart + typedef ChewieRoutePageBuilder = Widget Function( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + _ChewieControllerProvider controllerProvider, + ); + ``` + + It is now: + + ```dart + typedef ChewieRoutePageBuilder = Widget Function( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ChewieControllerProvider controllerProvider, + ); + ``` + + TL;DR: We had to make `_ChewieControllerProvider` public. + +* 🛠️ Fixed lint and formatting problems +* 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). + +## 1.2.3 + +* ⬆️ Update 'provider' to 6.0.1 + - fixes [#568](https://github.com/brianegan/chewie/issues/568) +* ⬆️ Update 'video_player' to 2.2.7 +* ⬆️ Update 'wakelock' to 0.5.6 +* ⬆️ Update 'lint' to 1.7.2 +* ⬆️ Update roadmap +* 🛠️ Fix lint problems +* 💡 Add very_good_analysis package +* 💡 Add analysis_options.yaml for example app + +## 1.2.2 + +* 🛠️ Fix Incorrect use of ParentDataWidget. + - Fixes: [#485](https://github.com/brianegan/chewie/issues/485) + +## 1.2.1 + +* 💡 add `showOptions` flag to show/hide the options-menu + - Fixes: [#491](https://github.com/brianegan/chewie/issues/491) +* ⬆️ update `video_player` to 2.1.5 +* 🛠️ fix MaterialUI duration text (RichText) + +## 1.2.0 + +* 🖥 __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) + - Fixes: [#188](https://github.com/brianegan/chewie/issues/478) +* Redesign `MaterialControls` (inspired by Youtube Mobile and Desktop) +* Fix squeeze of `CenterPlayButton` +* 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! + +> See [Options](https://github.com/brianegan/chewie#options) to customize your Chewie options + +## 1.1.0 + +* Add subtitle functionality + - Thanks to kirill09: [#188](https://github.com/brianegan/chewie/pull/188) with which we've improved and optimized subtitles + +> See readme on how to create subtitles and provide your own subtitleBuilder: [Subtitles](https://github.com/brianegan/chewie#Subtitles) + +## 1.0.0 + +* Migrate to Null Safety + - Thanks to miDeb: [#406](https://github.com/brianegan/chewie/pull/443) + +## 0.12.1+1 + +* Lint: Format to line length 80 for pub score + +## 0.12.2 + +* Fix: Deprecation of [`resizeToAvoidBottomPadding`](https://api.flutter.dev/flutter/material/Scaffold/resizeToAvoidBottomPadding.html). Replaced by `resizeToAvoidBottomInset` + - Thanks to: [#423](https://github.com/brianegan/chewie/pull/423) + +## 0.12.1 + +* Fix: Duration called on null for cupertino controls + - Thanks to: [#406](https://github.com/brianegan/chewie/pull/406) +* Bump required Flutter version 1.20 -> 1.22 + - Thanks to: [#401](https://github.com/brianegan/chewie/pull/401) +* Export controls in chewie.dart. + - Thanks to: [#355](https://github.com/brianegan/chewie/pull/355) +* Add `lint` linter +* Add CI to analyze and check format + +## 0.12.0 + +* Add replay feature +* Add Animated Play/Pause Button + - Thanks to: [#228](https://github.com/brianegan/chewie/pull/228) + +## 0.11.0 + +* Add playback speed controls: + - Thanks to: [#390](https://github.com/brianegan/chewie/pull/390) +* Correct dependencies: + - Thanks to: [#395](https://github.com/brianegan/chewie/pull/395) + ## 0.10.4 * Update Android example to latest support @@ -5,7 +249,6 @@ * Update Flutter SDK * Update `wakelock` dependency - ## 0.10.3+1 * Format using `dartfmt -w .` for pub.dev @@ -138,3 +381,4 @@ Initial version of Chewie, the video player with a heart of gold. * Includes Material Player Controls * Includes Cupertino Player Controls * Spike version: Focus on good looking UI. Internal code is sloppy, needs a refactor and tests + diff --git a/README.md b/README.md index d7dddfd08..a6bcb1e4b 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,63 @@ # chewie -[![Version](https://img.shields.io/badge/pub-v0.10.4-blue)](https://pub.dev/packages/chewie) + +[![Flutter Community: chewie](https://fluttercommunity.dev/_github/header/chewie)](https://github.com/fluttercommunity/community) + +[![Version](https://img.shields.io/pub/v/chewie.svg)](https://pub.dev/packages/chewie) +![CI](https://github.com/brianegan/chewie/workflows/CI/badge.svg) [![Generic badge](https://img.shields.io/badge/platform-android%20|%20ios%20|%20web%20-blue.svg)](https://pub.dev/packages/chewie) The video player for Flutter with a heart of gold. -The [`video_player`](https://pub.dartlang.org/packages/video_player) plugin provides low-level access to video playback. Chewie uses the `video_player` under the hood and wraps it in a friendly Material or Cupertino UI! +The [`video_player`](https://pub.dartlang.org/packages/video_player) plugin provides low-level +access to video playback. + +Chewie uses the `video_player` under the hood and wraps it in a friendly Material or Cupertino UI! + +## Table of Contents +1. 🚨 [IMPORTANT!!! (READ THIS FIRST)](#-important-read-this-first) +2. 🔀 [Flutter Version Compatibility](#-flutter-version-compatibility) +3. 🖼️ [Preview](#%EF%B8%8F-preview) +4. ⬇️ [Installation](#%EF%B8%8F-installation) +5. 🕹️ [Using it](#%EF%B8%8F-using-it) +6. ⚙️ [Options](#%EF%B8%8F-options) +7. 🔡 [Subtitles](#-subtitles) +8. 🧪 [Example](#-example) +9. ⏪ [Migrating from Chewie < 0.9.0](#-migrating-from-chewie--090) +10. 🗺️ [Roadmap](#%EF%B8%8F-roadmap) +11. ⚠️ [Android warning](#%EF%B8%8F-android-warning) +12. 📱 [iOS warning](#-ios-warning) + + +## 🚨 IMPORTANT!!! (READ THIS FIRST) +This library is __NOT__ responsible for any issues caused by `video_player`, since it's merely a UI +layer on top of it. + +In other words, if you see any `PlatformException`s being thrown in your app due to video playback, +they are exclusive to the `video_player` library. + +Instead, please raise an issue related to it with the [Flutter Team](https://github.com/flutter/flutter/issues/new/choose). + +## 🔀 Flutter Version Compatibility + +This library will at the very least make a solid effort to support the second most recent version +of Flutter released. In other words, it will adopt `N-1` version support at +the bare minimum. -## Demo +However, this cannot be guaranteed due to major changes between Flutter versions. Should that occur, +future updates will be released as major or minor versions as needed. -![Demo](https://github.com/brianegan/chewie/raw/master/assets/chewie_demo.gif) +## 🖼️ Preview -## Installation +| MaterialControls | MaterialDesktopControls | +|:-------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------------:| +| ![](https://github.com/brianegan/chewie/raw/master/assets/MaterialControls.png) | ![](https://github.com/brianegan/chewie/raw/master/assets/MaterialDesktopControls.png) | -In your `pubspec.yaml` file within your Flutter Project: +### CupertinoControls +![](https://github.com/brianegan/chewie/raw/master/assets/CupertinoControls.png) + +## ⬇️ Installation + +In your `pubspec.yaml` file within your Flutter Project add `chewie` and `video_player` under dependencies: ```yaml dependencies: @@ -20,12 +65,14 @@ dependencies: video_player: ``` -## Use it +## 🕹️ Using it ```dart import 'package:chewie/chewie.dart'; -final videoPlayerController = VideoPlayerController.network( - 'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4'); +import 'package:video_player/video_player.dart'; + +final videoPlayerController = VideoPlayerController.networkUrl(Uri.parse( + 'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4')); await videoPlayerController.initialize(); @@ -40,7 +87,7 @@ final playerWidget = Chewie( ); ``` -Please make sure to dispose both controller widgets after use. For example by overriding the dispose method of the a `StatefulWidget`: +Please make sure to dispose both controller widgets after use. For example, by overriding the dispose method of the a `StatefulWidget`: ```dart @override void dispose() { @@ -50,12 +97,147 @@ void dispose() { } ``` -## Example +## ⚙️ Options + +![](https://github.com/brianegan/chewie/raw/master/assets/Options.png) + +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`. + +To add additional options, just add these lines to your `ChewieController`: + +```dart +additionalOptions: (context) { + return [ + OptionItem( + onTap: () => debugPrint('My option works!'), + iconData: Icons.chat, + title: 'My localized title', + ), + OptionItem( + onTap: () => + debugPrint('Another option that works!'), + iconData: Icons.chat, + title: 'Another localized title', + ), + ]; +}, +``` + +### Customizing the modal sheet + +If you don't like the default `showModalBottomSheet` for showing your options, you can override the View with the `optionsBuilder` method: + +```dart +optionsBuilder: (context, defaultOptions) async { + await showDialog( + context: context, + builder: (ctx) { + return AlertDialog( + content: ListView.builder( + itemCount: defaultOptions.length, + itemBuilder: (_, i) => ActionChip( + label: Text(defaultOptions[i].title), + onPressed: () => + defaultOptions[i].onTap!(), + ), + ), + ); + }, + ); +}, +``` + +Your `additionalOptions` are already included here (if you provided `additionalOptions`)! + +### Translations + +What is an option without proper translation? + +To add your translation strings add: + +```dart +optionsTranslation: OptionsTranslation( + playbackSpeedButtonText: 'Wiedergabegeschwindigkeit', + subtitlesButtonText: 'Untertitel', + cancelButtonText: 'Abbrechen', +), +``` + +## 🔡 Subtitles + +> Since version 1.1.0, Chewie supports subtitles. + +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. + +### Showing Subtitles by Default + +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`. + +### Adding Subtitles + +Here’s an example of how to add subtitles to your `ChewieController`: + +```dart +ChewieController( + videoPlayerController: _videoPlayerController, + autoPlay: true, + looping: true, + subtitle: Subtitles([ + Subtitle( + index: 0, + start: Duration.zero, + end: const Duration(seconds: 10), + text: 'Hello from subtitles', + ), + Subtitle( + index: 1, + start: const Duration(seconds: 10), + end: const Duration(seconds: 20), + text: 'What’s up? :)', + ), + ]), + showSubtitles: true, // Automatically display subtitles + subtitleBuilder: (context, subtitle) => Container( + padding: const EdgeInsets.all(10.0), + child: Text( + subtitle, + style: const TextStyle(color: Colors.white), + ), + ), +); +``` + +### Subtitle Structure + +The `Subtitle` model contains the following key attributes: + +- **`index`**: A unique identifier for the subtitle, useful for database integration. +- **`start`**: The starting point of the subtitle, defined as a `Duration`. +- **`end`**: The ending point of the subtitle, defined as a `Duration`. +- **`text`**: The subtitle text that will be displayed. + +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: + +```dart +Subtitle( + index: 0, + start: Duration.zero, + end: const Duration(seconds: 10), + text: 'Hello from subtitles', +), +``` + +### Customizing Subtitles + +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. + +## 🧪 Example Please run the app in the [`example/`](https://github.com/brianegan/chewie/tree/master/example) folder to start playing! -## Migrating from Chewie < 0.9.0 -Instead of passing the `VideoPlayerController` and your options to the `Chewie` widget you now pass them to the `ChewieController` and pass that latter to the `Chewie` widget. +## ⏪ Migrating from Chewie < 0.9.0 + +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. ```dart final playerWidget = Chewie( @@ -79,9 +261,73 @@ final playerWidget = Chewie( ); ``` -## iOS warning +## 🗺️ Roadmap + +- [x] MaterialUI +- [x] MaterialDesktopUI +- [x] CupertinoUI +- [x] Options with translations +- [x] Subtitles +- [x] CustomControls +- [x] Auto-Rotate on FullScreen depending on Source Aspect-Ratio +- [x] Live-Stream and UI +- [x] AutoPlay +- [x] Placeholder +- [x] Looping +- [x] Start video at +- [x] Custom Progress-Bar colors +- [x] Custom Overlay +- [x] Allow Sleep (Wakelock) +- [x] Playbackspeed Control +- [x] Custom Route-Pagebuilder +- [x] Custom Device-Orientation and SystemOverlay before and after fullscreen +- [x] Custom ErrorBuilder +- [ ] Support different resolutions of video +- [ ] Re-design State-Manager with Provider +- [ ] Screen-Mirroring / Casting (Google Chromecast) + + +## ⚠️ Android warning + +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). + +Add the following to partly fix this behavior: + +```dart + // Your init code can be above + videoController.addListener(yourListeningMethod); + + // ... + + bool wasPlayingBefore = false; + void yourListeningMethod() { + if (!videoController.value.isPlaying && !wasPlayingBefore) { + // -> Workaround if seekTo another position while it was paused before. + // On Android this might lead to infinite loading, so just play the + // video again. + videoController.play(); + } + + wasPlayingBefore = videoController.value.isPlaying; + + // ... + } +``` + +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. + +```dart +_chewieController = ChewieController( + videoPlayerController: _videoPlayerController, + progressIndicatorDelay: Platform.isAndroid ? const Duration(days: 1) : null, +); +``` + +## 📱 iOS warning + +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` +Please refer to this [issue](https://github.com/flutter/flutter/issues/14647). -The video player plugin used by chewie is not functional on iOS simulators. An iOS device must be used during development/testing. Please refer to this [issue](https://github.com/flutter/flutter/issues/14647). ``` diff --git a/analysis_options.yaml b/analysis_options.yaml index def4217f1..9f9f8bd14 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,18 +1,17 @@ +include: package:flutter_lints/flutter.yaml + analyzer: - strong-mode: - implicit-casts: false - implicit-dynamic: false + language: + strict-raw-types: true + exclude: + - lib/generated_plugin_registrant.dart + errors: + # allow self-reference to deprecated members (we do this because otherwise we have + # to annotate every member in every test, assert, etc, when we deprecate something) + deprecated_member_use_from_same_package: ignore linter: rules: - - await_only_futures - - cancel_subscriptions - - close_sinks - - hash_and_equals - - iterable_contains_unrelated_type - - list_remove_unrelated_type - - sort_constructors_first - - test_types_in_equals - - unnecessary_new - - unrelated_type_equality_checks - - valid_regexps + close_sinks: true + sort_constructors_first: true + sort_pub_dependencies: false diff --git a/assets/CupertinoControls.png b/assets/CupertinoControls.png new file mode 100644 index 000000000..7723a3ae0 Binary files /dev/null and b/assets/CupertinoControls.png differ diff --git a/assets/MaterialControls.png b/assets/MaterialControls.png new file mode 100644 index 000000000..05f7fb8f0 Binary files /dev/null and b/assets/MaterialControls.png differ diff --git a/assets/MaterialDesktopControls.png b/assets/MaterialDesktopControls.png new file mode 100644 index 000000000..cdb982161 Binary files /dev/null and b/assets/MaterialDesktopControls.png differ diff --git a/assets/Options.png b/assets/Options.png new file mode 100644 index 000000000..ee4e0c1d3 Binary files /dev/null and b/assets/Options.png differ diff --git a/assets/chewie_demo.gif b/assets/chewie_demo.gif deleted file mode 100644 index c39e5fe7d..000000000 Binary files a/assets/chewie_demo.gif and /dev/null differ diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 000000000..fa0b357c4 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/example/.metadata b/example/.metadata index 0aad8a1ad..5e2646bb9 100644 --- a/example/.metadata +++ b/example/.metadata @@ -4,5 +4,27 @@ # This file should be version controlled and should not be manually edited. version: - revision: ab4506cad2a860e1cb6186c0957eeb86024a7c6b - channel: dev + revision: "b0850beeb25f6d5b10426284f506557f66181b36" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: b0850beeb25f6d5b10426284f506557f66181b36 + base_revision: b0850beeb25f6d5b10426284f506557f66181b36 + - platform: ios + create_revision: b0850beeb25f6d5b10426284f506557f66181b36 + base_revision: b0850beeb25f6d5b10426284f506557f66181b36 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml new file mode 100644 index 000000000..3e67ba6df --- /dev/null +++ b/example/analysis_options.yaml @@ -0,0 +1,12 @@ +include: package:flutter_lints/flutter.yaml + +analyzer: + language: + strict-raw-types: true + exclude: + - lib/generated_plugin_registrant.dart + +linter: + rules: + close_sinks: true + sort_constructors_first: true diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 72fecefc7..d2622405e 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -1,3 +1,10 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + + def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { @@ -6,11 +13,6 @@ if (localPropertiesFile.exists()) { } } -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' @@ -21,25 +23,33 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" +def keystoreProperties = new Properties() +def keystorePropertiesFile = rootProject.file('key.properties') +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +} android { - compileSdkVersion 29 + namespace "com.example.example" + compileSdk 34 - sourceSets { - main.java.srcDirs += 'src/main/kotlin' + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = '17' } - lintOptions { - disable 'InvalidPackage' + sourceSets { + main.java.srcDirs += 'src/main/kotlin' } defaultConfig { - applicationId "com.example.chewieexample" - minSdkVersion 21 - targetSdkVersion 29 + applicationId "com.example.example" + minSdk 21 + targetSdk 34 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } @@ -58,5 +68,4 @@ flutter { } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml index c919106e6..c208884f3 100644 --- a/example/android/app/src/debug/AndroidManifest.xml +++ b/example/android/app/src/debug/AndroidManifest.xml @@ -1,5 +1,5 @@ + package="com.example.example"> diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 284147c23..3da38e309 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -1,10 +1,7 @@ - - - + package="com.example.example"> + android:windowSoftInputMode="adjustResize" + android:exported="true"> + + + + + + diff --git a/example/android/app/src/main/res/drawable/launch_background.xml b/example/android/app/src/main/res/drawable/launch_background.xml index f74085f3f..304732f88 100644 --- a/example/android/app/src/main/res/drawable/launch_background.xml +++ b/example/android/app/src/main/res/drawable/launch_background.xml @@ -1,7 +1,7 @@ - + diff --git a/example/android/build.gradle b/example/android/build.gradle index 3100ad2d5..37b740654 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,31 +1,16 @@ -buildscript { - ext.kotlin_version = '1.3.50' - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - allprojects { repositories { google() - jcenter() + mavenCentral() } } rootProject.buildDir = '../build' subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" -} -subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir -} +} \ No newline at end of file diff --git a/example/android/gradle.properties b/example/android/gradle.properties index a6738207f..94adc3a3f 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -1,4 +1,3 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true -android.enableR8=true diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 296b146b7..2a0fcc1b8 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle index 44e62bcf0..0cc6b7af9 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -1,11 +1,24 @@ -include ':app' +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file('local.properties').withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty('flutter.sdk') + assert flutterSdkPath != null, 'flutter.sdk not set in local.properties' + return flutterSdkPath + }() -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" +plugins { + id 'dev.flutter.flutter-plugin-loader' version '1.0.0' + id 'com.android.application' version '8.3.1' apply false + id 'org.jetbrains.kotlin.android' version '1.8.22' apply false +} +include ':app' \ No newline at end of file diff --git a/example/devtools_options.yaml b/example/devtools_options.yaml new file mode 100644 index 000000000..fa0b357c4 --- /dev/null +++ b/example/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/example/ios/.gitignore b/example/ios/.gitignore index 1e1aafd63..7a7f9873a 100644 --- a/example/ios/.gitignore +++ b/example/ios/.gitignore @@ -1,42 +1,34 @@ -.idea/ -.vagrant/ -.sconsign.dblite -.svn/ - -.DS_Store -*.swp -profile - -DerivedData/ -build/ -GeneratedPluginRegistrant.h -GeneratedPluginRegistrant.m - -*.pbxuser +**/dgph *.mode1v3 *.mode2v3 +*.moved-aside +*.pbxuser *.perspectivev3 - -!default.pbxuser +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. !default.mode1v3 !default.mode2v3 +!default.pbxuser !default.perspectivev3 - -xcuserdata - -*.moved-aside - -*.pyc -*sync/ -Icon? -.tags* - -/Flutter/app.flx -/Flutter/app.zip -/Flutter/flutter_assets/ -/Flutter/App.framework -/Flutter/Flutter.framework -/Flutter/Generated.xcconfig -/ServiceDefinitions.json - -Pods/ diff --git a/example/ios/Flutter/.last_build_id b/example/ios/Flutter/.last_build_id deleted file mode 100644 index 0a776e232..000000000 --- a/example/ios/Flutter/.last_build_id +++ /dev/null @@ -1 +0,0 @@ -fe30cadc226fd2eb06d265fd5b62c64a \ No newline at end of file diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 6c2de8086..7c5696400 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -20,11 +20,7 @@ ???? CFBundleVersion 1.0 - UIRequiredDeviceCapabilities - - arm64 - MinimumOSVersion - 8.0 + 12.0 diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig index e8efba114..ec97fc6f3 100644 --- a/example/ios/Flutter/Debug.xcconfig +++ b/example/ios/Flutter/Debug.xcconfig @@ -1,2 +1,2 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/example/ios/Flutter/Flutter.podspec b/example/ios/Flutter/Flutter.podspec deleted file mode 100644 index 5ca30416b..000000000 --- a/example/ios/Flutter/Flutter.podspec +++ /dev/null @@ -1,18 +0,0 @@ -# -# NOTE: This podspec is NOT to be published. It is only used as a local source! -# - -Pod::Spec.new do |s| - s.name = 'Flutter' - s.version = '1.0.0' - s.summary = 'High-performance, high-fidelity mobile apps.' - s.description = <<-DESC -Flutter provides an easy and productive way to build and deploy high-performance mobile apps for Android and iOS. - DESC - s.homepage = 'https://flutter.io' - s.license = { :type => 'MIT' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s } - s.ios.deployment_target = '8.0' - s.vendored_frameworks = 'Flutter.framework' -end diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig index 399e9340e..c4855bfe2 100644 --- a/example/ios/Flutter/Release.xcconfig +++ b/example/ios/Flutter/Release.xcconfig @@ -1,2 +1,2 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index cb2834eaa..ab6356343 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,22 +3,31 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ - 0ABD858FAEF33B8F44F581F0 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 891E43F580F30980509C464D /* libPods-Runner.a */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; - 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB31CF90195004384FC /* Generated.xcconfig */; }; - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; - 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 5B171717BA079CE808D1B32C /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 73C604A80B929E096139088E /* Pods_RunnerTests.framework */; }; + 65355E45871BBAC473F56EC4 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 161F52A253A901BB69307277 /* Pods_Runner.framework */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; @@ -33,23 +42,29 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 0EA25D90DB1772C2D6071B55 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 161F52A253A901BB69307277 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 6AEFCF184013ED5CA996B82B /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 6D5C22CEED22C7791375B03E /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 73C604A80B929E096139088E /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 75018BE4F219FC27188BF5C2 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; - 891E43F580F30980509C464D /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 8BEE04C81FA7DBA9A8D36BBD /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - B03FD6D8ED78792E9B6726F4 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + CCE42FB5D8CBF00852B83E23 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + D9EB2FAA5097BC9A403E4AC5 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -57,21 +72,52 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 0ABD858FAEF33B8F44F581F0 /* libPods-Runner.a in Frameworks */, + 65355E45871BBAC473F56EC4 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + ADF676FEC51290FAF51E0789 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 5B171717BA079CE808D1B32C /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 345CF767F333A6932C0BBB1C /* Frameworks */ = { + 07E76744EA73A063120BFE5C /* Pods */ = { + isa = PBXGroup; + children = ( + D9EB2FAA5097BC9A403E4AC5 /* Pods-Runner.debug.xcconfig */, + 6AEFCF184013ED5CA996B82B /* Pods-Runner.release.xcconfig */, + 0EA25D90DB1772C2D6071B55 /* Pods-Runner.profile.xcconfig */, + CCE42FB5D8CBF00852B83E23 /* Pods-RunnerTests.debug.xcconfig */, + 75018BE4F219FC27188BF5C2 /* Pods-RunnerTests.release.xcconfig */, + 6D5C22CEED22C7791375B03E /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 22109C890749BA0B4E7C88B0 /* Frameworks */ = { isa = PBXGroup; children = ( - 891E43F580F30980509C464D /* libPods-Runner.a */, + 161F52A253A901BB69307277 /* Pods_Runner.framework */, + 73C604A80B929E096139088E /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; }; + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -89,8 +135,9 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, - E60E3BF33DDF10E510884550 /* Pods */, - 345CF767F333A6932C0BBB1C /* Frameworks */, + 331C8082294A63A400263BE5 /* RunnerTests */, + 07E76744EA73A063120BFE5C /* Pods */, + 22109C890749BA0B4E7C88B0 /* Frameworks */, ); sourceTree = ""; }; @@ -98,6 +145,7 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -105,51 +153,52 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); path = Runner; sourceTree = ""; }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; - E60E3BF33DDF10E510884550 /* Pods */ = { - isa = PBXGroup; - children = ( - 8BEE04C81FA7DBA9A8D36BBD /* Pods-Runner.debug.xcconfig */, - B03FD6D8ED78792E9B6726F4 /* Pods-Runner.release.xcconfig */, - ); - name = Pods; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 3FADDB6B4D6AA8657115BAF4 /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ADF676FEC51290FAF51E0789 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 8C591DA2CC232657C8302C0F /* [CP] Check Pods Manifest.lock */, + 6B3BB5498CBA14DCD8E20CF8 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 8882ECDD31D097CE5A94142C /* [CP] Embed Pods Frameworks */, + 2261669A503474CE180D7658 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -166,17 +215,23 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 0910; - ORGANIZATIONNAME = "The Chromium Authors"; + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, @@ -188,19 +243,25 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); @@ -209,12 +270,31 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 2261669A503474CE180D7658 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -223,34 +303,42 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 8882ECDD31D097CE5A94142C /* [CP] Embed Pods Frameworks */ = { + 3FADDB6B4D6AA8657115BAF4 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${PODS_ROOT}/../Flutter/Flutter.framework", + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( ); - name = "[CP] Embed Pods Frameworks"; outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 8C591DA2CC232657C8302C0F /* [CP] Check Pods Manifest.lock */ = { + 6B3BB5498CBA14DCD8E20CF8 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); @@ -261,6 +349,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -276,18 +365,33 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -308,10 +412,135 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = K4NVK382X5; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = CCE42FB5D8CBF00852B83E23 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 75018BE4F219FC27188BF5C2 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6D5C22CEED22C7791375B03E /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -321,12 +550,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; @@ -339,6 +570,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -353,7 +585,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -365,6 +597,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -374,12 +607,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; @@ -392,6 +627,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -400,9 +636,12 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -412,21 +651,22 @@ isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { - ARCHS = arm64; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = K4NVK382X5; ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( + LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Flutter", + "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.chewieExample; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; @@ -434,32 +674,43 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { - ARCHS = arm64; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = K4NVK382X5; ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( + LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Flutter", + "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.chewieExample; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147031CF9000F007C117D /* Debug */, 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -469,6 +720,7 @@ buildConfigurations = ( 97C147061CF9000F007C117D /* Debug */, 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d526a16e..919434a62 100644 --- a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 1263ac84b..8e3ca5dfe 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ - - - - + + + + + + - - -#import - -@interface AppDelegate : FlutterAppDelegate - -@end diff --git a/example/ios/Runner/AppDelegate.m b/example/ios/Runner/AppDelegate.m deleted file mode 100644 index 112becd13..000000000 --- a/example/ios/Runner/AppDelegate.m +++ /dev/null @@ -1,12 +0,0 @@ -#include "AppDelegate.h" -#include "GeneratedPluginRegistrant.h" - -@implementation AppDelegate - -- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - // Override point for customization after application launch. - return [super application:application didFinishLaunchingWithOptions:launchOptions]; -} - -@end diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift index 70693e4a8..626664468 100644 --- a/example/ios/Runner/AppDelegate.swift +++ b/example/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ -import UIKit import Flutter +import UIKit -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index 3d43d11e6..dc9ada472 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png index 28c6bf030..7353c41ec 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png index 2ccbfd967..797d452e4 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png index f091b6b0b..6ed2d933e 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png index 4cde12118..4cd7b0099 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png index d0ef06e7e..fe730945a 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png index dcdc2306c..321773cd8 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png index 2ccbfd967..797d452e4 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png index c8f9ed8f5..502f463a9 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png index a6d6b8609..0ec303439 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png index a6d6b8609..0ec303439 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png index 75b2d164a..e9f5fea27 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png index c4df70d39..84ac32ae7 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png index 6a84f41e1..8953cba09 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png index d0e1f5853..0467bf12a 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index 040e7d694..7f553465b 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -3,7 +3,9 @@ CFBundleDevelopmentRegion - en + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Example CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -11,25 +13,21 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - chewie_example + example CFBundlePackageType APPL CFBundleShortVersionString - 1.0 + $(FLUTTER_BUILD_NAME) CFBundleSignature ???? CFBundleVersion - 1 + $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main - UIRequiredDeviceCapabilities - - arm64 - UISupportedInterfaceOrientations UIInterfaceOrientationPortrait @@ -45,10 +43,9 @@ UIViewControllerBasedStatusBarAppearance - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/example/ios/Runner/main.m b/example/ios/Runner/main.m deleted file mode 100644 index 0ccc45001..000000000 --- a/example/ios/Runner/main.m +++ /dev/null @@ -1,9 +0,0 @@ -#import -#import -#import "AppDelegate.h" - -int main(int argc, char * argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/example/ios/RunnerTests/RunnerTests.swift b/example/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 000000000..86a7c3b1b --- /dev/null +++ b/example/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/example/lib/app/app.dart b/example/lib/app/app.dart new file mode 100644 index 000000000..6d58f29f6 --- /dev/null +++ b/example/lib/app/app.dart @@ -0,0 +1,375 @@ + +import 'package:chewie/chewie.dart'; +import 'package:chewie_example/app/theme.dart'; +import 'package:flutter/material.dart'; +import 'package:video_player/video_player.dart'; + +class ChewieDemo extends StatefulWidget { + const ChewieDemo({super.key, this.title = 'Chewie Demo'}); + + final String title; + + @override + State createState() { + return _ChewieDemoState(); + } +} + +class _ChewieDemoState extends State { + TargetPlatform? _platform; + late VideoPlayerController _videoPlayerController1; + late VideoPlayerController _videoPlayerController2; + ChewieController? _chewieController; + int? bufferDelay; + + @override + void initState() { + super.initState(); + initializePlayer(); + } + + @override + void dispose() { + _videoPlayerController1.dispose(); + _videoPlayerController2.dispose(); + _chewieController?.dispose(); + super.dispose(); + } + + List srcs = [ + "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", + "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", + "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4", + ]; + + Future initializePlayer() async { + _videoPlayerController1 = VideoPlayerController.networkUrl( + Uri.parse(srcs[currPlayIndex]), + ); + _videoPlayerController2 = VideoPlayerController.networkUrl( + Uri.parse(srcs[currPlayIndex]), + ); + await Future.wait([ + _videoPlayerController1.initialize(), + _videoPlayerController2.initialize(), + ]); + _createChewieController(); + setState(() {}); + } + + void _createChewieController() { + // final subtitles = [ + // Subtitle( + // index: 0, + // start: Duration.zero, + // end: const Duration(seconds: 10), + // text: 'Hello from subtitles', + // ), + // Subtitle( + // index: 0, + // start: const Duration(seconds: 10), + // end: const Duration(seconds: 20), + // text: 'Whats up? :)', + // ), + // ]; + + final subtitles = [ + Subtitle( + index: 0, + start: Duration.zero, + end: const Duration(seconds: 10), + text: const TextSpan( + children: [ + TextSpan( + text: 'Hello', + style: TextStyle(color: Colors.red, fontSize: 22), + ), + TextSpan( + text: ' from ', + style: TextStyle(color: Colors.green, fontSize: 20), + ), + TextSpan( + text: 'subtitles', + style: TextStyle(color: Colors.blue, fontSize: 18), + ), + ], + ), + ), + Subtitle( + index: 0, + start: const Duration(seconds: 10), + end: const Duration(seconds: 20), + text: 'Whats up? :)', + // text: const TextSpan( + // text: 'Whats up? :)', + // style: TextStyle(color: Colors.amber, fontSize: 22, fontStyle: FontStyle.italic), + // ), + ), + ]; + + _chewieController = ChewieController( + videoPlayerController: _videoPlayerController1, + autoPlay: true, + looping: true, + progressIndicatorDelay: + bufferDelay != null ? Duration(milliseconds: bufferDelay!) : null, + + additionalOptions: (context) { + return [ + OptionItem( + onTap: (context) => toggleVideo(), + iconData: Icons.live_tv_sharp, + title: 'Toggle Video Src', + ), + ]; + }, + subtitle: Subtitles(subtitles), + showSubtitles: true, + subtitleBuilder: (context, dynamic subtitle) => Container( + padding: const EdgeInsets.all(10.0), + child: subtitle is InlineSpan + ? RichText(text: subtitle) + : Text( + subtitle.toString(), + style: const TextStyle(color: Colors.black), + ), + ), + + hideControlsTimer: const Duration(seconds: 1), + + // Try playing around with some of these other options: + + // showControls: false, + // materialProgressColors: ChewieProgressColors( + // playedColor: Colors.red, + // handleColor: Colors.blue, + // backgroundColor: Colors.grey, + // bufferedColor: Colors.lightGreen, + // ), + // placeholder: Container( + // color: Colors.grey, + // ), + // autoInitialize: true, + ); + } + + int currPlayIndex = 0; + + Future toggleVideo() async { + await _videoPlayerController1.pause(); + currPlayIndex += 1; + if (currPlayIndex >= srcs.length) { + currPlayIndex = 0; + } + await initializePlayer(); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: widget.title, + theme: AppTheme.light.copyWith( + platform: _platform ?? Theme.of(context).platform, + ), + home: Scaffold( + appBar: AppBar(title: Text(widget.title)), + body: Column( + children: [ + Expanded( + child: Center( + child: _chewieController != null && + _chewieController! + .videoPlayerController.value.isInitialized + ? Chewie(controller: _chewieController!) + : const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 20), + Text('Loading'), + ], + ), + ), + ), + TextButton( + onPressed: () { + _chewieController?.enterFullScreen(); + }, + child: const Text('Fullscreen'), + ), + Row( + children: [ + Expanded( + child: TextButton( + onPressed: () { + setState(() { + _videoPlayerController1.pause(); + _videoPlayerController1.seekTo(Duration.zero); + _createChewieController(); + }); + }, + child: const Padding( + padding: EdgeInsets.symmetric(vertical: 16.0), + child: Text("Landscape Video"), + ), + ), + ), + Expanded( + child: TextButton( + onPressed: () { + setState(() { + _videoPlayerController2.pause(); + _videoPlayerController2.seekTo(Duration.zero); + _chewieController = _chewieController!.copyWith( + videoPlayerController: _videoPlayerController2, + autoPlay: true, + looping: true, + /* subtitle: Subtitles([ + Subtitle( + index: 0, + start: Duration.zero, + end: const Duration(seconds: 10), + text: 'Hello from subtitles', + ), + Subtitle( + index: 0, + start: const Duration(seconds: 10), + end: const Duration(seconds: 20), + text: 'Whats up? :)', + ), + ]), + subtitleBuilder: (context, subtitle) => Container( + padding: const EdgeInsets.all(10.0), + child: Text( + subtitle, + style: const TextStyle(color: Colors.white), + ), + ), */ + ); + }); + }, + child: const Padding( + padding: EdgeInsets.symmetric(vertical: 16.0), + child: Text("Portrait Video"), + ), + ), + ), + ], + ), + Row( + children: [ + Expanded( + child: TextButton( + onPressed: () { + setState(() { + _platform = TargetPlatform.android; + }); + }, + child: const Padding( + padding: EdgeInsets.symmetric(vertical: 16.0), + child: Text("Android controls"), + ), + ), + ), + Expanded( + child: TextButton( + onPressed: () { + setState(() { + _platform = TargetPlatform.iOS; + }); + }, + child: const Padding( + padding: EdgeInsets.symmetric(vertical: 16.0), + child: Text("iOS controls"), + ), + ), + ), + ], + ), + Row( + children: [ + Expanded( + child: TextButton( + onPressed: () { + setState(() { + _platform = TargetPlatform.windows; + }); + }, + child: const Padding( + padding: EdgeInsets.symmetric(vertical: 16.0), + child: Text("Desktop controls"), + ), + ), + ), + ], + ), + if (Theme.of(context).platform == TargetPlatform.android) + ListTile( + title: const Text("Delay"), + subtitle: DelaySlider( + delay: + _chewieController?.progressIndicatorDelay?.inMilliseconds, + onSave: (delay) async { + if (delay != null) { + bufferDelay = delay == 0 ? null : delay; + await initializePlayer(); + } + }, + ), + ), + ], + ), + ), + ); + } +} + +class DelaySlider extends StatefulWidget { + const DelaySlider({super.key, required this.delay, required this.onSave}); + + final int? delay; + final void Function(int?) onSave; + @override + State createState() => _DelaySliderState(); +} + +class _DelaySliderState extends State { + int? delay; + bool saved = false; + + @override + void initState() { + super.initState(); + delay = widget.delay; + } + + @override + Widget build(BuildContext context) { + const int max = 1000; + return ListTile( + title: Text( + "Progress indicator delay ${delay != null ? "${delay.toString()} MS" : ""}", + ), + subtitle: Slider( + value: delay != null ? (delay! / max) : 0, + onChanged: (value) async { + delay = (value * max).toInt(); + setState(() { + saved = false; + }); + }, + ), + trailing: IconButton( + icon: const Icon(Icons.save), + onPressed: saved + ? null + : () { + widget.onSave(delay); + setState(() { + saved = true; + }); + }, + ), + ); + } +} diff --git a/example/lib/app/theme.dart b/example/lib/app/theme.dart new file mode 100644 index 000000000..41ac7e4d0 --- /dev/null +++ b/example/lib/app/theme.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +// ignore: avoid_classes_with_only_static_members +class AppTheme { + static final light = ThemeData( + brightness: Brightness.light, + useMaterial3: true, + colorScheme: const ColorScheme.light(secondary: Colors.red), + disabledColor: Colors.grey.shade400, + visualDensity: VisualDensity.adaptivePlatformDensity, + ); + + static final dark = ThemeData( + brightness: Brightness.dark, + colorScheme: const ColorScheme.dark(secondary: Colors.red), + disabledColor: Colors.grey.shade400, + useMaterial3: true, + visualDensity: VisualDensity.adaptivePlatformDensity, + ); +} diff --git a/example/lib/auto_rotate.dart b/example/lib/auto_rotate.dart deleted file mode 100644 index add5622e4..000000000 --- a/example/lib/auto_rotate.dart +++ /dev/null @@ -1,245 +0,0 @@ -import 'package:auto_orientation/auto_orientation.dart'; -import 'package:chewie/chewie.dart'; -import 'package:chewie/src/chewie_player.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:video_player/video_player.dart'; - -void main() { - runApp( - ChewieDemo(), - ); -} - -class ChewieDemo extends StatefulWidget { - ChewieDemo({this.title = 'Chewie Demo'}); - - final String title; - - @override - State createState() { - return _ChewieDemoState(); - } -} - -class _ChewieDemoState extends State { - TargetPlatform _platform; - VideoPlayerController _videoPlayerController1; - VideoPlayerController _videoPlayerController2; - ChewieController _chewieController; - - @override - void initState() { - super.initState(); - this.initializeAutoRotatePlayer(); - } - - Future initializeAutoRotatePlayer() async { - _videoPlayerController1 = VideoPlayerController.network( - 'https://assets.mixkit.co/videos/preview/mixkit-forest-stream-in-the-sunlight-529-large.mp4'); - await _videoPlayerController1.initialize(); - _videoPlayerController2 = VideoPlayerController.network( - 'https://assets.mixkit.co/videos/preview/mixkit-a-girl-blowing-a-bubble-gum-at-an-amusement-park-1226-large.mp4'); - await _videoPlayerController2.initialize(); - _chewieController = ChewieController( - videoPlayerController: _videoPlayerController1, - autoPlay: true, - looping: true, - routePageBuilder: (BuildContext context, Animation animation, - Animation secondAnimation, provider) { - return AnimatedBuilder( - animation: animation, - builder: (BuildContext context, Widget child) { - return VideoScaffold( - child: Scaffold( - resizeToAvoidBottomPadding: false, - body: Container( - alignment: Alignment.center, - color: Colors.black, - child: provider, - ), - ), - ); - }, - ); - } - // Try playing around with some of these other options: - - // showControls: false, - // materialProgressColors: ChewieProgressColors( - // playedColor: Colors.red, - // handleColor: Colors.blue, - // backgroundColor: Colors.grey, - // bufferedColor: Colors.lightGreen, - // ), - // placeholder: Container( - // color: Colors.grey, - // ), - // autoInitialize: true, - ); - setState(() {}); - } - - @override - void dispose() { - _videoPlayerController1.dispose(); - _videoPlayerController2.dispose(); - _chewieController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return MaterialApp( - title: widget.title, - theme: ThemeData.light().copyWith( - platform: _platform ?? Theme.of(context).platform, - ), - home: Scaffold( - appBar: AppBar( - title: Text(widget.title), - ), - body: Column( - children: [ - Expanded( - child: Center( - child: _chewieController != null && - _chewieController - .videoPlayerController.value.initialized - ? Chewie( - controller: _chewieController, - ) - : Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - CircularProgressIndicator(), - SizedBox(height: 20), - Text('Loading'), - ], - ), - ), - ), - FlatButton( - onPressed: () { - _chewieController.enterFullScreen(); - }, - child: Text('Fullscreen'), - ), - Row( - children: [ - Expanded( - child: FlatButton( - onPressed: () { - setState(() { - _chewieController.dispose(); - _videoPlayerController1.pause(); - _videoPlayerController1.seekTo(Duration(seconds: 0)); - _chewieController = ChewieController( - videoPlayerController: _videoPlayerController1, - autoPlay: true, - looping: true, - ); - }); - }, - child: Padding( - child: Text("Landscape Video"), - padding: EdgeInsets.symmetric(vertical: 16.0), - ), - ), - ), - Expanded( - child: FlatButton( - onPressed: () { - setState(() { - _chewieController.dispose(); - _videoPlayerController2.pause(); - _videoPlayerController2.seekTo(Duration(seconds: 0)); - _chewieController = ChewieController( - videoPlayerController: _videoPlayerController2, - autoPlay: true, - looping: true, - ); - }); - }, - child: Padding( - padding: EdgeInsets.symmetric(vertical: 16.0), - child: Text("Portrait Video"), - ), - ), - ) - ], - ), - Row( - children: [ - Expanded( - child: FlatButton( - onPressed: () { - setState(() { - _platform = TargetPlatform.android; - }); - }, - child: Padding( - child: Text("Android controls"), - padding: EdgeInsets.symmetric(vertical: 16.0), - ), - ), - ), - Expanded( - child: FlatButton( - onPressed: () { - setState(() { - _platform = TargetPlatform.iOS; - }); - }, - child: Padding( - padding: EdgeInsets.symmetric(vertical: 16.0), - child: Text("iOS controls"), - ), - ), - ) - ], - ) - ], - ), - ), - ); - } -} - -class VideoScaffold extends StatefulWidget { - const VideoScaffold({Key key, this.child}) : super(key: key); - - final Widget child; - - @override - State createState() => _VideoScaffoldState(); -} - -class _VideoScaffoldState extends State { - @override - void initState() { - SystemChrome.setPreferredOrientations([ - DeviceOrientation.landscapeRight, - DeviceOrientation.landscapeLeft, - ]); - AutoOrientation.portraitUpMode(); - super.initState(); - } - - @override - dispose() { - SystemChrome.setPreferredOrientations([ - DeviceOrientation.portraitUp, - DeviceOrientation.portraitDown, - ]); - AutoOrientation.portraitUpMode(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return widget.child; - } -} diff --git a/example/lib/main.dart b/example/lib/main.dart index 630dfe1e0..496728a63 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,189 +1,6 @@ -import 'package:chewie/chewie.dart'; -import 'package:chewie/src/chewie_player.dart'; -import 'package:flutter/cupertino.dart'; +import 'package:chewie_example/app/app.dart'; import 'package:flutter/material.dart'; -import 'package:video_player/video_player.dart'; void main() { - runApp( - ChewieDemo(), - ); -} - -class ChewieDemo extends StatefulWidget { - ChewieDemo({this.title = 'Chewie Demo'}); - - final String title; - - @override - State createState() { - return _ChewieDemoState(); - } -} - -class _ChewieDemoState extends State { - TargetPlatform _platform; - VideoPlayerController _videoPlayerController1; - VideoPlayerController _videoPlayerController2; - ChewieController _chewieController; - - @override - void initState() { - super.initState(); - this.initializePlayer(); - } - - @override - void dispose() { - _videoPlayerController1.dispose(); - _videoPlayerController2.dispose(); - _chewieController.dispose(); - super.dispose(); - } - - Future initializePlayer() async { - _videoPlayerController1 = VideoPlayerController.network( - 'https://assets.mixkit.co/videos/preview/mixkit-forest-stream-in-the-sunlight-529-large.mp4'); - await _videoPlayerController1.initialize(); - _videoPlayerController2 = VideoPlayerController.network( - 'https://assets.mixkit.co/videos/preview/mixkit-a-girl-blowing-a-bubble-gum-at-an-amusement-park-1226-large.mp4'); - await _videoPlayerController2.initialize(); - _chewieController = ChewieController( - videoPlayerController: _videoPlayerController1, - autoPlay: true, - looping: true, - // Try playing around with some of these other options: - - // showControls: false, - // materialProgressColors: ChewieProgressColors( - // playedColor: Colors.red, - // handleColor: Colors.blue, - // backgroundColor: Colors.grey, - // bufferedColor: Colors.lightGreen, - // ), - // placeholder: Container( - // color: Colors.grey, - // ), - // autoInitialize: true, - ); - setState(() {}); - } - - @override - Widget build(BuildContext context) { - return MaterialApp( - title: widget.title, - theme: ThemeData.light().copyWith( - platform: _platform ?? Theme.of(context).platform, - ), - home: Scaffold( - appBar: AppBar( - title: Text(widget.title), - ), - body: Column( - children: [ - Expanded( - child: Center( - child: _chewieController != null && - _chewieController - .videoPlayerController.value.initialized - ? Chewie( - controller: _chewieController, - ) - : Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - CircularProgressIndicator(), - SizedBox(height: 20), - Text('Loading'), - ], - ), - ), - ), - FlatButton( - onPressed: () { - _chewieController.enterFullScreen(); - }, - child: Text('Fullscreen'), - ), - Row( - children: [ - Expanded( - child: FlatButton( - onPressed: () { - setState(() { - _chewieController.dispose(); - _videoPlayerController1.pause(); - _videoPlayerController1.seekTo(Duration(seconds: 0)); - _chewieController = ChewieController( - videoPlayerController: _videoPlayerController1, - autoPlay: true, - looping: true, - ); - }); - }, - child: Padding( - child: Text("Landscape Video"), - padding: EdgeInsets.symmetric(vertical: 16.0), - ), - ), - ), - Expanded( - child: FlatButton( - onPressed: () { - setState(() { - _chewieController.dispose(); - _videoPlayerController2.pause(); - _videoPlayerController2.seekTo(Duration(seconds: 0)); - _chewieController = ChewieController( - videoPlayerController: _videoPlayerController2, - autoPlay: true, - looping: true, - ); - }); - }, - child: Padding( - padding: EdgeInsets.symmetric(vertical: 16.0), - child: Text("Portrait Video"), - ), - ), - ) - ], - ), - Row( - children: [ - Expanded( - child: FlatButton( - onPressed: () { - setState(() { - _platform = TargetPlatform.android; - }); - }, - child: Padding( - child: Text("Android controls"), - padding: EdgeInsets.symmetric(vertical: 16.0), - ), - ), - ), - Expanded( - child: FlatButton( - onPressed: () { - setState(() { - _platform = TargetPlatform.iOS; - }); - }, - child: Padding( - padding: EdgeInsets.symmetric(vertical: 16.0), - child: Text("iOS controls"), - ), - ), - ) - ], - ) - ], - ), - ), - ); - } + runApp(const ChewieDemo()); } diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 7e0f68340..be412fe2a 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,30 +1,29 @@ name: chewie_example description: An example of how to use the chewie for Flutter +version: 1.0.0 +publish_to: none + +environment: + sdk: '>=3.6.0 <4.0.0' + flutter: ">=3.27.0" dependencies: chewie: path: ../ - video_player: ^1.0.0 - auto_orientation: ^1.0.6 flutter: sdk: flutter - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - - cupertino_icons: ^1.0.0 + video_player: ^2.9.3 dev_dependencies: flutter_test: sdk: flutter - + flutter_lints: ^5.0.0 # For information on the generic Dart part of this file, see the # following page: https://www.dartlang.org/tools/pub/pubspec # The following section is specific to Flutter. flutter: - # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart index aadb1712a..922999316 100644 --- a/example/test/widget_test.dart +++ b/example/test/widget_test.dart @@ -5,12 +5,12 @@ // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. -import 'package:chewie_example/auto_rotate.dart'; +import 'package:chewie_example/app/app.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. - await tester.pumpWidget(ChewieDemo()); + await tester.pumpWidget(const ChewieDemo()); }); } diff --git a/lib/chewie.dart b/lib/chewie.dart index ed95928a7..7061505ad 100644 --- a/lib/chewie.dart +++ b/lib/chewie.dart @@ -1,4 +1,9 @@ -library chewie; +library; export 'src/chewie_player.dart'; export 'src/chewie_progress_colors.dart'; +export 'src/cupertino/cupertino_controls.dart'; +export 'src/material/material_controls.dart'; +export 'src/material/material_desktop_controls.dart'; +export 'src/material/material_progress_bar.dart'; +export 'src/models/index.dart'; diff --git a/lib/src/animated_play_pause.dart b/lib/src/animated_play_pause.dart new file mode 100644 index 000000000..e41111b7a --- /dev/null +++ b/lib/src/animated_play_pause.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; + +/// A widget that animates implicitly between a play and a pause icon. +class AnimatedPlayPause extends StatefulWidget { + const AnimatedPlayPause({ + super.key, + required this.playing, + this.size, + this.color, + }); + + final double? size; + final bool playing; + final Color? color; + + @override + State createState() => AnimatedPlayPauseState(); +} + +class AnimatedPlayPauseState extends State + with SingleTickerProviderStateMixin { + late final animationController = AnimationController( + vsync: this, + value: widget.playing ? 1 : 0, + duration: const Duration(milliseconds: 400), + ); + + @override + void didUpdateWidget(AnimatedPlayPause oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.playing != oldWidget.playing) { + if (widget.playing) { + animationController.forward(); + } else { + animationController.reverse(); + } + } + } + + @override + void dispose() { + animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Center( + child: AnimatedIcon( + color: widget.color, + size: widget.size, + icon: AnimatedIcons.play_pause, + progress: animationController, + ), + ); + } +} diff --git a/lib/src/center_play_button.dart b/lib/src/center_play_button.dart new file mode 100644 index 000000000..2b1656497 --- /dev/null +++ b/lib/src/center_play_button.dart @@ -0,0 +1,55 @@ +import 'package:chewie/src/animated_play_pause.dart'; +import 'package:flutter/material.dart'; + +class CenterPlayButton extends StatelessWidget { + const CenterPlayButton({ + super.key, + required this.backgroundColor, + this.iconColor, + required this.show, + required this.isPlaying, + required this.isFinished, + this.onPressed, + }); + + final Color backgroundColor; + final Color? iconColor; + final bool show; + final bool isPlaying; + final bool isFinished; + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + return ColoredBox( + color: Colors.transparent, + child: Center( + child: UnconstrainedBox( + child: AnimatedOpacity( + opacity: show ? 1.0 : 0.0, + duration: const Duration(milliseconds: 300), + child: DecoratedBox( + decoration: BoxDecoration( + color: backgroundColor, + shape: BoxShape.circle, + ), + // Always set the iconSize on the IconButton, not on the Icon itself: + // https://github.com/flutter/flutter/issues/52980 + child: IconButton( + iconSize: 32, + padding: const EdgeInsets.all(12.0), + icon: isFinished + ? Icon(Icons.replay, color: iconColor) + : AnimatedPlayPause( + color: iconColor, + playing: isPlaying, + ), + onPressed: onPressed, + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/center_seek_button.dart b/lib/src/center_seek_button.dart new file mode 100644 index 000000000..dd0373f99 --- /dev/null +++ b/lib/src/center_seek_button.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; + +class CenterSeekButton extends StatelessWidget { + const CenterSeekButton({ + super.key, + required this.iconData, + required this.backgroundColor, + this.iconColor, + required this.show, + this.fadeDuration = const Duration(milliseconds: 300), + this.iconSize = 26, + this.onPressed, + }); + + final IconData iconData; + final Color backgroundColor; + final Color? iconColor; + final bool show; + final VoidCallback? onPressed; + final Duration fadeDuration; + final double iconSize; + + @override + Widget build(BuildContext context) { + return ColoredBox( + color: Colors.transparent, + child: Center( + child: UnconstrainedBox( + child: AnimatedOpacity( + opacity: show ? 1.0 : 0.0, + duration: fadeDuration, + child: DecoratedBox( + decoration: BoxDecoration( + color: backgroundColor, + shape: BoxShape.circle, + ), + // Always set the iconSize on the IconButton, not on the Icon itself: + // https://github.com/flutter/flutter/issues/52980 + child: IconButton( + iconSize: iconSize, + padding: const EdgeInsets.all(8.0), + icon: Icon(iconData, color: iconColor), + onPressed: onPressed, + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/chewie_player.dart b/lib/src/chewie_player.dart index 472d68507..5d76fdf6d 100644 --- a/lib/src/chewie_player.dart +++ b/lib/src/chewie_player.dart @@ -1,18 +1,23 @@ import 'dart:async'; import 'package:chewie/src/chewie_progress_colors.dart'; +import 'package:chewie/src/models/option_item.dart'; +import 'package:chewie/src/models/options_translation.dart'; +import 'package:chewie/src/models/subtitle_model.dart'; +import 'package:chewie/src/notifiers/player_notifier.dart'; import 'package:chewie/src/player_with_controls.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; import 'package:video_player/video_player.dart'; -import 'package:wakelock/wakelock.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; -typedef Widget ChewieRoutePageBuilder( +typedef ChewieRoutePageBuilder = Widget Function( BuildContext context, Animation animation, Animation secondaryAnimation, - _ChewieControllerProvider controllerProvider, + ChewieControllerProvider controllerProvider, ); /// A Video Player with Material and Cupertino skins. @@ -20,11 +25,10 @@ typedef Widget ChewieRoutePageBuilder( /// `video_player` is pretty low level. Chewie wraps it in a friendly skin to /// make it easy to use! class Chewie extends StatefulWidget { - Chewie({ - Key key, - this.controller, - }) : assert(controller != null, 'You must provide a chewie controller'), - super(key: key); + const Chewie({ + super.key, + required this.controller, + }); /// The [ChewieController] final ChewieController controller; @@ -38,15 +42,20 @@ class Chewie extends StatefulWidget { class ChewieState extends State { bool _isFullScreen = false; + bool get isControllerFullScreen => widget.controller.isFullScreen; + late PlayerNotifier notifier; + @override void initState() { super.initState(); widget.controller.addListener(listener); + notifier = PlayerNotifier.init(); } @override void dispose() { widget.controller.removeListener(listener); + notifier.dispose(); super.dispose(); } @@ -56,33 +65,42 @@ class ChewieState extends State { widget.controller.addListener(listener); } super.didUpdateWidget(oldWidget); + if (_isFullScreen != isControllerFullScreen) { + widget.controller._isFullScreen = _isFullScreen; + } } - void listener() async { - if (widget.controller.isFullScreen && !_isFullScreen) { - _isFullScreen = true; + Future listener() async { + if (isControllerFullScreen && !_isFullScreen) { + _isFullScreen = isControllerFullScreen; await _pushFullScreenWidget(context); } else if (_isFullScreen) { - Navigator.of(context, rootNavigator: true).pop(); + Navigator.of( + context, + rootNavigator: widget.controller.useRootNavigator, + ).pop(); _isFullScreen = false; } } @override Widget build(BuildContext context) { - return _ChewieControllerProvider( + return ChewieControllerProvider( controller: widget.controller, - child: PlayerWithControls(), + child: ChangeNotifierProvider.value( + value: notifier, + builder: (context, w) => const PlayerWithControls(), + ), ); } Widget _buildFullScreenVideo( BuildContext context, Animation animation, - _ChewieControllerProvider controllerProvider, + ChewieControllerProvider controllerProvider, ) { return Scaffold( - resizeToAvoidBottomPadding: false, + resizeToAvoidBottomInset: false, body: Container( alignment: Alignment.center, color: Colors.black, @@ -95,11 +113,11 @@ class ChewieState extends State { BuildContext context, Animation animation, Animation secondaryAnimation, - _ChewieControllerProvider controllerProvider, + ChewieControllerProvider controllerProvider, ) { return AnimatedBuilder( animation: animation, - builder: (BuildContext context, Widget child) { + builder: (BuildContext context, Widget? child) { return _buildFullScreenVideo(context, animation, controllerProvider); }, ); @@ -110,42 +128,64 @@ class ChewieState extends State { Animation animation, Animation secondaryAnimation, ) { - var controllerProvider = _ChewieControllerProvider( + final controllerProvider = ChewieControllerProvider( controller: widget.controller, - child: PlayerWithControls(), + child: ChangeNotifierProvider.value( + value: notifier, + builder: (context, w) => const PlayerWithControls(), + ), ); if (widget.controller.routePageBuilder == null) { return _defaultRoutePageBuilder( - context, animation, secondaryAnimation, controllerProvider); + context, + animation, + secondaryAnimation, + controllerProvider, + ); } - return widget.controller.routePageBuilder( - context, animation, secondaryAnimation, controllerProvider); + return widget.controller.routePageBuilder!( + context, + animation, + secondaryAnimation, + controllerProvider, + ); } Future _pushFullScreenWidget(BuildContext context) async { - final TransitionRoute route = PageRouteBuilder( + final TransitionRoute route = PageRouteBuilder( pageBuilder: _fullScreenRoutePageBuilder, ); onEnterFullScreen(); if (!widget.controller.allowedScreenSleep) { - Wakelock.enable(); + WakelockPlus.enable(); + } + + await Navigator.of( + context, + rootNavigator: widget.controller.useRootNavigator, + ).push(route); + + if (kIsWeb) { + _reInitializeControllers(); } - await Navigator.of(context, rootNavigator: true).push(route); _isFullScreen = false; widget.controller.exitFullScreen(); - // The wakelock plugins checks whether it needs to perform an action internally, - // so we do not need to check Wakelock.isEnabled. - Wakelock.disable(); + if (!widget.controller.allowedScreenSleep) { + WakelockPlus.disable(); + } - SystemChrome.setEnabledSystemUIOverlays( - widget.controller.systemOverlaysAfterFullScreen); + SystemChrome.setEnabledSystemUIMode( + SystemUiMode.manual, + overlays: widget.controller.systemOverlaysAfterFullScreen, + ); SystemChrome.setPreferredOrientations( - widget.controller.deviceOrientationsAfterFullScreen); + widget.controller.deviceOrientationsAfterFullScreen, + ); } void onEnterFullScreen() { @@ -153,23 +193,31 @@ class ChewieState extends State { final videoHeight = widget.controller.videoPlayerController.value.size.height; - if (widget.controller.systemOverlaysOnEnterFullScreen != null) { - /// Optional user preferred settings - SystemChrome.setEnabledSystemUIOverlays( - widget.controller.systemOverlaysOnEnterFullScreen); - } else { - /// Default behavior - SystemChrome.setEnabledSystemUIOverlays([]); - } + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); + + // if (widget.controller.systemOverlaysOnEnterFullScreen != null) { + // /// Optional user preferred settings + // SystemChrome.setEnabledSystemUIMode( + // SystemUiMode.manual, + // overlays: widget.controller.systemOverlaysOnEnterFullScreen, + // ); + // } else { + // /// Default behavior + // SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); + // } if (widget.controller.deviceOrientationsOnEnterFullScreen != null) { /// Optional user preferred settings SystemChrome.setPreferredOrientations( - widget.controller.deviceOrientationsOnEnterFullScreen); + widget.controller.deviceOrientationsOnEnterFullScreen!, + ); } else { + final isLandscapeVideo = videoWidth > videoHeight; + final isPortraitVideo = videoWidth < videoHeight; + /// Default behavior /// Video w > h means we force landscape - if (videoWidth > videoHeight) { + if (isLandscapeVideo) { SystemChrome.setPreferredOrientations([ DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight, @@ -177,7 +225,7 @@ class ChewieState extends State { } /// Video h > w means we force portrait - else if (videoHeight > videoWidth) { + else if (isPortraitVideo) { SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, DeviceOrientation.portraitDown, @@ -190,6 +238,18 @@ class ChewieState extends State { } } } + + ///When viewing full screen on web, returning from full screen causes original video to lose the picture. + ///We re initialise controllers for web only when returning from full screen + void _reInitializeControllers() { + final prevPosition = widget.controller.videoPlayerController.value.position; + widget.controller.videoPlayerController.initialize().then((_) async { + widget.controller._initialize(); + widget.controller.videoPlayerController.seekTo(prevPosition); + await widget.controller.videoPlayerController.play(); + widget.controller.videoPlayerController.pause(); + }); + } } /// The ChewieController is used to configure and drive the Chewie Player @@ -204,35 +264,208 @@ class ChewieState extends State { /// `VideoPlayerController`. class ChewieController extends ChangeNotifier { ChewieController({ - this.videoPlayerController, + required this.videoPlayerController, + this.optionsTranslation, this.aspectRatio, this.autoInitialize = false, this.autoPlay = false, + this.draggableProgressBar = true, this.startAt, this.looping = false, this.fullScreenByDefault = false, this.cupertinoProgressColors, this.materialProgressColors, + this.materialSeekButtonFadeDuration = const Duration(milliseconds: 300), + this.materialSeekButtonSize = 26, this.placeholder, this.overlay, this.showControlsOnInitialize = true, + this.showOptions = true, + this.optionsBuilder, + this.additionalOptions, this.showControls = true, + this.transformationController, + this.zoomAndPan = false, + this.maxScale = 2.5, + this.subtitle, + this.showSubtitles = false, + this.subtitleBuilder, this.customControls, this.errorBuilder, + this.bufferingBuilder, this.allowedScreenSleep = true, this.isLive = false, this.allowFullScreen = true, this.allowMuting = true, + this.allowPlaybackSpeedChanging = true, + this.useRootNavigator = true, + this.playbackSpeeds = const [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2], this.systemOverlaysOnEnterFullScreen, this.deviceOrientationsOnEnterFullScreen, this.systemOverlaysAfterFullScreen = SystemUiOverlay.values, this.deviceOrientationsAfterFullScreen = DeviceOrientation.values, - this.routePageBuilder = null, - }) : assert(videoPlayerController != null, - 'You must provide a controller to play a video') { + this.routePageBuilder, + this.progressIndicatorDelay, + this.hideControlsTimer = defaultHideControlsTimer, + this.controlsSafeAreaMinimum = EdgeInsets.zero, + this.pauseOnBackgroundTap = false, + }) : assert( + playbackSpeeds.every((speed) => speed > 0), + 'The playbackSpeeds values must all be greater than 0', + ) { _initialize(); } + ChewieController copyWith({ + VideoPlayerController? videoPlayerController, + OptionsTranslation? optionsTranslation, + double? aspectRatio, + bool? autoInitialize, + bool? autoPlay, + bool? draggableProgressBar, + Duration? startAt, + bool? looping, + bool? fullScreenByDefault, + ChewieProgressColors? cupertinoProgressColors, + ChewieProgressColors? materialProgressColors, + Duration? materialSeekButtonFadeDuration, + double? materialSeekButtonSize, + Widget? placeholder, + Widget? overlay, + bool? showControlsOnInitialize, + bool? showOptions, + Future Function(BuildContext, List)? optionsBuilder, + List Function(BuildContext)? additionalOptions, + bool? showControls, + TransformationController? transformationController, + bool? zoomAndPan, + double? maxScale, + Subtitles? subtitle, + bool? showSubtitles, + Widget Function(BuildContext, dynamic)? subtitleBuilder, + Widget? customControls, + WidgetBuilder? bufferingBuilder, + Widget Function(BuildContext, String)? errorBuilder, + bool? allowedScreenSleep, + bool? isLive, + bool? allowFullScreen, + bool? allowMuting, + bool? allowPlaybackSpeedChanging, + bool? useRootNavigator, + Duration? hideControlsTimer, + EdgeInsets? controlsSafeAreaMinimum, + List? playbackSpeeds, + List? systemOverlaysOnEnterFullScreen, + List? deviceOrientationsOnEnterFullScreen, + List? systemOverlaysAfterFullScreen, + List? deviceOrientationsAfterFullScreen, + Duration? progressIndicatorDelay, + Widget Function( + BuildContext, + Animation, + Animation, + ChewieControllerProvider, + )? routePageBuilder, + bool? pauseOnBackgroundTap, + }) { + return ChewieController( + draggableProgressBar: draggableProgressBar ?? this.draggableProgressBar, + videoPlayerController: + videoPlayerController ?? this.videoPlayerController, + optionsTranslation: optionsTranslation ?? this.optionsTranslation, + aspectRatio: aspectRatio ?? this.aspectRatio, + autoInitialize: autoInitialize ?? this.autoInitialize, + autoPlay: autoPlay ?? this.autoPlay, + startAt: startAt ?? this.startAt, + looping: looping ?? this.looping, + fullScreenByDefault: fullScreenByDefault ?? this.fullScreenByDefault, + cupertinoProgressColors: + cupertinoProgressColors ?? this.cupertinoProgressColors, + materialProgressColors: + materialProgressColors ?? this.materialProgressColors, + materialSeekButtonFadeDuration: + materialSeekButtonFadeDuration ?? this.materialSeekButtonFadeDuration, + materialSeekButtonSize: + materialSeekButtonSize ?? this.materialSeekButtonSize, + placeholder: placeholder ?? this.placeholder, + overlay: overlay ?? this.overlay, + showControlsOnInitialize: + showControlsOnInitialize ?? this.showControlsOnInitialize, + showOptions: showOptions ?? this.showOptions, + optionsBuilder: optionsBuilder ?? this.optionsBuilder, + additionalOptions: additionalOptions ?? this.additionalOptions, + showControls: showControls ?? this.showControls, + showSubtitles: showSubtitles ?? this.showSubtitles, + subtitle: subtitle ?? this.subtitle, + subtitleBuilder: subtitleBuilder ?? this.subtitleBuilder, + customControls: customControls ?? this.customControls, + errorBuilder: errorBuilder ?? this.errorBuilder, + bufferingBuilder: bufferingBuilder ?? this.bufferingBuilder, + allowedScreenSleep: allowedScreenSleep ?? this.allowedScreenSleep, + isLive: isLive ?? this.isLive, + allowFullScreen: allowFullScreen ?? this.allowFullScreen, + allowMuting: allowMuting ?? this.allowMuting, + allowPlaybackSpeedChanging: + allowPlaybackSpeedChanging ?? this.allowPlaybackSpeedChanging, + useRootNavigator: useRootNavigator ?? this.useRootNavigator, + playbackSpeeds: playbackSpeeds ?? this.playbackSpeeds, + systemOverlaysOnEnterFullScreen: systemOverlaysOnEnterFullScreen ?? + this.systemOverlaysOnEnterFullScreen, + deviceOrientationsOnEnterFullScreen: + deviceOrientationsOnEnterFullScreen ?? + this.deviceOrientationsOnEnterFullScreen, + systemOverlaysAfterFullScreen: + systemOverlaysAfterFullScreen ?? this.systemOverlaysAfterFullScreen, + deviceOrientationsAfterFullScreen: deviceOrientationsAfterFullScreen ?? + this.deviceOrientationsAfterFullScreen, + routePageBuilder: routePageBuilder ?? this.routePageBuilder, + hideControlsTimer: hideControlsTimer ?? this.hideControlsTimer, + progressIndicatorDelay: + progressIndicatorDelay ?? this.progressIndicatorDelay, + pauseOnBackgroundTap: pauseOnBackgroundTap ?? this.pauseOnBackgroundTap, + ); + } + + static const defaultHideControlsTimer = Duration(seconds: 3); + + /// If false, the options button in MaterialUI and MaterialDesktopUI + /// won't be shown. + final bool showOptions; + + /// Pass your translations for the options like: + /// - PlaybackSpeed + /// - Subtitles + /// - Cancel + /// + /// Buttons + /// + /// These are required for the default `OptionItem`'s + final OptionsTranslation? optionsTranslation; + + /// Build your own options with default chewieOptions shiped through + /// the builder method. Just add your own options to the Widget + /// you'll build. If you want to hide the chewieOptions, just leave them + /// out from your Widget. + final Future Function( + BuildContext context, + List chewieOptions, + )? optionsBuilder; + + /// Add your own additional options on top of chewie options + final List Function(BuildContext context)? additionalOptions; + + /// Define here your own Widget on how your n'th subtitle will look like + Widget Function(BuildContext context, dynamic subtitle)? subtitleBuilder; + + /// Add a List of Subtitles here in `Subtitles.subtitle` + Subtitles? subtitle; + + /// Determines whether subtitles should be shown by default when the video starts. + /// + /// If set to `true`, subtitles will be displayed automatically when the video + /// begins playing. If set to `false`, subtitles will be hidden by default. + bool showSubtitles; + /// The controller for the video you want to play final VideoPlayerController videoPlayerController; @@ -242,46 +475,68 @@ class ChewieController extends ChangeNotifier { /// Play the video as soon as it's displayed final bool autoPlay; + /// Non-Draggable Progress Bar + final bool draggableProgressBar; + /// Start video at a certain position - final Duration startAt; + final Duration? startAt; /// Whether or not the video should loop final bool looping; - /// Weather or not to show the controls when initializing the widget. + /// Wether or not to show the controls when initializing the widget. final bool showControlsOnInitialize; /// Whether or not to show the controls at all final bool showControls; + /// Controller to pass into the [InteractiveViewer] component + final TransformationController? transformationController; + + /// Whether or not to allow zooming and panning + final bool zoomAndPan; + + /// Max scale when zooming + final double maxScale; + /// Defines customised controls. Check [MaterialControls] or /// [CupertinoControls] for reference. - final Widget customControls; + final Widget? customControls; - /// When the video playback runs into an error, you can build a custom + /// When the video playback runs into an error, you can build a custom /// error message. - final Widget Function(BuildContext context, String errorMessage) errorBuilder; + final Widget Function(BuildContext context, String errorMessage)? + errorBuilder; + + /// When the video is buffering, you can build a custom widget. + final WidgetBuilder? bufferingBuilder; /// The Aspect Ratio of the Video. Important to get the correct size of the /// video! /// /// Will fallback to fitting within the space allowed. - final double aspectRatio; + final double? aspectRatio; /// The colors to use for controls on iOS. By default, the iOS player uses /// colors sampled from the original iOS 11 designs. - final ChewieProgressColors cupertinoProgressColors; + final ChewieProgressColors? cupertinoProgressColors; /// The colors to use for the Material Progress Bar. By default, the Material /// player uses the colors from your Theme. - final ChewieProgressColors materialProgressColors; + final ChewieProgressColors? materialProgressColors; + + // The duration of the fade animation for the seek button (Material Player only) + final Duration materialSeekButtonFadeDuration; + + // The size of the seek button for the Material Player only + final double materialSeekButtonSize; /// The placeholder is displayed underneath the Video before it is initialized /// or played. - final Widget placeholder; + final Widget? placeholder; /// A widget which is placed between the video and the controls - final Widget overlay; + final Widget? overlay; /// Defines if the player will start in fullscreen when play is pressed final bool fullScreenByDefault; @@ -289,7 +544,7 @@ class ChewieController extends ChangeNotifier { /// Defines if the player will sleep in fullscreen or not final bool allowedScreenSleep; - /// Defines if the controls should be for live stream video + /// Defines if the controls should be shown for live stream video final bool isLive; /// Defines if the fullscreen control should be shown @@ -298,11 +553,23 @@ class ChewieController extends ChangeNotifier { /// Defines if the mute control should be shown final bool allowMuting; + /// Defines if the playback speed control should be shown + final bool allowPlaybackSpeedChanging; + + /// Defines if push/pop navigations use the rootNavigator + final bool useRootNavigator; + + /// Defines the [Duration] before the video controls are hidden. By default, this is set to three seconds. + final Duration hideControlsTimer; + + /// Defines the set of allowed playback speeds user can change + final List playbackSpeeds; + /// Defines the system overlays visible on entering fullscreen - final List systemOverlaysOnEnterFullScreen; + final List? systemOverlaysOnEnterFullScreen; /// Defines the set of allowed device orientations on entering fullscreen - final List deviceOrientationsOnEnterFullScreen; + final List? deviceOrientationsOnEnterFullScreen; /// Defines the system overlays visible after exiting fullscreen final List systemOverlaysAfterFullScreen; @@ -311,11 +578,21 @@ class ChewieController extends ChangeNotifier { final List deviceOrientationsAfterFullScreen; /// Defines a custom RoutePageBuilder for the fullscreen - final ChewieRoutePageBuilder routePageBuilder; + final ChewieRoutePageBuilder? routePageBuilder; + + /// Defines a delay in milliseconds between entering buffering state and displaying the loading spinner. Set null (default) to disable it. + final Duration? progressIndicatorDelay; + + /// Adds additional padding to the controls' [SafeArea] as desired. + /// Defaults to [EdgeInsets.zero]. + final EdgeInsets controlsSafeAreaMinimum; + + /// Defines if the player should pause when the background is tapped + final bool pauseOnBackgroundTap; static ChewieController of(BuildContext context) { final chewieControllerProvider = - context.dependOnInheritedWidgetOfExactType<_ChewieControllerProvider>(); + context.dependOnInheritedWidgetOfExactType()!; return chewieControllerProvider.controller; } @@ -326,11 +603,11 @@ class ChewieController extends ChangeNotifier { bool get isPlaying => videoPlayerController.value.isPlaying; - Future _initialize() async { + Future _initialize() async { await videoPlayerController.setLooping(looping); if ((autoInitialize || autoPlay) && - !videoPlayerController.value.initialized) { + !videoPlayerController.value.isInitialized) { await videoPlayerController.initialize(); } @@ -343,7 +620,7 @@ class ChewieController extends ChangeNotifier { } if (startAt != null) { - await videoPlayerController.seekTo(startAt); + await videoPlayerController.seekTo(startAt!); } if (fullScreenByDefault) { @@ -351,7 +628,7 @@ class ChewieController extends ChangeNotifier { } } - void _fullScreenListener() async { + Future _fullScreenListener() async { if (videoPlayerController.value.isPlaying && !_isFullScreen) { enterFullScreen(); videoPlayerController.removeListener(_fullScreenListener); @@ -381,6 +658,7 @@ class ChewieController extends ChangeNotifier { await videoPlayerController.play(); } + // ignore: avoid_positional_boolean_parameters Future setLooping(bool looping) async { await videoPlayerController.setLooping(looping); } @@ -396,20 +674,22 @@ class ChewieController extends ChangeNotifier { Future setVolume(double volume) async { await videoPlayerController.setVolume(volume); } + + void setSubtitle(List newSubtitle) { + subtitle = Subtitles(newSubtitle); + } } -class _ChewieControllerProvider extends InheritedWidget { - const _ChewieControllerProvider({ - Key key, - @required this.controller, - @required Widget child, - }) : assert(controller != null), - assert(child != null), - super(key: key, child: child); +class ChewieControllerProvider extends InheritedWidget { + const ChewieControllerProvider({ + super.key, + required this.controller, + required super.child, + }); final ChewieController controller; @override - bool updateShouldNotify(_ChewieControllerProvider old) => - controller != old.controller; + bool updateShouldNotify(ChewieControllerProvider oldWidget) => + controller != oldWidget.controller; } diff --git a/lib/src/chewie_progress_colors.dart b/lib/src/chewie_progress_colors.dart index 9f73717c4..75e48c1cf 100644 --- a/lib/src/chewie_progress_colors.dart +++ b/lib/src/chewie_progress_colors.dart @@ -2,10 +2,10 @@ import 'package:flutter/rendering.dart'; class ChewieProgressColors { ChewieProgressColors({ - Color playedColor: const Color.fromRGBO(255, 0, 0, 0.7), - Color bufferedColor: const Color.fromRGBO(30, 30, 200, 0.2), - Color handleColor: const Color.fromRGBO(200, 200, 200, 1.0), - Color backgroundColor: const Color.fromRGBO(200, 200, 200, 0.5), + Color playedColor = const Color.fromRGBO(255, 0, 0, 0.7), + Color bufferedColor = const Color.fromRGBO(30, 30, 200, 0.2), + Color handleColor = const Color.fromRGBO(200, 200, 200, 1.0), + Color backgroundColor = const Color.fromRGBO(200, 200, 200, 0.5), }) : playedPaint = Paint()..color = playedColor, bufferedPaint = Paint()..color = bufferedColor, handlePaint = Paint()..color = handleColor, diff --git a/lib/src/cupertino/cupertino_controls.dart b/lib/src/cupertino/cupertino_controls.dart new file mode 100644 index 000000000..7e1a7330c --- /dev/null +++ b/lib/src/cupertino/cupertino_controls.dart @@ -0,0 +1,872 @@ +import 'dart:async'; +import 'dart:math' as math; +import 'dart:ui' as ui; + +import 'package:chewie/src/animated_play_pause.dart'; +import 'package:chewie/src/center_play_button.dart'; +import 'package:chewie/src/chewie_player.dart'; +import 'package:chewie/src/chewie_progress_colors.dart'; +import 'package:chewie/src/cupertino/cupertino_progress_bar.dart'; +import 'package:chewie/src/cupertino/widgets/cupertino_options_dialog.dart'; +import 'package:chewie/src/helpers/utils.dart'; +import 'package:chewie/src/models/option_item.dart'; +import 'package:chewie/src/models/subtitle_model.dart'; +import 'package:chewie/src/notifiers/index.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:video_player/video_player.dart'; + +class CupertinoControls extends StatefulWidget { + const CupertinoControls({ + required this.backgroundColor, + required this.iconColor, + this.showPlayButton = true, + super.key, + }); + + final Color backgroundColor; + final Color iconColor; + final bool showPlayButton; + + @override + State createState() { + return _CupertinoControlsState(); + } +} + +class _CupertinoControlsState extends State + with SingleTickerProviderStateMixin { + late PlayerNotifier notifier; + late VideoPlayerValue _latestValue; + double? _latestVolume; + Timer? _hideTimer; + final marginSize = 5.0; + Timer? _expandCollapseTimer; + Timer? _initTimer; + bool _dragging = false; + Duration? _subtitlesPosition; + bool _subtitleOn = false; + Timer? _bufferingDisplayTimer; + bool _displayBufferingIndicator = false; + double selectedSpeed = 1.0; + late VideoPlayerController controller; + + // We know that _chewieController is set in didChangeDependencies + ChewieController get chewieController => _chewieController!; + ChewieController? _chewieController; + + @override + void initState() { + super.initState(); + notifier = Provider.of(context, listen: false); + } + + @override + Widget build(BuildContext context) { + if (_latestValue.hasError) { + return chewieController.errorBuilder != null + ? chewieController.errorBuilder!( + context, + chewieController.videoPlayerController.value.errorDescription!, + ) + : const Center( + child: Icon( + CupertinoIcons.exclamationmark_circle, + color: Colors.white, + size: 42, + ), + ); + } + + final backgroundColor = widget.backgroundColor; + final iconColor = widget.iconColor; + final orientation = MediaQuery.of(context).orientation; + final barHeight = orientation == Orientation.portrait ? 30.0 : 47.0; + final buttonPadding = orientation == Orientation.portrait ? 16.0 : 24.0; + + return MouseRegion( + onHover: (_) => _cancelAndRestartTimer(), + child: GestureDetector( + onTap: () => _cancelAndRestartTimer(), + child: AbsorbPointer( + absorbing: notifier.hideStuff, + child: Stack( + children: [ + if (_displayBufferingIndicator) + _chewieController?.bufferingBuilder?.call(context) ?? + const Center( + child: CircularProgressIndicator(), + ) + else + _buildHitArea(), + Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildTopBar( + backgroundColor, + iconColor, + barHeight, + buttonPadding, + ), + const Spacer(), + if (_subtitleOn) + Transform.translate( + offset: Offset( + 0.0, + notifier.hideStuff ? barHeight * 0.8 : 0.0, + ), + child: _buildSubtitles(chewieController.subtitle!), + ), + _buildBottomBar(backgroundColor, iconColor, barHeight), + ], + ), + ], + ), + ), + ), + ); + } + + @override + void dispose() { + _dispose(); + super.dispose(); + } + + void _dispose() { + controller.removeListener(_updateState); + _hideTimer?.cancel(); + _expandCollapseTimer?.cancel(); + _initTimer?.cancel(); + } + + @override + void didChangeDependencies() { + final oldController = _chewieController; + _chewieController = ChewieController.of(context); + controller = chewieController.videoPlayerController; + + if (oldController != chewieController) { + _dispose(); + _initialize(); + } + + super.didChangeDependencies(); + } + + GestureDetector _buildOptionsButton( + Color iconColor, + double barHeight, + ) { + final options = []; + + if (chewieController.additionalOptions != null && + chewieController.additionalOptions!(context).isNotEmpty) { + options.addAll(chewieController.additionalOptions!(context)); + } + + return GestureDetector( + onTap: () async { + _hideTimer?.cancel(); + + if (chewieController.optionsBuilder != null) { + await chewieController.optionsBuilder!(context, options); + } else { + await showCupertinoModalPopup( + context: context, + semanticsDismissible: true, + useRootNavigator: chewieController.useRootNavigator, + builder: (context) => CupertinoOptionsDialog( + options: options, + cancelButtonText: + chewieController.optionsTranslation?.cancelButtonText, + ), + ); + if (_latestValue.isPlaying) { + _startHideTimer(); + } + } + }, + child: Container( + height: barHeight, + color: Colors.transparent, + padding: const EdgeInsets.only(left: 4.0, right: 8.0), + margin: const EdgeInsets.only(right: 6.0), + child: Icon( + Icons.more_vert, + color: iconColor, + size: 18, + ), + ), + ); + } + + Widget _buildSubtitles(Subtitles subtitles) { + if (!_subtitleOn) { + return const SizedBox(); + } + if (_subtitlesPosition == null) { + return const SizedBox(); + } + final currentSubtitle = subtitles.getByPosition(_subtitlesPosition!); + if (currentSubtitle.isEmpty) { + return const SizedBox(); + } + + if (chewieController.subtitleBuilder != null) { + return chewieController.subtitleBuilder!( + context, + currentSubtitle.first!.text, + ); + } + + return Padding( + padding: EdgeInsets.only(left: marginSize, right: marginSize), + child: Container( + padding: const EdgeInsets.all(5), + decoration: BoxDecoration( + color: const Color(0x96000000), + borderRadius: BorderRadius.circular(10.0), + ), + child: Text( + currentSubtitle.first!.text.toString(), + style: const TextStyle( + fontSize: 18, + ), + textAlign: TextAlign.center, + ), + ), + ); + } + + Widget _buildBottomBar( + Color backgroundColor, + Color iconColor, + double barHeight, + ) { + return SafeArea( + bottom: chewieController.isFullScreen, + minimum: chewieController.controlsSafeAreaMinimum, + child: AnimatedOpacity( + opacity: notifier.hideStuff ? 0.0 : 1.0, + duration: const Duration(milliseconds: 300), + child: Container( + color: Colors.transparent, + alignment: Alignment.bottomCenter, + margin: EdgeInsets.all(marginSize), + child: ClipRRect( + borderRadius: BorderRadius.circular(10.0), + child: BackdropFilter( + filter: ui.ImageFilter.blur( + sigmaX: 10.0, + sigmaY: 10.0, + ), + child: Container( + height: barHeight, + color: backgroundColor, + child: chewieController.isLive + ? Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildPlayPause(controller, iconColor, barHeight), + _buildLive(iconColor), + ], + ) + : Row( + children: [ + _buildSkipBack(iconColor, barHeight), + _buildPlayPause(controller, iconColor, barHeight), + _buildSkipForward(iconColor, barHeight), + _buildPosition(iconColor), + _buildProgressBar(), + _buildRemaining(iconColor), + _buildSubtitleToggle(iconColor, barHeight), + if (chewieController.allowPlaybackSpeedChanging) + _buildSpeedButton(controller, iconColor, barHeight), + if (chewieController.additionalOptions != null && + chewieController + .additionalOptions!(context).isNotEmpty) + _buildOptionsButton(iconColor, barHeight), + ], + ), + ), + ), + ), + ), + ), + ); + } + + Widget _buildLive(Color iconColor) { + return Padding( + padding: const EdgeInsets.only(right: 12.0), + child: Text( + 'LIVE', + style: TextStyle(color: iconColor, fontSize: 12.0), + ), + ); + } + + GestureDetector _buildExpandButton( + Color backgroundColor, + Color iconColor, + double barHeight, + double buttonPadding, + ) { + return GestureDetector( + onTap: _onExpandCollapse, + child: AnimatedOpacity( + opacity: notifier.hideStuff ? 0.0 : 1.0, + duration: const Duration(milliseconds: 300), + child: ClipRRect( + borderRadius: BorderRadius.circular(10.0), + child: BackdropFilter( + filter: ui.ImageFilter.blur(sigmaX: 10.0), + child: Container( + height: barHeight, + padding: EdgeInsets.only( + left: buttonPadding, + right: buttonPadding, + ), + color: backgroundColor, + child: Center( + child: Icon( + chewieController.isFullScreen + ? CupertinoIcons.arrow_down_right_arrow_up_left + : CupertinoIcons.arrow_up_left_arrow_down_right, + color: iconColor, + size: 16, + ), + ), + ), + ), + ), + ), + ); + } + + Widget _buildHitArea() { + final bool isFinished = (_latestValue.position >= _latestValue.duration) && + _latestValue.duration.inSeconds > 0; + final bool showPlayButton = + widget.showPlayButton && !_latestValue.isPlaying && !_dragging; + + return GestureDetector( + onTap: _latestValue.isPlaying + ? _chewieController?.pauseOnBackgroundTap ?? false + ? () { + _playPause(); + + setState(() { + notifier.hideStuff = true; + }); + } + : _cancelAndRestartTimer + : () { + _hideTimer?.cancel(); + + setState(() { + notifier.hideStuff = false; + }); + }, + child: CenterPlayButton( + backgroundColor: widget.backgroundColor, + iconColor: widget.iconColor, + isFinished: isFinished, + isPlaying: controller.value.isPlaying, + show: showPlayButton, + onPressed: _playPause, + ), + ); + } + + GestureDetector _buildMuteButton( + VideoPlayerController controller, + Color backgroundColor, + Color iconColor, + double barHeight, + double buttonPadding, + ) { + return GestureDetector( + onTap: () { + _cancelAndRestartTimer(); + + if (_latestValue.volume == 0) { + controller.setVolume(_latestVolume ?? 0.5); + } else { + _latestVolume = controller.value.volume; + controller.setVolume(0.0); + } + }, + child: AnimatedOpacity( + opacity: notifier.hideStuff ? 0.0 : 1.0, + duration: const Duration(milliseconds: 300), + child: ClipRRect( + borderRadius: BorderRadius.circular(10.0), + child: BackdropFilter( + filter: ui.ImageFilter.blur(sigmaX: 10.0), + child: ColoredBox( + color: backgroundColor, + child: Container( + height: barHeight, + padding: EdgeInsets.only( + left: buttonPadding, + right: buttonPadding, + ), + child: Icon( + _latestValue.volume > 0 ? Icons.volume_up : Icons.volume_off, + color: iconColor, + size: 16, + ), + ), + ), + ), + ), + ), + ); + } + + GestureDetector _buildPlayPause( + VideoPlayerController controller, + Color iconColor, + double barHeight, + ) { + return GestureDetector( + onTap: _playPause, + child: Container( + height: barHeight, + color: Colors.transparent, + padding: const EdgeInsets.only( + left: 6.0, + right: 6.0, + ), + child: AnimatedPlayPause( + color: widget.iconColor, + playing: controller.value.isPlaying, + ), + ), + ); + } + + Widget _buildPosition(Color iconColor) { + final position = _latestValue.position; + + return Padding( + padding: const EdgeInsets.only(right: 12.0), + child: Text( + formatDuration(position), + style: TextStyle( + color: iconColor, + fontSize: 12.0, + ), + ), + ); + } + + Widget _buildRemaining(Color iconColor) { + final position = _latestValue.duration - _latestValue.position; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Text( + '-${formatDuration(position)}', + style: TextStyle(color: iconColor, fontSize: 12.0), + ), + ); + } + + Widget _buildSubtitleToggle(Color iconColor, double barHeight) { + //if don't have subtitle hiden button + if (chewieController.subtitle?.isEmpty ?? true) { + return const SizedBox(); + } + return GestureDetector( + onTap: _subtitleToggle, + child: Container( + height: barHeight, + color: Colors.transparent, + margin: const EdgeInsets.only(right: 10.0), + padding: const EdgeInsets.only( + left: 6.0, + right: 6.0, + ), + child: Icon( + Icons.subtitles, + color: _subtitleOn ? iconColor : Colors.grey[700], + size: 16.0, + ), + ), + ); + } + + void _subtitleToggle() { + setState(() { + _subtitleOn = !_subtitleOn; + }); + } + + GestureDetector _buildSkipBack(Color iconColor, double barHeight) { + return GestureDetector( + onTap: _skipBack, + child: Container( + height: barHeight, + color: Colors.transparent, + margin: const EdgeInsets.only(left: 10.0), + padding: const EdgeInsets.only( + left: 6.0, + right: 6.0, + ), + child: Icon( + CupertinoIcons.gobackward_15, + color: iconColor, + size: 18.0, + ), + ), + ); + } + + GestureDetector _buildSkipForward(Color iconColor, double barHeight) { + return GestureDetector( + onTap: _skipForward, + child: Container( + height: barHeight, + color: Colors.transparent, + padding: const EdgeInsets.only( + left: 6.0, + right: 8.0, + ), + margin: const EdgeInsets.only( + right: 8.0, + ), + child: Icon( + CupertinoIcons.goforward_15, + color: iconColor, + size: 18.0, + ), + ), + ); + } + + GestureDetector _buildSpeedButton( + VideoPlayerController controller, + Color iconColor, + double barHeight, + ) { + return GestureDetector( + onTap: () async { + _hideTimer?.cancel(); + + final chosenSpeed = await showCupertinoModalPopup( + context: context, + semanticsDismissible: true, + useRootNavigator: chewieController.useRootNavigator, + builder: (context) => _PlaybackSpeedDialog( + speeds: chewieController.playbackSpeeds, + selected: _latestValue.playbackSpeed, + ), + ); + + if (chosenSpeed != null) { + controller.setPlaybackSpeed(chosenSpeed); + + selectedSpeed = chosenSpeed; + } + + if (_latestValue.isPlaying) { + _startHideTimer(); + } + }, + child: Container( + height: barHeight, + color: Colors.transparent, + padding: const EdgeInsets.only( + left: 6.0, + right: 8.0, + ), + margin: const EdgeInsets.only( + right: 8.0, + ), + child: Transform( + alignment: Alignment.center, + transform: Matrix4.skewY(0.0) + ..rotateX(math.pi) + ..rotateZ(math.pi * 0.8), + child: Icon( + Icons.speed, + color: iconColor, + size: 18.0, + ), + ), + ), + ); + } + + Widget _buildTopBar( + Color backgroundColor, + Color iconColor, + double barHeight, + double buttonPadding, + ) { + return Container( + height: barHeight, + margin: EdgeInsets.only( + top: marginSize, + right: marginSize, + left: marginSize, + ), + child: Row( + children: [ + if (chewieController.allowFullScreen) + _buildExpandButton( + backgroundColor, + iconColor, + barHeight, + buttonPadding, + ), + const Spacer(), + if (chewieController.allowMuting) + _buildMuteButton( + controller, + backgroundColor, + iconColor, + barHeight, + buttonPadding, + ), + ], + ), + ); + } + + void _cancelAndRestartTimer() { + _hideTimer?.cancel(); + + setState(() { + notifier.hideStuff = false; + + _startHideTimer(); + }); + } + + Future _initialize() async { + _subtitleOn = chewieController.showSubtitles && + (chewieController.subtitle?.isNotEmpty ?? false); + controller.addListener(_updateState); + + _updateState(); + + if (controller.value.isPlaying || chewieController.autoPlay) { + _startHideTimer(); + } + + if (chewieController.showControlsOnInitialize) { + _initTimer = Timer(const Duration(milliseconds: 200), () { + setState(() { + notifier.hideStuff = false; + }); + }); + } + } + + void _onExpandCollapse() { + setState(() { + notifier.hideStuff = true; + + chewieController.toggleFullScreen(); + _expandCollapseTimer = Timer(const Duration(milliseconds: 300), () { + setState(() { + _cancelAndRestartTimer(); + }); + }); + }); + } + + Widget _buildProgressBar() { + return Expanded( + child: Padding( + padding: const EdgeInsets.only(right: 12.0), + child: CupertinoVideoProgressBar( + controller, + onDragStart: () { + setState(() { + _dragging = true; + }); + + _hideTimer?.cancel(); + }, + onDragUpdate: () { + _hideTimer?.cancel(); + }, + onDragEnd: () { + setState(() { + _dragging = false; + }); + + _startHideTimer(); + }, + colors: chewieController.cupertinoProgressColors ?? + ChewieProgressColors( + playedColor: const Color.fromARGB( + 120, + 255, + 255, + 255, + ), + handleColor: const Color.fromARGB( + 255, + 255, + 255, + 255, + ), + bufferedColor: const Color.fromARGB( + 60, + 255, + 255, + 255, + ), + backgroundColor: const Color.fromARGB( + 20, + 255, + 255, + 255, + ), + ), + draggableProgressBar: chewieController.draggableProgressBar, + ), + ), + ); + } + + void _playPause() { + final isFinished = _latestValue.position >= _latestValue.duration && + _latestValue.duration.inSeconds > 0; + + setState(() { + if (controller.value.isPlaying) { + notifier.hideStuff = false; + _hideTimer?.cancel(); + controller.pause(); + } else { + _cancelAndRestartTimer(); + + if (!controller.value.isInitialized) { + controller.initialize().then((_) { + controller.play(); + }); + } else { + if (isFinished) { + controller.seekTo(Duration.zero); + } + controller.play(); + } + } + }); + } + + Future _skipBack() async { + _cancelAndRestartTimer(); + final beginning = Duration.zero.inMilliseconds; + final skip = + (_latestValue.position - const Duration(seconds: 15)).inMilliseconds; + await controller.seekTo(Duration(milliseconds: math.max(skip, beginning))); + // Restoring the video speed to selected speed + // A delay of 1 second is added to ensure a smooth transition of speed after reversing the video as reversing is an asynchronous function + Future.delayed(const Duration(milliseconds: 1000), () { + controller.setPlaybackSpeed(selectedSpeed); + }); + } + + Future _skipForward() async { + _cancelAndRestartTimer(); + final end = _latestValue.duration.inMilliseconds; + final skip = + (_latestValue.position + const Duration(seconds: 15)).inMilliseconds; + await controller.seekTo(Duration(milliseconds: math.min(skip, end))); + // Restoring the video speed to selected speed + // A delay of 1 second is added to ensure a smooth transition of speed after forwarding the video as forwaring is an asynchronous function + Future.delayed(const Duration(milliseconds: 1000), () { + controller.setPlaybackSpeed(selectedSpeed); + }); + } + + void _startHideTimer() { + final hideControlsTimer = chewieController.hideControlsTimer.isNegative + ? ChewieController.defaultHideControlsTimer + : chewieController.hideControlsTimer; + _hideTimer = Timer(hideControlsTimer, () { + setState(() { + notifier.hideStuff = true; + }); + }); + } + + void _bufferingTimerTimeout() { + _displayBufferingIndicator = true; + if (mounted) { + setState(() {}); + } + } + + void _updateState() { + if (!mounted) return; + + final bool buffering = getIsBuffering(controller); + + // display the progress bar indicator only after the buffering delay if it has been set + if (chewieController.progressIndicatorDelay != null) { + if (buffering) { + _bufferingDisplayTimer ??= Timer( + chewieController.progressIndicatorDelay!, + _bufferingTimerTimeout, + ); + } else { + _bufferingDisplayTimer?.cancel(); + _bufferingDisplayTimer = null; + _displayBufferingIndicator = false; + } + } else { + _displayBufferingIndicator = buffering; + } + + setState(() { + _latestValue = controller.value; + _subtitlesPosition = controller.value.position; + }); + } +} + +class _PlaybackSpeedDialog extends StatelessWidget { + const _PlaybackSpeedDialog({ + required List speeds, + required double selected, + }) : _speeds = speeds, + _selected = selected; + + final List _speeds; + final double _selected; + + @override + Widget build(BuildContext context) { + final selectedColor = CupertinoTheme.of(context).primaryColor; + + return CupertinoActionSheet( + actions: _speeds + .map( + (e) => CupertinoActionSheetAction( + onPressed: () { + Navigator.of(context).pop(e); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (e == _selected) + Icon(Icons.check, size: 20.0, color: selectedColor), + Text(e.toString()), + ], + ), + ), + ) + .toList(), + ); + } +} diff --git a/lib/src/cupertino/cupertino_progress_bar.dart b/lib/src/cupertino/cupertino_progress_bar.dart new file mode 100644 index 000000000..f24fc0f80 --- /dev/null +++ b/lib/src/cupertino/cupertino_progress_bar.dart @@ -0,0 +1,39 @@ +import 'package:chewie/src/chewie_progress_colors.dart'; +import 'package:chewie/src/progress_bar.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:video_player/video_player.dart'; + +class CupertinoVideoProgressBar extends StatelessWidget { + CupertinoVideoProgressBar( + this.controller, { + ChewieProgressColors? colors, + this.onDragEnd, + this.onDragStart, + this.onDragUpdate, + super.key, + this.draggableProgressBar = true, + }) : colors = colors ?? ChewieProgressColors(); + + final VideoPlayerController controller; + final ChewieProgressColors colors; + final Function()? onDragStart; + final Function()? onDragEnd; + final Function()? onDragUpdate; + final bool draggableProgressBar; + + @override + Widget build(BuildContext context) { + return VideoProgressBar( + controller, + barHeight: 5, + handleHeight: 6, + drawShadow: true, + colors: colors, + onDragEnd: onDragEnd, + onDragStart: onDragStart, + onDragUpdate: onDragUpdate, + draggableProgressBar: draggableProgressBar, + ); + } +} diff --git a/lib/src/cupertino/widgets/cupertino_options_dialog.dart b/lib/src/cupertino/widgets/cupertino_options_dialog.dart new file mode 100644 index 000000000..4b923dc6e --- /dev/null +++ b/lib/src/cupertino/widgets/cupertino_options_dialog.dart @@ -0,0 +1,40 @@ +import 'package:chewie/src/models/option_item.dart'; +import 'package:flutter/cupertino.dart'; + +class CupertinoOptionsDialog extends StatefulWidget { + const CupertinoOptionsDialog({ + super.key, + required this.options, + this.cancelButtonText, + }); + + final List options; + final String? cancelButtonText; + + @override + // ignore: library_private_types_in_public_api + _CupertinoOptionsDialogState createState() => _CupertinoOptionsDialogState(); +} + +class _CupertinoOptionsDialogState extends State { + @override + Widget build(BuildContext context) { + return SafeArea( + child: CupertinoActionSheet( + actions: widget.options + .map( + (option) => CupertinoActionSheetAction( + onPressed: () => option.onTap(context), + child: Text(option.title), + ), + ) + .toList(), + cancelButton: CupertinoActionSheetAction( + onPressed: () => Navigator.pop(context), + isDestructiveAction: true, + child: Text(widget.cancelButtonText ?? 'Cancel'), + ), + ), + ); + } +} diff --git a/lib/src/cupertino_controls.dart b/lib/src/cupertino_controls.dart deleted file mode 100644 index b916c52de..000000000 --- a/lib/src/cupertino_controls.dart +++ /dev/null @@ -1,547 +0,0 @@ -import 'dart:async'; -import 'dart:math' as math; -import 'dart:ui' as ui; - -import 'package:chewie/src/chewie_player.dart'; -import 'package:chewie/src/chewie_progress_colors.dart'; -import 'package:chewie/src/cupertino_progress_bar.dart'; -import 'package:chewie/src/utils.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:video_player/video_player.dart'; - -class CupertinoControls extends StatefulWidget { - const CupertinoControls({ - @required this.backgroundColor, - @required this.iconColor, - }); - - final Color backgroundColor; - final Color iconColor; - - @override - State createState() { - return _CupertinoControlsState(); - } -} - -class _CupertinoControlsState extends State { - VideoPlayerValue _latestValue; - double _latestVolume; - bool _hideStuff = true; - Timer _hideTimer; - final marginSize = 5.0; - Timer _expandCollapseTimer; - Timer _initTimer; - - VideoPlayerController controller; - ChewieController chewieController; - - @override - Widget build(BuildContext context) { - chewieController = ChewieController.of(context); - - if (_latestValue.hasError) { - return chewieController.errorBuilder != null - ? chewieController.errorBuilder( - context, - chewieController.videoPlayerController.value.errorDescription, - ) - : Center( - child: Icon( - CupertinoIcons.exclamationmark_circle, - color: Colors.white, - size: 42, - ), - ); - } - - final backgroundColor = widget.backgroundColor; - final iconColor = widget.iconColor; - chewieController = ChewieController.of(context); - controller = chewieController.videoPlayerController; - final orientation = MediaQuery.of(context).orientation; - final barHeight = orientation == Orientation.portrait ? 30.0 : 47.0; - final buttonPadding = orientation == Orientation.portrait ? 16.0 : 24.0; - - return MouseRegion( - onHover: (_) { - _cancelAndRestartTimer(); - }, - child: GestureDetector( - onTap: () { - _cancelAndRestartTimer(); - }, - child: AbsorbPointer( - absorbing: _hideStuff, - child: Column( - children: [ - _buildTopBar( - backgroundColor, iconColor, barHeight, buttonPadding), - _buildHitArea(), - _buildBottomBar(backgroundColor, iconColor, barHeight), - ], - ), - ), - ), - ); - } - - @override - void dispose() { - _dispose(); - super.dispose(); - } - - void _dispose() { - controller.removeListener(_updateState); - _hideTimer?.cancel(); - _expandCollapseTimer?.cancel(); - _initTimer?.cancel(); - } - - @override - void didChangeDependencies() { - final _oldController = chewieController; - chewieController = ChewieController.of(context); - controller = chewieController.videoPlayerController; - - if (_oldController != chewieController) { - _dispose(); - _initialize(); - } - - super.didChangeDependencies(); - } - - AnimatedOpacity _buildBottomBar( - Color backgroundColor, - Color iconColor, - double barHeight, - ) { - return AnimatedOpacity( - opacity: _hideStuff ? 0.0 : 1.0, - duration: Duration(milliseconds: 300), - child: Container( - color: Colors.transparent, - alignment: Alignment.bottomCenter, - margin: EdgeInsets.all(marginSize), - child: ClipRRect( - borderRadius: BorderRadius.circular(10.0), - child: BackdropFilter( - filter: ui.ImageFilter.blur( - sigmaX: 10.0, - sigmaY: 10.0, - ), - child: Container( - height: barHeight, - color: backgroundColor, - child: chewieController.isLive - ? Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _buildPlayPause(controller, iconColor, barHeight), - _buildLive(iconColor), - ], - ) - : Row( - children: [ - _buildSkipBack(iconColor, barHeight), - _buildPlayPause(controller, iconColor, barHeight), - _buildSkipForward(iconColor, barHeight), - _buildPosition(iconColor), - _buildProgressBar(), - _buildRemaining(iconColor) - ], - ), - ), - ), - ), - ), - ); - } - - Widget _buildLive(Color iconColor) { - return Padding( - padding: EdgeInsets.only(right: 12.0), - child: Text( - 'LIVE', - style: TextStyle(color: iconColor, fontSize: 12.0), - ), - ); - } - - GestureDetector _buildExpandButton( - Color backgroundColor, - Color iconColor, - double barHeight, - double buttonPadding, - ) { - return GestureDetector( - onTap: _onExpandCollapse, - child: AnimatedOpacity( - opacity: _hideStuff ? 0.0 : 1.0, - duration: Duration(milliseconds: 300), - child: ClipRRect( - borderRadius: BorderRadius.circular(10.0), - child: BackdropFilter( - filter: ui.ImageFilter.blur(sigmaX: 10.0), - child: Container( - height: barHeight, - padding: EdgeInsets.only( - left: buttonPadding, - right: buttonPadding, - ), - color: backgroundColor, - child: Center( - child: Icon( - chewieController.isFullScreen - ? CupertinoIcons.arrow_down_right_arrow_up_left - : CupertinoIcons.arrow_up_left_arrow_down_right, - color: iconColor, - size: 16, - ), - ), - ), - ), - ), - ), - ); - } - - Expanded _buildHitArea() { - return Expanded( - child: GestureDetector( - onTap: _latestValue != null && _latestValue.isPlaying - ? _cancelAndRestartTimer - : () { - _hideTimer?.cancel(); - - setState(() { - _hideStuff = false; - }); - }, - child: Container( - color: Colors.transparent, - ), - ), - ); - } - - GestureDetector _buildMuteButton( - VideoPlayerController controller, - Color backgroundColor, - Color iconColor, - double barHeight, - double buttonPadding, - ) { - return GestureDetector( - onTap: () { - _cancelAndRestartTimer(); - - if (_latestValue.volume == 0) { - controller.setVolume(_latestVolume ?? 0.5); - } else { - _latestVolume = controller.value.volume; - controller.setVolume(0.0); - } - }, - child: AnimatedOpacity( - opacity: _hideStuff ? 0.0 : 1.0, - duration: Duration(milliseconds: 300), - child: ClipRRect( - borderRadius: BorderRadius.circular(10.0), - child: BackdropFilter( - filter: ui.ImageFilter.blur(sigmaX: 10.0), - child: Container( - color: backgroundColor, - child: Container( - height: barHeight, - padding: EdgeInsets.only( - left: buttonPadding, - right: buttonPadding, - ), - child: Icon( - (_latestValue != null && _latestValue.volume > 0) - ? Icons.volume_up - : Icons.volume_off, - color: iconColor, - size: 16, - ), - ), - ), - ), - ), - ), - ); - } - - GestureDetector _buildPlayPause( - VideoPlayerController controller, - Color iconColor, - double barHeight, - ) { - return GestureDetector( - onTap: _playPause, - child: Container( - height: barHeight, - color: Colors.transparent, - padding: EdgeInsets.only( - left: 6.0, - right: 6.0, - ), - child: Icon( - controller.value.isPlaying ? Icons.pause : Icons.play_arrow, - color: iconColor, - ), - ), - ); - } - - Widget _buildPosition(Color iconColor) { - final position = - _latestValue != null ? _latestValue.position : Duration(seconds: 0); - - return Padding( - padding: EdgeInsets.only(right: 12.0), - child: Text( - formatDuration(position), - style: TextStyle( - color: iconColor, - fontSize: 12.0, - ), - ), - ); - } - - Widget _buildRemaining(Color iconColor) { - final position = _latestValue != null && _latestValue.duration != null - ? _latestValue.duration - _latestValue.position - : Duration(seconds: 0); - - return Padding( - padding: EdgeInsets.only(right: 12.0), - child: Text( - '-${formatDuration(position)}', - style: TextStyle(color: iconColor, fontSize: 12.0), - ), - ); - } - - GestureDetector _buildSkipBack(Color iconColor, double barHeight) { - return GestureDetector( - onTap: _skipBack, - child: Container( - height: barHeight, - color: Colors.transparent, - margin: EdgeInsets.only(left: 10.0), - padding: EdgeInsets.only( - left: 6.0, - right: 6.0, - ), - child: Icon( - CupertinoIcons.gobackward_15, - color: iconColor, - size: 18.0, - ), - ), - ); - } - - GestureDetector _buildSkipForward(Color iconColor, double barHeight) { - return GestureDetector( - onTap: _skipForward, - child: Container( - height: barHeight, - color: Colors.transparent, - padding: EdgeInsets.only( - left: 6.0, - right: 8.0, - ), - margin: EdgeInsets.only( - right: 8.0, - ), - child: Icon( - CupertinoIcons.goforward_15, - color: iconColor, - size: 18.0, - ), - ), - ); - } - - Widget _buildTopBar( - Color backgroundColor, - Color iconColor, - double barHeight, - double buttonPadding, - ) { - return Container( - height: barHeight, - margin: EdgeInsets.only( - top: marginSize, - right: marginSize, - left: marginSize, - ), - child: Row( - children: [ - chewieController.allowFullScreen - ? _buildExpandButton( - backgroundColor, iconColor, barHeight, buttonPadding) - : Container(), - Expanded(child: Container()), - chewieController.allowMuting - ? _buildMuteButton(controller, backgroundColor, iconColor, - barHeight, buttonPadding) - : Container(), - ], - ), - ); - } - - void _cancelAndRestartTimer() { - _hideTimer?.cancel(); - - setState(() { - _hideStuff = false; - - _startHideTimer(); - }); - } - - Future _initialize() async { - controller.addListener(_updateState); - - _updateState(); - - if ((controller.value != null && controller.value.isPlaying) || - chewieController.autoPlay) { - _startHideTimer(); - } - - if (chewieController.showControlsOnInitialize) { - _initTimer = Timer(Duration(milliseconds: 200), () { - setState(() { - _hideStuff = false; - }); - }); - } - } - - void _onExpandCollapse() { - setState(() { - _hideStuff = true; - - chewieController.toggleFullScreen(); - _expandCollapseTimer = Timer(Duration(milliseconds: 300), () { - setState(() { - _cancelAndRestartTimer(); - }); - }); - }); - } - - Widget _buildProgressBar() { - return Expanded( - child: Padding( - padding: EdgeInsets.only(right: 12.0), - child: CupertinoVideoProgressBar( - controller, - onDragStart: () { - _hideTimer?.cancel(); - }, - onDragEnd: () { - _startHideTimer(); - }, - colors: chewieController.cupertinoProgressColors ?? - ChewieProgressColors( - playedColor: Color.fromARGB( - 120, - 255, - 255, - 255, - ), - handleColor: Color.fromARGB( - 255, - 255, - 255, - 255, - ), - bufferedColor: Color.fromARGB( - 60, - 255, - 255, - 255, - ), - backgroundColor: Color.fromARGB( - 20, - 255, - 255, - 255, - ), - ), - ), - ), - ); - } - - void _playPause() { - bool isFinished; - if (_latestValue.duration != null) { - isFinished = _latestValue.position >= _latestValue.duration; - } else { - isFinished = false; - } - - setState(() { - if (controller.value.isPlaying) { - _hideStuff = false; - _hideTimer?.cancel(); - controller.pause(); - } else { - _cancelAndRestartTimer(); - - if (!controller.value.initialized) { - controller.initialize().then((_) { - controller.play(); - }); - } else { - if (isFinished) { - controller.seekTo(Duration(seconds: 0)); - } - controller.play(); - } - } - }); - } - - void _skipBack() { - _cancelAndRestartTimer(); - final beginning = Duration(seconds: 0).inMilliseconds; - final skip = (_latestValue.position - Duration(seconds: 15)).inMilliseconds; - controller.seekTo(Duration(milliseconds: math.max(skip, beginning))); - } - - void _skipForward() { - _cancelAndRestartTimer(); - final end = _latestValue.duration.inMilliseconds; - final skip = (_latestValue.position + Duration(seconds: 15)).inMilliseconds; - controller.seekTo(Duration(milliseconds: math.min(skip, end))); - } - - void _startHideTimer() { - _hideTimer = Timer(const Duration(seconds: 3), () { - setState(() { - _hideStuff = true; - }); - }); - } - - void _updateState() { - if (!this.mounted) return; - setState(() { - _latestValue = controller.value; - }); - } -} diff --git a/lib/src/cupertino_progress_bar.dart b/lib/src/cupertino_progress_bar.dart deleted file mode 100644 index 60256df54..000000000 --- a/lib/src/cupertino_progress_bar.dart +++ /dev/null @@ -1,169 +0,0 @@ -import 'package:chewie/src/chewie_progress_colors.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; -import 'package:video_player/video_player.dart'; - -class CupertinoVideoProgressBar extends StatefulWidget { - CupertinoVideoProgressBar( - this.controller, { - ChewieProgressColors colors, - this.onDragEnd, - this.onDragStart, - this.onDragUpdate, - }) : colors = colors ?? ChewieProgressColors(); - - final VideoPlayerController controller; - final ChewieProgressColors colors; - final Function() onDragStart; - final Function() onDragEnd; - final Function() onDragUpdate; - - @override - _VideoProgressBarState createState() { - return _VideoProgressBarState(); - } -} - -class _VideoProgressBarState extends State { - bool _controllerWasPlaying = false; - - VideoPlayerController get controller => widget.controller; - - @override - Widget build(BuildContext context) { - void seekToRelativePosition(Offset globalPosition) { - final box = context.findRenderObject() as RenderBox; - final Offset tapPos = box.globalToLocal(globalPosition); - final double relative = tapPos.dx / box.size.width; - final Duration position = controller.value.duration * relative; - controller.seekTo(position); - } - - return GestureDetector( - child: Center( - child: Container( - height: MediaQuery.of(context).size.height, - width: MediaQuery.of(context).size.width, - color: Colors.transparent, - child: CustomPaint( - painter: _ProgressBarPainter( - controller.value, - widget.colors, - ), - ), - ), - ), - onHorizontalDragStart: (DragStartDetails details) { - if (!controller.value.initialized) { - return; - } - _controllerWasPlaying = controller.value.isPlaying; - if (_controllerWasPlaying) { - controller.pause(); - } - - if (widget.onDragStart != null) { - widget.onDragStart(); - } - }, - onHorizontalDragUpdate: (DragUpdateDetails details) { - if (!controller.value.initialized) { - return; - } - seekToRelativePosition(details.globalPosition); - - if (widget.onDragUpdate != null) { - widget.onDragUpdate(); - } - }, - onHorizontalDragEnd: (DragEndDetails details) { - if (_controllerWasPlaying) { - controller.play(); - } - - if (widget.onDragEnd != null) { - widget.onDragEnd(); - } - }, - onTapDown: (TapDownDetails details) { - if (!controller.value.initialized) { - return; - } - seekToRelativePosition(details.globalPosition); - }, - ); - } -} - -class _ProgressBarPainter extends CustomPainter { - _ProgressBarPainter(this.value, this.colors); - - VideoPlayerValue value; - ChewieProgressColors colors; - - @override - bool shouldRepaint(CustomPainter painter) { - return true; - } - - @override - void paint(Canvas canvas, Size size) { - final barHeight = 5.0; - final handleHeight = 6.0; - final baseOffset = size.height / 2 - barHeight / 2.0; - - canvas.drawRRect( - RRect.fromRectAndRadius( - Rect.fromPoints( - Offset(0.0, baseOffset), - Offset(size.width, baseOffset + barHeight), - ), - Radius.circular(4.0), - ), - colors.backgroundPaint, - ); - if (!value.initialized) { - return; - } - final double playedPartPercent = - value.position.inMilliseconds / value.duration.inMilliseconds; - final double playedPart = - playedPartPercent > 1 ? size.width : playedPartPercent * size.width; - for (DurationRange range in value.buffered) { - final double start = range.startFraction(value.duration) * size.width; - final double end = range.endFraction(value.duration) * size.width; - canvas.drawRRect( - RRect.fromRectAndRadius( - Rect.fromPoints( - Offset(start, baseOffset), - Offset(end, baseOffset + barHeight), - ), - Radius.circular(4.0), - ), - colors.bufferedPaint, - ); - } - canvas.drawRRect( - RRect.fromRectAndRadius( - Rect.fromPoints( - Offset(0.0, baseOffset), - Offset(playedPart, baseOffset + barHeight), - ), - Radius.circular(4.0), - ), - colors.playedPaint, - ); - - final shadowPath = Path() - ..addOval(Rect.fromCircle( - center: Offset(playedPart, baseOffset + barHeight / 2), - radius: handleHeight)); - - canvas.drawShadow(shadowPath, Colors.black, 0.2, false); - canvas.drawCircle( - Offset(playedPart, baseOffset + barHeight / 2), - handleHeight, - colors.handlePaint, - ); - } -} diff --git a/lib/src/helpers/adaptive_controls.dart b/lib/src/helpers/adaptive_controls.dart new file mode 100644 index 000000000..ef8968d3b --- /dev/null +++ b/lib/src/helpers/adaptive_controls.dart @@ -0,0 +1,28 @@ +import 'package:chewie/chewie.dart'; +import 'package:flutter/material.dart'; + +class AdaptiveControls extends StatelessWidget { + const AdaptiveControls({ + super.key, + }); + + @override + Widget build(BuildContext context) { + switch (Theme.of(context).platform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + return const MaterialControls(); + + case TargetPlatform.macOS: + case TargetPlatform.windows: + case TargetPlatform.linux: + return const MaterialDesktopControls(); + + case TargetPlatform.iOS: + return const CupertinoControls( + backgroundColor: Color.fromRGBO(41, 41, 41, 0.7), + iconColor: Color.fromARGB(255, 200, 200, 200), + ); + } + } +} diff --git a/lib/src/helpers/utils.dart b/lib/src/helpers/utils.dart new file mode 100644 index 000000000..17ae2161a --- /dev/null +++ b/lib/src/helpers/utils.dart @@ -0,0 +1,71 @@ +import 'package:flutter/foundation.dart'; +import 'package:video_player/video_player.dart'; + +String formatDuration(Duration position) { + final ms = position.inMilliseconds; + + int seconds = ms ~/ 1000; + final int hours = seconds ~/ 3600; + seconds = seconds % 3600; + final minutes = seconds ~/ 60; + seconds = seconds % 60; + + final hoursString = hours >= 10 + ? '$hours' + : hours == 0 + ? '00' + : '0$hours'; + + final minutesString = minutes >= 10 + ? '$minutes' + : minutes == 0 + ? '00' + : '0$minutes'; + + final secondsString = seconds >= 10 + ? '$seconds' + : seconds == 0 + ? '00' + : '0$seconds'; + + final formattedTime = + '${hoursString == '00' ? '' : '$hoursString:'}$minutesString:$secondsString'; + + return formattedTime; +} + +/// Gets the current buffering state of the video player. +/// +/// For Android, it will use a workaround due to a [bug](https://github.com/flutter/flutter/issues/165149) +/// affecting the `video_player` plugin, preventing it from getting the +/// actual buffering state. This currently results in the `VideoPlayerController` always buffering, +/// thus breaking UI elements. +/// +/// For this, the actual buffer position is used to determine if the video is +/// buffering or not. See Issue [#912](https://github.com/fluttercommunity/chewie/pull/912) for more details. +bool getIsBuffering(VideoPlayerController controller) { + final VideoPlayerValue value = controller.value; + + if (defaultTargetPlatform == TargetPlatform.android) { + if (value.isBuffering) { + // -> Check if we actually buffer, as android has a bug preventing to + // get the correct buffering state from this single bool. + final int position = value.position.inMilliseconds; + + // Special case, if the video is finished, we don't want to show the + // buffering indicator anymore + if (position >= value.duration.inMilliseconds) { + return false; + } else { + final int buffer = value.buffered.lastOrNull?.end.inMilliseconds ?? -1; + + return position >= buffer; + } + } else { + // -> No buffering + return false; + } + } + + return value.isBuffering; +} diff --git a/lib/src/material/color_compat_extensions.dart b/lib/src/material/color_compat_extensions.dart new file mode 100644 index 000000000..bff4f3071 --- /dev/null +++ b/lib/src/material/color_compat_extensions.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +//ignore_for_file: deprecated_member_use +extension ColorCompatExtensions on Color { + /// Returns a new color that matches this color with the given opacity. + /// + /// This is a compatibility layer that ensures compatibility with Flutter + /// versions below 3.27. In Flutter 3.27 and later, `Color.withOpacity` + /// has been deprecated in favor of `Color.withValues`. + /// + /// This method bridges the gap by providing a consistent way to adjust + /// the opacity of a color across different Flutter versions. + /// + /// **Important:** Once the minimum supported Flutter version is bumped + /// to 3.27 or higher, this method should be removed and replaced with + /// `withValues(alpha: opacity)`. + /// + /// See also: + /// * [Color.withOpacity], which is deprecated in Flutter 3.27 and later. + /// * [Color.withValues], the recommended replacement for `withOpacity`. + Color withOpacityCompat(double opacity) { + // Compatibility layer that uses the legacy withOpacity method, while + // ignoring the deprecation for now (in order to guarantee N-1 minimum + // version compatibility). + // Once it's removed from a future update, we'll have to replace uses of + // this method with withValues(alpha: opacity). + // TODO: Replace this bridge method once the above holds true. + return withOpacity(opacity); + } +} diff --git a/lib/src/material/material_controls.dart b/lib/src/material/material_controls.dart new file mode 100644 index 000000000..3ea67f31d --- /dev/null +++ b/lib/src/material/material_controls.dart @@ -0,0 +1,706 @@ +import 'dart:async'; + +import 'package:chewie/src/center_play_button.dart'; +import 'package:chewie/src/center_seek_button.dart'; +import 'package:chewie/src/chewie_player.dart'; +import 'package:chewie/src/chewie_progress_colors.dart'; +import 'package:chewie/src/helpers/utils.dart'; +import 'package:chewie/src/material/color_compat_extensions.dart'; +import 'package:chewie/src/material/material_progress_bar.dart'; +import 'package:chewie/src/material/widgets/options_dialog.dart'; +import 'package:chewie/src/material/widgets/playback_speed_dialog.dart'; +import 'package:chewie/src/models/option_item.dart'; +import 'package:chewie/src/models/subtitle_model.dart'; +import 'package:chewie/src/notifiers/index.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:video_player/video_player.dart'; + +class MaterialControls extends StatefulWidget { + const MaterialControls({ + this.showPlayButton = true, + super.key, + }); + + final bool showPlayButton; + + @override + State createState() { + return _MaterialControlsState(); + } +} + +class _MaterialControlsState extends State + with SingleTickerProviderStateMixin { + late PlayerNotifier notifier; + late VideoPlayerValue _latestValue; + double? _latestVolume; + Timer? _hideTimer; + Timer? _initTimer; + late var _subtitlesPosition = Duration.zero; + bool _subtitleOn = false; + Timer? _showAfterExpandCollapseTimer; + bool _dragging = false; + bool _displayTapped = false; + Timer? _bufferingDisplayTimer; + bool _displayBufferingIndicator = false; + + final barHeight = 48.0 * 1.5; + final marginSize = 5.0; + + late VideoPlayerController controller; + ChewieController? _chewieController; + + // We know that _chewieController is set in didChangeDependencies + ChewieController get chewieController => _chewieController!; + + @override + void initState() { + super.initState(); + notifier = Provider.of(context, listen: false); + } + + @override + Widget build(BuildContext context) { + if (_latestValue.hasError) { + return chewieController.errorBuilder?.call( + context, + chewieController.videoPlayerController.value.errorDescription!, + ) ?? + const Center( + child: Icon( + Icons.error, + color: Colors.white, + size: 42, + ), + ); + } + + return MouseRegion( + onHover: (_) { + _cancelAndRestartTimer(); + }, + child: GestureDetector( + onTap: () => _cancelAndRestartTimer(), + child: AbsorbPointer( + absorbing: notifier.hideStuff, + child: Stack( + children: [ + if (_displayBufferingIndicator) + _chewieController?.bufferingBuilder?.call(context) ?? + const Center( + child: CircularProgressIndicator(), + ) + else + _buildHitArea(), + _buildActionBar(), + Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (_subtitleOn) + Transform.translate( + offset: Offset( + 0.0, + notifier.hideStuff ? barHeight * 0.8 : 0.0, + ), + child: + _buildSubtitles(context, chewieController.subtitle!), + ), + _buildBottomBar(context), + ], + ), + ], + ), + ), + ), + ); + } + + @override + void dispose() { + _dispose(); + super.dispose(); + } + + void _dispose() { + controller.removeListener(_updateState); + _hideTimer?.cancel(); + _initTimer?.cancel(); + _showAfterExpandCollapseTimer?.cancel(); + } + + @override + void didChangeDependencies() { + final oldController = _chewieController; + _chewieController = ChewieController.of(context); + controller = chewieController.videoPlayerController; + + if (oldController != chewieController) { + _dispose(); + _initialize(); + } + + super.didChangeDependencies(); + } + + Widget _buildActionBar() { + return Positioned( + top: 0, + right: 0, + child: SafeArea( + child: AnimatedOpacity( + opacity: notifier.hideStuff ? 0.0 : 1.0, + duration: const Duration(milliseconds: 250), + child: Row( + children: [ + _buildSubtitleToggle(), + if (chewieController.showOptions) _buildOptionsButton(), + ], + ), + ), + ), + ); + } + + List _buildOptions(BuildContext context) { + final options = [ + OptionItem( + onTap: (context) async { + Navigator.pop(context); + _onSpeedButtonTap(); + }, + iconData: Icons.speed, + title: chewieController.optionsTranslation?.playbackSpeedButtonText ?? + 'Playback speed', + ) + ]; + + if (chewieController.additionalOptions != null && + chewieController.additionalOptions!(context).isNotEmpty) { + options.addAll(chewieController.additionalOptions!(context)); + } + return options; + } + + Widget _buildOptionsButton() { + return AnimatedOpacity( + opacity: notifier.hideStuff ? 0.0 : 1.0, + duration: const Duration(milliseconds: 250), + child: IconButton( + onPressed: () async { + _hideTimer?.cancel(); + + if (chewieController.optionsBuilder != null) { + await chewieController.optionsBuilder!( + context, _buildOptions(context)); + } else { + await showModalBottomSheet( + context: context, + isScrollControlled: true, + useRootNavigator: chewieController.useRootNavigator, + builder: (context) => OptionsDialog( + options: _buildOptions(context), + cancelButtonText: + chewieController.optionsTranslation?.cancelButtonText, + ), + ); + } + + if (_latestValue.isPlaying) { + _startHideTimer(); + } + }, + icon: const Icon( + Icons.more_vert, + color: Colors.white, + ), + ), + ); + } + + Widget _buildSubtitles(BuildContext context, Subtitles subtitles) { + if (!_subtitleOn) { + return const SizedBox(); + } + final currentSubtitle = subtitles.getByPosition(_subtitlesPosition); + if (currentSubtitle.isEmpty) { + return const SizedBox(); + } + + if (chewieController.subtitleBuilder != null) { + return chewieController.subtitleBuilder!( + context, + currentSubtitle.first!.text, + ); + } + + return Padding( + padding: EdgeInsets.all(marginSize), + child: Container( + padding: const EdgeInsets.all(5), + decoration: BoxDecoration( + color: const Color(0x96000000), + borderRadius: BorderRadius.circular(10.0), + ), + child: Text( + currentSubtitle.first!.text.toString(), + style: const TextStyle( + fontSize: 18, + ), + textAlign: TextAlign.center, + ), + ), + ); + } + + AnimatedOpacity _buildBottomBar( + BuildContext context, + ) { + final iconColor = Theme.of(context).textTheme.labelLarge!.color; + + return AnimatedOpacity( + opacity: notifier.hideStuff ? 0.0 : 1.0, + duration: const Duration(milliseconds: 300), + child: Container( + height: barHeight + (chewieController.isFullScreen ? 10.0 : 0), + padding: EdgeInsets.only( + left: 20, + right: 20, + bottom: !chewieController.isFullScreen ? 10.0 : 0, + ), + child: SafeArea( + top: false, + bottom: chewieController.isFullScreen, + minimum: chewieController.controlsSafeAreaMinimum, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (chewieController.isLive) + const Expanded(child: Text('LIVE')) + else + _buildPosition(iconColor), + if (chewieController.allowMuting) + _buildMuteButton(controller), + const Spacer(), + if (chewieController.allowFullScreen) _buildExpandButton(), + ], + ), + ), + SizedBox( + height: chewieController.isFullScreen ? 15.0 : 0, + ), + if (!chewieController.isLive) + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + children: [ + _buildProgressBar(), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } + + GestureDetector _buildMuteButton( + VideoPlayerController controller, + ) { + return GestureDetector( + onTap: () { + _cancelAndRestartTimer(); + + if (_latestValue.volume == 0) { + controller.setVolume(_latestVolume ?? 0.5); + } else { + _latestVolume = controller.value.volume; + controller.setVolume(0.0); + } + }, + child: AnimatedOpacity( + opacity: notifier.hideStuff ? 0.0 : 1.0, + duration: const Duration(milliseconds: 300), + child: ClipRect( + child: Container( + height: barHeight, + padding: const EdgeInsets.only( + left: 6.0, + ), + child: Icon( + _latestValue.volume > 0 ? Icons.volume_up : Icons.volume_off, + color: Colors.white, + ), + ), + ), + ), + ); + } + + GestureDetector _buildExpandButton() { + return GestureDetector( + onTap: _onExpandCollapse, + child: AnimatedOpacity( + opacity: notifier.hideStuff ? 0.0 : 1.0, + duration: const Duration(milliseconds: 300), + child: Container( + height: barHeight + (chewieController.isFullScreen ? 15.0 : 0), + margin: const EdgeInsets.only(right: 12.0), + padding: const EdgeInsets.only( + left: 8.0, + right: 8.0, + ), + child: Center( + child: Icon( + chewieController.isFullScreen + ? Icons.fullscreen_exit + : Icons.fullscreen, + color: Colors.white, + ), + ), + ), + ), + ); + } + + Widget _buildHitArea() { + final bool isFinished = (_latestValue.position >= _latestValue.duration) && + _latestValue.duration.inSeconds > 0; + final bool showPlayButton = + widget.showPlayButton && !_dragging && !notifier.hideStuff; + + return GestureDetector( + onTap: () { + if (_latestValue.isPlaying) { + if (_chewieController?.pauseOnBackgroundTap ?? false) { + _playPause(); + _cancelAndRestartTimer(); + } else { + if (_displayTapped) { + setState(() { + notifier.hideStuff = true; + }); + } else { + _cancelAndRestartTimer(); + } + } + } else { + _playPause(); + + setState(() { + notifier.hideStuff = true; + }); + } + }, + child: Container( + alignment: Alignment.center, + color: Colors + .transparent, // The Gesture Detector doesn't expand to the full size of the container without this; Not sure why! + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (!isFinished && !chewieController.isLive) + CenterSeekButton( + iconData: Icons.replay_10, + backgroundColor: Colors.black54, + iconColor: Colors.white, + show: showPlayButton, + fadeDuration: chewieController.materialSeekButtonFadeDuration, + iconSize: chewieController.materialSeekButtonSize, + onPressed: _seekBackward, + ), + Container( + margin: EdgeInsets.symmetric( + horizontal: marginSize, + ), + child: CenterPlayButton( + backgroundColor: Colors.black54, + iconColor: Colors.white, + isFinished: isFinished, + isPlaying: controller.value.isPlaying, + show: showPlayButton, + onPressed: _playPause, + ), + ), + if (!isFinished && !chewieController.isLive) + CenterSeekButton( + iconData: Icons.forward_10, + backgroundColor: Colors.black54, + iconColor: Colors.white, + show: showPlayButton, + fadeDuration: chewieController.materialSeekButtonFadeDuration, + iconSize: chewieController.materialSeekButtonSize, + onPressed: _seekForward, + ), + ], + ), + ), + ); + } + + Future _onSpeedButtonTap() async { + _hideTimer?.cancel(); + + final chosenSpeed = await showModalBottomSheet( + context: context, + isScrollControlled: true, + useRootNavigator: chewieController.useRootNavigator, + builder: (context) => PlaybackSpeedDialog( + speeds: chewieController.playbackSpeeds, + selected: _latestValue.playbackSpeed, + ), + ); + + if (chosenSpeed != null) { + controller.setPlaybackSpeed(chosenSpeed); + } + + if (_latestValue.isPlaying) { + _startHideTimer(); + } + } + + Widget _buildPosition(Color? iconColor) { + final position = _latestValue.position; + final duration = _latestValue.duration; + + return RichText( + text: TextSpan( + text: '${formatDuration(position)} ', + children: [ + TextSpan( + text: '/ ${formatDuration(duration)}', + style: TextStyle( + fontSize: 14.0, + color: Colors.white.withOpacityCompat(.75), + fontWeight: FontWeight.normal, + ), + ) + ], + style: const TextStyle( + fontSize: 14.0, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ); + } + + Widget _buildSubtitleToggle() { + //if don't have subtitle hiden button + if (chewieController.subtitle?.isEmpty ?? true) { + return const SizedBox(); + } + return GestureDetector( + onTap: _onSubtitleTap, + child: Container( + height: barHeight, + color: Colors.transparent, + padding: const EdgeInsets.only( + left: 12.0, + right: 12.0, + ), + child: Icon( + _subtitleOn + ? Icons.closed_caption + : Icons.closed_caption_off_outlined, + color: _subtitleOn ? Colors.white : Colors.grey[700], + ), + ), + ); + } + + void _onSubtitleTap() { + setState(() { + _subtitleOn = !_subtitleOn; + }); + } + + void _cancelAndRestartTimer() { + _hideTimer?.cancel(); + _startHideTimer(); + + setState(() { + notifier.hideStuff = false; + _displayTapped = true; + }); + } + + Future _initialize() async { + _subtitleOn = chewieController.showSubtitles && + (chewieController.subtitle?.isNotEmpty ?? false); + controller.addListener(_updateState); + + _updateState(); + + if (controller.value.isPlaying || chewieController.autoPlay) { + _startHideTimer(); + } + + if (chewieController.showControlsOnInitialize) { + _initTimer = Timer(const Duration(milliseconds: 200), () { + setState(() { + notifier.hideStuff = false; + }); + }); + } + } + + void _onExpandCollapse() { + setState(() { + notifier.hideStuff = true; + + chewieController.toggleFullScreen(); + _showAfterExpandCollapseTimer = + Timer(const Duration(milliseconds: 300), () { + setState(() { + _cancelAndRestartTimer(); + }); + }); + }); + } + + void _playPause() { + final bool isFinished = (_latestValue.position >= _latestValue.duration) && + _latestValue.duration.inSeconds > 0; + + setState(() { + if (controller.value.isPlaying) { + notifier.hideStuff = false; + _hideTimer?.cancel(); + controller.pause(); + } else { + _cancelAndRestartTimer(); + + if (!controller.value.isInitialized) { + controller.initialize().then((_) { + controller.play(); + }); + } else { + if (isFinished) { + controller.seekTo(Duration.zero); + } + controller.play(); + } + } + }); + } + + void _seekRelative(Duration relativeSeek) { + _cancelAndRestartTimer(); + final position = _latestValue.position + relativeSeek; + final duration = _latestValue.duration; + + if (position < Duration.zero) { + controller.seekTo(Duration.zero); + } else if (position > duration) { + controller.seekTo(duration); + } else { + controller.seekTo(position); + } + } + + void _seekBackward() { + _seekRelative( + const Duration( + seconds: -10, + ), + ); + } + + void _seekForward() { + _seekRelative( + const Duration( + seconds: 10, + ), + ); + } + + void _startHideTimer() { + final hideControlsTimer = chewieController.hideControlsTimer.isNegative + ? ChewieController.defaultHideControlsTimer + : chewieController.hideControlsTimer; + _hideTimer = Timer(hideControlsTimer, () { + setState(() { + notifier.hideStuff = true; + }); + }); + } + + void _bufferingTimerTimeout() { + _displayBufferingIndicator = true; + if (mounted) { + setState(() {}); + } + } + + void _updateState() { + if (!mounted) return; + + final bool buffering = getIsBuffering(controller); + + // display the progress bar indicator only after the buffering delay if it has been set + if (chewieController.progressIndicatorDelay != null) { + if (buffering) { + _bufferingDisplayTimer ??= Timer( + chewieController.progressIndicatorDelay!, + _bufferingTimerTimeout, + ); + } else { + _bufferingDisplayTimer?.cancel(); + _bufferingDisplayTimer = null; + _displayBufferingIndicator = false; + } + } else { + _displayBufferingIndicator = buffering; + } + + setState(() { + _latestValue = controller.value; + _subtitlesPosition = controller.value.position; + }); + } + + Widget _buildProgressBar() { + return Expanded( + child: MaterialVideoProgressBar( + controller, + onDragStart: () { + setState(() { + _dragging = true; + }); + + _hideTimer?.cancel(); + }, + onDragUpdate: () { + _hideTimer?.cancel(); + }, + onDragEnd: () { + setState(() { + _dragging = false; + }); + + _startHideTimer(); + }, + colors: chewieController.materialProgressColors ?? + ChewieProgressColors( + playedColor: Theme.of(context).colorScheme.secondary, + handleColor: Theme.of(context).colorScheme.secondary, + bufferedColor: + Theme.of(context).colorScheme.surface.withOpacityCompat(0.5), + backgroundColor: + Theme.of(context).disabledColor.withOpacityCompat(.5), + ), + draggableProgressBar: chewieController.draggableProgressBar, + ), + ); + } +} diff --git a/lib/src/material/material_desktop_controls.dart b/lib/src/material/material_desktop_controls.dart new file mode 100644 index 000000000..ebffd9560 --- /dev/null +++ b/lib/src/material/material_desktop_controls.dart @@ -0,0 +1,672 @@ +import 'dart:async'; + +import 'package:chewie/src/animated_play_pause.dart'; +import 'package:chewie/src/center_play_button.dart'; +import 'package:chewie/src/chewie_player.dart'; +import 'package:chewie/src/chewie_progress_colors.dart'; +import 'package:chewie/src/helpers/utils.dart'; +import 'package:chewie/src/material/color_compat_extensions.dart'; +import 'package:chewie/src/material/material_progress_bar.dart'; +import 'package:chewie/src/material/widgets/options_dialog.dart'; +import 'package:chewie/src/material/widgets/playback_speed_dialog.dart'; +import 'package:chewie/src/models/option_item.dart'; +import 'package:chewie/src/models/subtitle_model.dart'; +import 'package:chewie/src/notifiers/index.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; +import 'package:video_player/video_player.dart'; + +class MaterialDesktopControls extends StatefulWidget { + const MaterialDesktopControls({ + this.showPlayButton = true, + super.key, + }); + + final bool showPlayButton; + + @override + State createState() { + return _MaterialDesktopControlsState(); + } +} + +class _MaterialDesktopControlsState extends State + with SingleTickerProviderStateMixin { + late PlayerNotifier notifier; + late VideoPlayerValue _latestValue; + double? _latestVolume; + Timer? _hideTimer; + Timer? _initTimer; + late var _subtitlesPosition = Duration.zero; + bool _subtitleOn = false; + Timer? _showAfterExpandCollapseTimer; + bool _dragging = false; + bool _displayTapped = false; + Timer? _bufferingDisplayTimer; + bool _displayBufferingIndicator = false; + + final barHeight = 48.0 * 1.5; + final marginSize = 5.0; + + late VideoPlayerController controller; + ChewieController? _chewieController; + late final FocusNode _focusNode; + + // We know that _chewieController is set in didChangeDependencies + ChewieController get chewieController => _chewieController!; + + @override + void initState() { + super.initState(); + _focusNode = FocusNode(); + _focusNode.requestFocus(); + notifier = Provider.of(context, listen: false); + } + + void _handleKeyPress(event) { + if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.space) { + _playPause(); + } else if (event is KeyDownEvent && + event.logicalKey == LogicalKeyboardKey.arrowRight) { + _seekForward(); + } else if (event is KeyDownEvent && + event.logicalKey == LogicalKeyboardKey.arrowLeft) { + _seekBackward(); + } else if (event is KeyDownEvent && + event.logicalKey == LogicalKeyboardKey.escape) { + if (chewieController.isFullScreen) { + _onExpandCollapse(); + } + } + } + + @override + Widget build(BuildContext context) { + if (_latestValue.hasError) { + return chewieController.errorBuilder?.call( + context, + chewieController.videoPlayerController.value.errorDescription!, + ) ?? + const Center( + child: Icon( + Icons.error, + color: Colors.white, + size: 42, + ), + ); + } + + return KeyboardListener( + focusNode: _focusNode, + onKeyEvent: _handleKeyPress, + child: MouseRegion( + onHover: (_) { + _focusNode.requestFocus(); + _cancelAndRestartTimer(); + }, + child: GestureDetector( + onTap: () { + _playPause(); + _cancelAndRestartTimer(); + }, + child: AbsorbPointer( + absorbing: notifier.hideStuff, + child: Stack( + children: [ + if (_displayBufferingIndicator) + _chewieController?.bufferingBuilder?.call(context) ?? + const Center( + child: CircularProgressIndicator(), + ) + else + _buildHitArea(), + Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (_subtitleOn) + Transform.translate( + offset: Offset( + 0.0, + notifier.hideStuff ? barHeight * 0.8 : 0.0, + ), + child: _buildSubtitles( + context, chewieController.subtitle!), + ), + _buildBottomBar(context), + ], + ), + ], + ), + ), + ), + ), + ); + } + + @override + void dispose() { + _dispose(); + _focusNode.dispose(); + super.dispose(); + } + + void _dispose() { + controller.removeListener(_updateState); + _hideTimer?.cancel(); + _initTimer?.cancel(); + _showAfterExpandCollapseTimer?.cancel(); + } + + @override + void didChangeDependencies() { + final oldController = _chewieController; + _chewieController = ChewieController.of(context); + controller = chewieController.videoPlayerController; + + if (oldController != chewieController) { + _dispose(); + _initialize(); + } + + super.didChangeDependencies(); + } + + Widget _buildSubtitleToggle({IconData? icon, bool isPadded = false}) { + return IconButton( + padding: isPadded ? const EdgeInsets.all(8.0) : EdgeInsets.zero, + icon: Icon(icon, color: _subtitleOn ? Colors.white : Colors.grey[700]), + onPressed: _onSubtitleTap, + ); + } + + Widget _buildOptionsButton({ + IconData? icon, + bool isPadded = false, + }) { + final options = [ + OptionItem( + onTap: (context) async { + Navigator.pop(context); + _onSpeedButtonTap(); + }, + iconData: Icons.speed, + title: chewieController.optionsTranslation?.playbackSpeedButtonText ?? + 'Playback speed', + ) + ]; + + if (chewieController.additionalOptions != null && + chewieController.additionalOptions!(context).isNotEmpty) { + options.addAll(chewieController.additionalOptions!(context)); + } + + return AnimatedOpacity( + opacity: notifier.hideStuff ? 0.0 : 1.0, + duration: const Duration(milliseconds: 250), + child: IconButton( + padding: isPadded ? const EdgeInsets.all(8.0) : EdgeInsets.zero, + onPressed: () async { + _hideTimer?.cancel(); + + if (chewieController.optionsBuilder != null) { + await chewieController.optionsBuilder!(context, options); + } else { + await showModalBottomSheet( + context: context, + isScrollControlled: true, + useRootNavigator: chewieController.useRootNavigator, + builder: (context) => OptionsDialog( + options: options, + cancelButtonText: + chewieController.optionsTranslation?.cancelButtonText, + ), + ); + } + + if (_latestValue.isPlaying) { + _startHideTimer(); + } + }, + icon: Icon( + icon ?? Icons.more_vert, + color: Colors.white, + ), + ), + ); + } + + Widget _buildSubtitles(BuildContext context, Subtitles subtitles) { + if (!_subtitleOn) { + return const SizedBox(); + } + final currentSubtitle = subtitles.getByPosition(_subtitlesPosition); + if (currentSubtitle.isEmpty) { + return const SizedBox(); + } + + if (chewieController.subtitleBuilder != null) { + return chewieController.subtitleBuilder!( + context, + currentSubtitle.first!.text, + ); + } + + return Padding( + padding: EdgeInsets.all(marginSize), + child: Container( + padding: const EdgeInsets.all(5), + decoration: BoxDecoration( + color: const Color(0x96000000), + borderRadius: BorderRadius.circular(10.0), + ), + child: Text( + currentSubtitle.first!.text.toString(), + style: const TextStyle( + fontSize: 18, + ), + textAlign: TextAlign.center, + ), + ), + ); + } + + AnimatedOpacity _buildBottomBar( + BuildContext context, + ) { + final iconColor = Theme.of(context).textTheme.labelLarge!.color; + + return AnimatedOpacity( + opacity: notifier.hideStuff ? 0.0 : 1.0, + duration: const Duration(milliseconds: 300), + child: Container( + height: barHeight + (chewieController.isFullScreen ? 20.0 : 0), + padding: + EdgeInsets.only(bottom: chewieController.isFullScreen ? 10.0 : 15), + child: SafeArea( + bottom: chewieController.isFullScreen, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + verticalDirection: VerticalDirection.up, + children: [ + Flexible( + child: Row( + children: [ + _buildPlayPause(controller), + if (chewieController.allowMuting) + _buildMuteButton(controller), + if (chewieController.isLive) + const Expanded(child: Text('LIVE')) + else + _buildPosition(iconColor), + const Spacer(), + if (chewieController.showControls && + chewieController.subtitle != null && + chewieController.subtitle!.isNotEmpty) + _buildSubtitleToggle(icon: Icons.subtitles), + if (chewieController.showOptions) + _buildOptionsButton(icon: Icons.settings), + if (chewieController.allowFullScreen) _buildExpandButton(), + ], + ), + ), + if (!chewieController.isLive) + Expanded( + child: Container( + padding: EdgeInsets.only( + right: 20, + left: 20, + bottom: chewieController.isFullScreen ? 5.0 : 0, + ), + child: Row( + children: [ + _buildProgressBar(), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } + + GestureDetector _buildExpandButton() { + return GestureDetector( + onTap: _onExpandCollapse, + child: AnimatedOpacity( + opacity: notifier.hideStuff ? 0.0 : 1.0, + duration: const Duration(milliseconds: 300), + child: Container( + height: barHeight + (chewieController.isFullScreen ? 15.0 : 0), + margin: const EdgeInsets.only(right: 12.0), + padding: const EdgeInsets.only( + left: 8.0, + right: 8.0, + ), + child: Center( + child: Icon( + chewieController.isFullScreen + ? Icons.fullscreen_exit + : Icons.fullscreen, + color: Colors.white, + ), + ), + ), + ), + ); + } + + Widget _buildHitArea() { + final bool isFinished = _latestValue.position >= _latestValue.duration && + _latestValue.duration.inSeconds > 0; + final bool showPlayButton = + widget.showPlayButton && !_dragging && !notifier.hideStuff; + + return GestureDetector( + onTap: () { + if (_latestValue.isPlaying) { + if (_chewieController?.pauseOnBackgroundTap ?? false) { + _playPause(); + _cancelAndRestartTimer(); + } else { + if (_displayTapped) { + setState(() { + notifier.hideStuff = true; + }); + } else { + _cancelAndRestartTimer(); + } + } + } else { + _playPause(); + + setState(() { + notifier.hideStuff = true; + }); + } + }, + child: CenterPlayButton( + backgroundColor: Colors.black54, + iconColor: Colors.white, + isFinished: isFinished, + isPlaying: controller.value.isPlaying, + show: showPlayButton, + onPressed: _playPause, + ), + ); + } + + Future _onSpeedButtonTap() async { + _hideTimer?.cancel(); + + final chosenSpeed = await showModalBottomSheet( + context: context, + isScrollControlled: true, + useRootNavigator: chewieController.useRootNavigator, + builder: (context) => PlaybackSpeedDialog( + speeds: chewieController.playbackSpeeds, + selected: _latestValue.playbackSpeed, + ), + ); + + if (chosenSpeed != null) { + controller.setPlaybackSpeed(chosenSpeed); + } + + if (_latestValue.isPlaying) { + _startHideTimer(); + } + } + + GestureDetector _buildMuteButton( + VideoPlayerController controller, + ) { + return GestureDetector( + onTap: () { + _cancelAndRestartTimer(); + + if (_latestValue.volume == 0) { + controller.setVolume(_latestVolume ?? 0.5); + } else { + _latestVolume = controller.value.volume; + controller.setVolume(0.0); + } + }, + child: AnimatedOpacity( + opacity: notifier.hideStuff ? 0.0 : 1.0, + duration: const Duration(milliseconds: 300), + child: ClipRect( + child: Container( + height: barHeight, + padding: const EdgeInsets.only( + right: 15.0, + ), + child: Icon( + _latestValue.volume > 0 ? Icons.volume_up : Icons.volume_off, + color: Colors.white, + ), + ), + ), + ), + ); + } + + GestureDetector _buildPlayPause(VideoPlayerController controller) { + return GestureDetector( + onTap: _playPause, + child: Container( + height: barHeight, + color: Colors.transparent, + margin: const EdgeInsets.only(left: 8.0, right: 4.0), + padding: const EdgeInsets.only( + left: 12.0, + right: 12.0, + ), + child: AnimatedPlayPause( + playing: controller.value.isPlaying, + color: Colors.white, + ), + ), + ); + } + + Widget _buildPosition(Color? iconColor) { + final position = _latestValue.position; + final duration = _latestValue.duration; + + return Text( + '${formatDuration(position)} / ${formatDuration(duration)}', + style: const TextStyle( + fontSize: 14.0, + color: Colors.white, + ), + ); + } + + void _onSubtitleTap() { + setState(() { + _subtitleOn = !_subtitleOn; + }); + } + + void _cancelAndRestartTimer() { + _hideTimer?.cancel(); + _startHideTimer(); + + setState(() { + notifier.hideStuff = false; + _displayTapped = true; + }); + } + + Future _initialize() async { + _subtitleOn = chewieController.showSubtitles && + (chewieController.subtitle?.isNotEmpty ?? false); + controller.addListener(_updateState); + + _updateState(); + + if (controller.value.isPlaying || chewieController.autoPlay) { + _startHideTimer(); + } + + if (chewieController.showControlsOnInitialize) { + _initTimer = Timer(const Duration(milliseconds: 200), () { + setState(() { + notifier.hideStuff = false; + }); + }); + } + } + + void _onExpandCollapse() { + setState(() { + notifier.hideStuff = true; + }); + + chewieController.toggleFullScreen(); + + _showAfterExpandCollapseTimer = + Timer(const Duration(milliseconds: 300), () { + setState(() { + _cancelAndRestartTimer(); + }); + }); + } + + void _playPause() { + if (controller.value.isPlaying) { + setState(() { + notifier.hideStuff = false; + }); + + _hideTimer?.cancel(); + controller.pause(); + } else { + _cancelAndRestartTimer(); + + if (!controller.value.isInitialized) { + controller.initialize().then((_) { + //[VideoPlayerController.play] If the video is at the end, this method starts playing from the beginning + controller.play(); + }); + } else { + //[VideoPlayerController.play] If the video is at the end, this method starts playing from the beginning + controller.play(); + } + } + } + + void _startHideTimer() { + final hideControlsTimer = chewieController.hideControlsTimer.isNegative + ? ChewieController.defaultHideControlsTimer + : chewieController.hideControlsTimer; + _hideTimer = Timer(hideControlsTimer, () { + setState(() { + notifier.hideStuff = true; + }); + }); + } + + void _bufferingTimerTimeout() { + _displayBufferingIndicator = true; + if (mounted) { + setState(() {}); + } + } + + void _updateState() { + if (!mounted) return; + + final bool buffering = getIsBuffering(controller); + + // display the progress bar indicator only after the buffering delay if it has been set + if (chewieController.progressIndicatorDelay != null) { + if (buffering) { + _bufferingDisplayTimer ??= Timer( + chewieController.progressIndicatorDelay!, + _bufferingTimerTimeout, + ); + } else { + _bufferingDisplayTimer?.cancel(); + _bufferingDisplayTimer = null; + _displayBufferingIndicator = false; + } + } else { + _displayBufferingIndicator = buffering; + } + + setState(() { + _latestValue = controller.value; + _subtitlesPosition = controller.value.position; + }); + } + + void _seekBackward() { + _seekRelative( + const Duration( + seconds: -10, + ), + ); + } + + void _seekForward() { + _seekRelative( + const Duration( + seconds: 10, + ), + ); + } + + void _seekRelative(Duration relativeSeek) { + _cancelAndRestartTimer(); + final position = _latestValue.position + relativeSeek; + final duration = _latestValue.duration; + + if (position < Duration.zero) { + controller.seekTo(Duration.zero); + } else if (position > duration) { + controller.seekTo(duration); + } else { + controller.seekTo(position); + } + } + + Widget _buildProgressBar() { + return Expanded( + child: MaterialVideoProgressBar( + controller, + onDragStart: () { + setState(() { + _dragging = true; + }); + + _hideTimer?.cancel(); + }, + onDragUpdate: () { + _hideTimer?.cancel(); + }, + onDragEnd: () { + setState(() { + _dragging = false; + }); + + _startHideTimer(); + }, + colors: chewieController.materialProgressColors ?? + ChewieProgressColors( + playedColor: Theme.of(context).colorScheme.secondary, + handleColor: Theme.of(context).colorScheme.secondary, + bufferedColor: + Theme.of(context).colorScheme.surface.withOpacityCompat(0.5), + backgroundColor: + Theme.of(context).disabledColor.withOpacityCompat(0.5), + ), + draggableProgressBar: chewieController.draggableProgressBar, + ), + ); + } +} diff --git a/lib/src/material/material_progress_bar.dart b/lib/src/material/material_progress_bar.dart new file mode 100644 index 000000000..ba58a37d6 --- /dev/null +++ b/lib/src/material/material_progress_bar.dart @@ -0,0 +1,44 @@ +import 'package:chewie/src/chewie_progress_colors.dart'; +import 'package:chewie/src/progress_bar.dart'; +import 'package:flutter/material.dart'; +import 'package:video_player/video_player.dart'; + +class MaterialVideoProgressBar extends StatelessWidget { + MaterialVideoProgressBar( + this.controller, { + this.height = kToolbarHeight, + this.barHeight = 10, + this.handleHeight = 6, + ChewieProgressColors? colors, + this.onDragEnd, + this.onDragStart, + this.onDragUpdate, + super.key, + this.draggableProgressBar = true, + }) : colors = colors ?? ChewieProgressColors(); + + final double height; + final double barHeight; + final double handleHeight; + final VideoPlayerController controller; + final ChewieProgressColors colors; + final Function()? onDragStart; + final Function()? onDragEnd; + final Function()? onDragUpdate; + final bool draggableProgressBar; + + @override + Widget build(BuildContext context) { + return VideoProgressBar( + controller, + barHeight: barHeight, + handleHeight: handleHeight, + drawShadow: true, + colors: colors, + onDragEnd: onDragEnd, + onDragStart: onDragStart, + onDragUpdate: onDragUpdate, + draggableProgressBar: draggableProgressBar, + ); + } +} diff --git a/lib/src/material/widgets/options_dialog.dart b/lib/src/material/widgets/options_dialog.dart new file mode 100644 index 000000000..047f12097 --- /dev/null +++ b/lib/src/material/widgets/options_dialog.dart @@ -0,0 +1,57 @@ +import 'package:chewie/src/models/option_item.dart'; +import 'package:flutter/material.dart'; + +class OptionsDialog extends StatefulWidget { + const OptionsDialog({ + super.key, + required this.options, + this.cancelButtonText, + }); + + final List options; + final String? cancelButtonText; + + @override + // ignore: library_private_types_in_public_api + _OptionsDialogState createState() => _OptionsDialogState(); +} + +class _OptionsDialogState extends State { + @override + Widget build(BuildContext context) { + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListView.builder( + shrinkWrap: true, + itemCount: widget.options.length, + itemBuilder: (context, i) { + return ListTile( + onTap: () => widget.options[i].onTap(context), + leading: Icon(widget.options[i].iconData), + title: Text(widget.options[i].title), + subtitle: widget.options[i].subtitle != null + ? Text(widget.options[i].subtitle!) + : null, + ); + }, + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Divider( + thickness: 1.0, + ), + ), + ListTile( + onTap: () => Navigator.pop(context), + leading: const Icon(Icons.close), + title: Text( + widget.cancelButtonText ?? 'Cancel', + ), + ), + ], + ), + ); + } +} diff --git a/lib/src/material/widgets/playback_speed_dialog.dart b/lib/src/material/widgets/playback_speed_dialog.dart new file mode 100644 index 000000000..0525a8f3b --- /dev/null +++ b/lib/src/material/widgets/playback_speed_dialog.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; + +class PlaybackSpeedDialog extends StatelessWidget { + const PlaybackSpeedDialog({ + super.key, + required List speeds, + required double selected, + }) : _speeds = speeds, + _selected = selected; + + final List _speeds; + final double _selected; + + @override + Widget build(BuildContext context) { + final Color selectedColor = Theme.of(context).primaryColor; + + return ListView.builder( + shrinkWrap: true, + physics: const ScrollPhysics(), + itemBuilder: (context, index) { + final speed = _speeds[index]; + return ListTile( + dense: true, + title: Row( + children: [ + if (speed == _selected) + Icon( + Icons.check, + size: 20.0, + color: selectedColor, + ) + else + Container(width: 20.0), + const SizedBox(width: 16.0), + Text(speed.toString()), + ], + ), + selected: speed == _selected, + onTap: () { + Navigator.of(context).pop(speed); + }, + ); + }, + itemCount: _speeds.length, + ); + } +} diff --git a/lib/src/material_controls.dart b/lib/src/material_controls.dart deleted file mode 100644 index 8ad8be8a1..000000000 --- a/lib/src/material_controls.dart +++ /dev/null @@ -1,399 +0,0 @@ -import 'dart:async'; - -import 'package:chewie/src/chewie_player.dart'; -import 'package:chewie/src/chewie_progress_colors.dart'; -import 'package:chewie/src/material_progress_bar.dart'; -import 'package:chewie/src/utils.dart'; -import 'package:flutter/material.dart'; -import 'package:video_player/video_player.dart'; - -class MaterialControls extends StatefulWidget { - const MaterialControls({Key key}) : super(key: key); - - @override - State createState() { - return _MaterialControlsState(); - } -} - -class _MaterialControlsState extends State { - VideoPlayerValue _latestValue; - double _latestVolume; - bool _hideStuff = true; - Timer _hideTimer; - Timer _initTimer; - Timer _showAfterExpandCollapseTimer; - bool _dragging = false; - bool _displayTapped = false; - - final barHeight = 48.0; - final marginSize = 5.0; - - VideoPlayerController controller; - ChewieController chewieController; - - @override - Widget build(BuildContext context) { - if (_latestValue.hasError) { - return chewieController.errorBuilder != null - ? chewieController.errorBuilder( - context, - chewieController.videoPlayerController.value.errorDescription, - ) - : Center( - child: Icon( - Icons.error, - color: Colors.white, - size: 42, - ), - ); - } - - return MouseRegion( - onHover: (_) { - _cancelAndRestartTimer(); - }, - child: GestureDetector( - onTap: () => _cancelAndRestartTimer(), - child: AbsorbPointer( - absorbing: _hideStuff, - child: Column( - children: [ - _latestValue != null && - !_latestValue.isPlaying && - _latestValue.duration == null || - _latestValue.isBuffering - ? const Expanded( - child: const Center( - child: const CircularProgressIndicator(), - ), - ) - : _buildHitArea(), - _buildBottomBar(context), - ], - ), - ), - ), - ); - } - - @override - void dispose() { - _dispose(); - super.dispose(); - } - - void _dispose() { - controller.removeListener(_updateState); - _hideTimer?.cancel(); - _initTimer?.cancel(); - _showAfterExpandCollapseTimer?.cancel(); - } - - @override - void didChangeDependencies() { - final _oldController = chewieController; - chewieController = ChewieController.of(context); - controller = chewieController.videoPlayerController; - - if (_oldController != chewieController) { - _dispose(); - _initialize(); - } - - super.didChangeDependencies(); - } - - AnimatedOpacity _buildBottomBar( - BuildContext context, - ) { - final iconColor = Theme.of(context).textTheme.button.color; - - return AnimatedOpacity( - opacity: _hideStuff ? 0.0 : 1.0, - duration: Duration(milliseconds: 300), - child: Container( - height: barHeight, - color: Theme.of(context).dialogBackgroundColor, - child: Row( - children: [ - _buildPlayPause(controller), - chewieController.isLive - ? Expanded(child: const Text('LIVE')) - : _buildPosition(iconColor), - chewieController.isLive ? const SizedBox() : _buildProgressBar(), - chewieController.allowMuting - ? _buildMuteButton(controller) - : Container(), - chewieController.allowFullScreen - ? _buildExpandButton() - : Container(), - ], - ), - ), - ); - } - - GestureDetector _buildExpandButton() { - return GestureDetector( - onTap: _onExpandCollapse, - child: AnimatedOpacity( - opacity: _hideStuff ? 0.0 : 1.0, - duration: Duration(milliseconds: 300), - child: Container( - height: barHeight, - margin: EdgeInsets.only(right: 12.0), - padding: EdgeInsets.only( - left: 8.0, - right: 8.0, - ), - child: Center( - child: Icon( - chewieController.isFullScreen - ? Icons.fullscreen_exit - : Icons.fullscreen, - ), - ), - ), - ), - ); - } - - Expanded _buildHitArea() { - return Expanded( - child: GestureDetector( - onTap: () { - if (_latestValue != null && _latestValue.isPlaying) { - if (_displayTapped) { - setState(() { - _hideStuff = true; - }); - } else - _cancelAndRestartTimer(); - } else { - _playPause(); - - setState(() { - _hideStuff = true; - }); - } - }, - child: Container( - color: Colors.transparent, - child: Center( - child: AnimatedOpacity( - opacity: - _latestValue != null && !_latestValue.isPlaying && !_dragging - ? 1.0 - : 0.0, - duration: Duration(milliseconds: 300), - child: GestureDetector( - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).dialogBackgroundColor, - borderRadius: BorderRadius.circular(48.0), - ), - child: Padding( - padding: EdgeInsets.all(12.0), - child: Icon(Icons.play_arrow, size: 32.0), - ), - ), - ), - ), - ), - ), - ), - ); - } - - GestureDetector _buildMuteButton( - VideoPlayerController controller, - ) { - return GestureDetector( - onTap: () { - _cancelAndRestartTimer(); - - if (_latestValue.volume == 0) { - controller.setVolume(_latestVolume ?? 0.5); - } else { - _latestVolume = controller.value.volume; - controller.setVolume(0.0); - } - }, - child: AnimatedOpacity( - opacity: _hideStuff ? 0.0 : 1.0, - duration: Duration(milliseconds: 300), - child: ClipRect( - child: Container( - child: Container( - height: barHeight, - padding: EdgeInsets.only( - left: 8.0, - right: 8.0, - ), - child: Icon( - (_latestValue != null && _latestValue.volume > 0) - ? Icons.volume_up - : Icons.volume_off, - ), - ), - ), - ), - ), - ); - } - - GestureDetector _buildPlayPause(VideoPlayerController controller) { - return GestureDetector( - onTap: _playPause, - child: Container( - height: barHeight, - color: Colors.transparent, - margin: EdgeInsets.only(left: 8.0, right: 4.0), - padding: EdgeInsets.only( - left: 12.0, - right: 12.0, - ), - child: Icon( - controller.value.isPlaying ? Icons.pause : Icons.play_arrow, - ), - ), - ); - } - - Widget _buildPosition(Color iconColor) { - final position = _latestValue != null && _latestValue.position != null - ? _latestValue.position - : Duration.zero; - final duration = _latestValue != null && _latestValue.duration != null - ? _latestValue.duration - : Duration.zero; - - return Padding( - padding: EdgeInsets.only(right: 24.0), - child: Text( - '${formatDuration(position)} / ${formatDuration(duration)}', - style: TextStyle( - fontSize: 14.0, - ), - ), - ); - } - - void _cancelAndRestartTimer() { - _hideTimer?.cancel(); - _startHideTimer(); - - setState(() { - _hideStuff = false; - _displayTapped = true; - }); - } - - Future _initialize() async { - controller.addListener(_updateState); - - _updateState(); - - if ((controller.value != null && controller.value.isPlaying) || - chewieController.autoPlay) { - _startHideTimer(); - } - - if (chewieController.showControlsOnInitialize) { - _initTimer = Timer(Duration(milliseconds: 200), () { - setState(() { - _hideStuff = false; - }); - }); - } - } - - void _onExpandCollapse() { - setState(() { - _hideStuff = true; - - chewieController.toggleFullScreen(); - _showAfterExpandCollapseTimer = Timer(Duration(milliseconds: 300), () { - setState(() { - _cancelAndRestartTimer(); - }); - }); - }); - } - - void _playPause() { - bool isFinished; - if (_latestValue.duration != null) { - isFinished = _latestValue.position >= _latestValue.duration; - } else { - isFinished = false; - } - - setState(() { - if (controller.value.isPlaying) { - _hideStuff = false; - _hideTimer?.cancel(); - controller.pause(); - } else { - _cancelAndRestartTimer(); - - if (!controller.value.initialized) { - controller.initialize().then((_) { - controller.play(); - }); - } else { - if (isFinished) { - controller.seekTo(Duration(seconds: 0)); - } - controller.play(); - } - } - }); - } - - void _startHideTimer() { - _hideTimer = Timer(const Duration(seconds: 3), () { - setState(() { - _hideStuff = true; - }); - }); - } - - void _updateState() { - setState(() { - _latestValue = controller.value; - }); - } - - Widget _buildProgressBar() { - return Expanded( - child: Padding( - padding: EdgeInsets.only(right: 20.0), - child: MaterialVideoProgressBar( - controller, - onDragStart: () { - setState(() { - _dragging = true; - }); - - _hideTimer?.cancel(); - }, - onDragEnd: () { - setState(() { - _dragging = false; - }); - - _startHideTimer(); - }, - colors: chewieController.materialProgressColors ?? - ChewieProgressColors( - playedColor: Theme.of(context).accentColor, - handleColor: Theme.of(context).accentColor, - bufferedColor: Theme.of(context).backgroundColor, - backgroundColor: Theme.of(context).disabledColor), - ), - ), - ); - } -} diff --git a/lib/src/material_progress_bar.dart b/lib/src/material_progress_bar.dart deleted file mode 100644 index 5beac567f..000000000 --- a/lib/src/material_progress_bar.dart +++ /dev/null @@ -1,180 +0,0 @@ -import 'package:chewie/src/chewie_progress_colors.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; -import 'package:video_player/video_player.dart'; - -class MaterialVideoProgressBar extends StatefulWidget { - MaterialVideoProgressBar( - this.controller, { - ChewieProgressColors colors, - this.onDragEnd, - this.onDragStart, - this.onDragUpdate, - }) : colors = colors ?? ChewieProgressColors(); - - final VideoPlayerController controller; - final ChewieProgressColors colors; - final Function() onDragStart; - final Function() onDragEnd; - final Function() onDragUpdate; - - @override - _VideoProgressBarState createState() { - return _VideoProgressBarState(); - } -} - -class _VideoProgressBarState extends State { - _VideoProgressBarState() { - listener = () { - if (!this.mounted) return; - setState(() {}); - }; - } - - VoidCallback listener; - bool _controllerWasPlaying = false; - - VideoPlayerController get controller => widget.controller; - - @override - void initState() { - super.initState(); - controller.addListener(listener); - } - - @override - void deactivate() { - controller.removeListener(listener); - super.deactivate(); - } - - @override - Widget build(BuildContext context) { - void seekToRelativePosition(Offset globalPosition) { - final box = context.findRenderObject() as RenderBox; - final Offset tapPos = box.globalToLocal(globalPosition); - final double relative = tapPos.dx / box.size.width; - final Duration position = controller.value.duration * relative; - controller.seekTo(position); - } - - return GestureDetector( - child: Center( - child: Container( - height: MediaQuery.of(context).size.height / 2, - width: MediaQuery.of(context).size.width, - color: Colors.transparent, - child: CustomPaint( - painter: _ProgressBarPainter( - controller.value, - widget.colors, - ), - ), - ), - ), - onHorizontalDragStart: (DragStartDetails details) { - if (!controller.value.initialized) { - return; - } - _controllerWasPlaying = controller.value.isPlaying; - if (_controllerWasPlaying) { - controller.pause(); - } - - if (widget.onDragStart != null) { - widget.onDragStart(); - } - }, - onHorizontalDragUpdate: (DragUpdateDetails details) { - if (!controller.value.initialized) { - return; - } - seekToRelativePosition(details.globalPosition); - - if (widget.onDragUpdate != null) { - widget.onDragUpdate(); - } - }, - onHorizontalDragEnd: (DragEndDetails details) { - if (_controllerWasPlaying) { - controller.play(); - } - - if (widget.onDragEnd != null) { - widget.onDragEnd(); - } - }, - onTapDown: (TapDownDetails details) { - if (!controller.value.initialized) { - return; - } - seekToRelativePosition(details.globalPosition); - }, - ); - } -} - -class _ProgressBarPainter extends CustomPainter { - _ProgressBarPainter(this.value, this.colors); - - VideoPlayerValue value; - ChewieProgressColors colors; - - @override - bool shouldRepaint(CustomPainter painter) { - return true; - } - - @override - void paint(Canvas canvas, Size size) { - final height = 2.0; - - canvas.drawRRect( - RRect.fromRectAndRadius( - Rect.fromPoints( - Offset(0.0, size.height / 2), - Offset(size.width, size.height / 2 + height), - ), - Radius.circular(4.0), - ), - colors.backgroundPaint, - ); - if (!value.initialized) { - return; - } - final double playedPartPercent = - value.position.inMilliseconds / value.duration.inMilliseconds; - final double playedPart = - playedPartPercent > 1 ? size.width : playedPartPercent * size.width; - for (DurationRange range in value.buffered) { - final double start = range.startFraction(value.duration) * size.width; - final double end = range.endFraction(value.duration) * size.width; - canvas.drawRRect( - RRect.fromRectAndRadius( - Rect.fromPoints( - Offset(start, size.height / 2), - Offset(end, size.height / 2 + height), - ), - Radius.circular(4.0), - ), - colors.bufferedPaint, - ); - } - canvas.drawRRect( - RRect.fromRectAndRadius( - Rect.fromPoints( - Offset(0.0, size.height / 2), - Offset(playedPart, size.height / 2 + height), - ), - Radius.circular(4.0), - ), - colors.playedPaint, - ); - canvas.drawCircle( - Offset(playedPart, size.height / 2 + height / 2), - height * 3, - colors.handlePaint, - ); - } -} diff --git a/lib/src/models/index.dart b/lib/src/models/index.dart new file mode 100644 index 000000000..a308c33db --- /dev/null +++ b/lib/src/models/index.dart @@ -0,0 +1,3 @@ +export 'option_item.dart'; +export 'options_translation.dart'; +export 'subtitle_model.dart'; diff --git a/lib/src/models/option_item.dart b/lib/src/models/option_item.dart new file mode 100644 index 000000000..089d34294 --- /dev/null +++ b/lib/src/models/option_item.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; + +class OptionItem { + OptionItem({ + required this.onTap, + required this.iconData, + required this.title, + this.subtitle, + }); + + final void Function(BuildContext context) onTap; + final IconData iconData; + final String title; + final String? subtitle; + + OptionItem copyWith({ + Function(BuildContext context)? onTap, + IconData? iconData, + String? title, + String? subtitle, + }) { + return OptionItem( + onTap: onTap ?? this.onTap, + iconData: iconData ?? this.iconData, + title: title ?? this.title, + subtitle: subtitle ?? this.subtitle, + ); + } + + @override + String toString() => + 'OptionItem(onTap: $onTap, iconData: $iconData, title: $title, subtitle: $subtitle)'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is OptionItem && + other.onTap == onTap && + other.iconData == iconData && + other.title == title && + other.subtitle == subtitle; + } + + @override + int get hashCode => + onTap.hashCode ^ iconData.hashCode ^ title.hashCode ^ subtitle.hashCode; +} diff --git a/lib/src/models/options_translation.dart b/lib/src/models/options_translation.dart new file mode 100644 index 000000000..ccbe97654 --- /dev/null +++ b/lib/src/models/options_translation.dart @@ -0,0 +1,44 @@ +class OptionsTranslation { + OptionsTranslation({ + this.playbackSpeedButtonText, + this.subtitlesButtonText, + this.cancelButtonText, + }); + + String? playbackSpeedButtonText; + String? subtitlesButtonText; + String? cancelButtonText; + + OptionsTranslation copyWith({ + String? playbackSpeedButtonText, + String? subtitlesButtonText, + String? cancelButtonText, + }) { + return OptionsTranslation( + playbackSpeedButtonText: + playbackSpeedButtonText ?? this.playbackSpeedButtonText, + subtitlesButtonText: subtitlesButtonText ?? this.subtitlesButtonText, + cancelButtonText: cancelButtonText ?? this.cancelButtonText, + ); + } + + @override + String toString() => + 'OptionsTranslation(playbackSpeedButtonText: $playbackSpeedButtonText, subtitlesButtonText: $subtitlesButtonText, cancelButtonText: $cancelButtonText)'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is OptionsTranslation && + other.playbackSpeedButtonText == playbackSpeedButtonText && + other.subtitlesButtonText == subtitlesButtonText && + other.cancelButtonText == cancelButtonText; + } + + @override + int get hashCode => + playbackSpeedButtonText.hashCode ^ + subtitlesButtonText.hashCode ^ + cancelButtonText.hashCode; +} diff --git a/lib/src/models/subtitle_model.dart b/lib/src/models/subtitle_model.dart new file mode 100644 index 000000000..edcac3a59 --- /dev/null +++ b/lib/src/models/subtitle_model.dart @@ -0,0 +1,67 @@ +class Subtitles { + Subtitles(this.subtitle); + + final List subtitle; + + bool get isEmpty => subtitle.isEmpty; + + bool get isNotEmpty => !isEmpty; + + List getByPosition(Duration position) { + final found = subtitle.where((item) { + if (item != null) return position >= item.start && position <= item.end; + return false; + }).toList(); + + return found; + } +} + +class Subtitle { + Subtitle({ + required this.index, + required this.start, + required this.end, + required this.text, + }); + + Subtitle copyWith({ + int? index, + Duration? start, + Duration? end, + dynamic text, + }) { + return Subtitle( + index: index ?? this.index, + start: start ?? this.start, + end: end ?? this.end, + text: text ?? this.text, + ); + } + + final int index; + final Duration start; + final Duration end; + final dynamic text; + + @override + String toString() { + return 'Subtitle(index: $index, start: $start, end: $end, text: $text)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is Subtitle && + other.index == index && + other.start == start && + other.end == end && + other.text == text; + } + + @override + int get hashCode { + return index.hashCode ^ start.hashCode ^ end.hashCode ^ text.hashCode; + } +} diff --git a/lib/src/notifiers/index.dart b/lib/src/notifiers/index.dart new file mode 100644 index 000000000..2fd693a5f --- /dev/null +++ b/lib/src/notifiers/index.dart @@ -0,0 +1 @@ +export 'player_notifier.dart'; diff --git a/lib/src/notifiers/player_notifier.dart b/lib/src/notifiers/player_notifier.dart new file mode 100644 index 000000000..dbba4faac --- /dev/null +++ b/lib/src/notifiers/player_notifier.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +/// +/// The new State-Manager for Chewie! +/// Has to be an instance of Singleton to survive +/// over all State-Changes inside chewie +/// +class PlayerNotifier extends ChangeNotifier { + PlayerNotifier._( + bool hideStuff, + ) : _hideStuff = hideStuff; + + bool _hideStuff; + + bool get hideStuff => _hideStuff; + + set hideStuff(bool value) { + _hideStuff = value; + notifyListeners(); + } + + // ignore: prefer_constructors_over_static_methods + static PlayerNotifier init() { + return PlayerNotifier._( + true, + ); + } +} diff --git a/lib/src/player_with_controls.dart b/lib/src/player_with_controls.dart index b8e216c59..65d51a69b 100644 --- a/lib/src/player_with_controls.dart +++ b/lib/src/player_with_controls.dart @@ -1,20 +1,18 @@ -import 'dart:ui'; - import 'package:chewie/src/chewie_player.dart'; -import 'package:chewie/src/cupertino_controls.dart'; -import 'package:chewie/src/material_controls.dart'; -import 'package:flutter/foundation.dart'; +import 'package:chewie/src/helpers/adaptive_controls.dart'; +import 'package:chewie/src/notifiers/index.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:video_player/video_player.dart'; class PlayerWithControls extends StatelessWidget { - PlayerWithControls({Key key}) : super(key: key); + const PlayerWithControls({super.key}); @override Widget build(BuildContext context) { final ChewieController chewieController = ChewieController.of(context); - double _calculateAspectRatio(BuildContext context) { + double calculateAspectRatio(BuildContext context) { final size = MediaQuery.of(context).size; final width = size.width; final height = size.height; @@ -22,56 +20,81 @@ class PlayerWithControls extends StatelessWidget { return width > height ? width / height : height / width; } - Widget _buildControls( + Widget buildControls( BuildContext context, ChewieController chewieController, ) { return chewieController.showControls - ? chewieController.customControls != null - ? chewieController.customControls - : Theme.of(context).platform == TargetPlatform.android - ? MaterialControls() - : CupertinoControls( - backgroundColor: Color.fromRGBO(41, 41, 41, 0.7), - iconColor: Color.fromARGB(255, 200, 200, 200), - ) - : Container(); + ? chewieController.customControls ?? const AdaptiveControls() + : const SizedBox(); } - Container _buildPlayerWithControls( - ChewieController chewieController, BuildContext context) { - return Container( - child: Stack( - children: [ - chewieController.placeholder ?? Container(), - Center( + Widget buildPlayerWithControls( + ChewieController chewieController, + BuildContext context, + ) { + return Stack( + children: [ + if (chewieController.placeholder != null) + chewieController.placeholder!, + InteractiveViewer( + transformationController: chewieController.transformationController, + maxScale: chewieController.maxScale, + panEnabled: chewieController.zoomAndPan, + scaleEnabled: chewieController.zoomAndPan, + child: Center( child: AspectRatio( aspectRatio: chewieController.aspectRatio ?? chewieController.videoPlayerController.value.aspectRatio, child: VideoPlayer(chewieController.videoPlayerController), ), ), - chewieController.overlay ?? Container(), - if (!chewieController.isFullScreen) - _buildControls(context, chewieController) - else - SafeArea( - child: _buildControls(context, chewieController), + ), + if (chewieController.overlay != null) chewieController.overlay!, + if (Theme.of(context).platform != TargetPlatform.iOS) + Consumer( + builder: ( + BuildContext context, + PlayerNotifier notifier, + Widget? widget, + ) => + Visibility( + visible: !notifier.hideStuff, + child: AnimatedOpacity( + opacity: notifier.hideStuff ? 0.0 : 0.8, + duration: const Duration( + milliseconds: 250, + ), + child: const DecoratedBox( + decoration: BoxDecoration(color: Colors.black54), + child: SizedBox.expand(), + ), + ), ), - ], - ), + ), + if (!chewieController.isFullScreen) + buildControls(context, chewieController) + else + SafeArea( + bottom: false, + child: buildControls(context, chewieController), + ), + ], ); } - return Center( - child: Container( - height: MediaQuery.of(context).size.height, - width: MediaQuery.of(context).size.width, - child: AspectRatio( - aspectRatio: _calculateAspectRatio(context), - child: _buildPlayerWithControls(chewieController, context), + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return Center( + child: SizedBox( + height: constraints.maxHeight, + width: constraints.maxWidth, + child: AspectRatio( + aspectRatio: calculateAspectRatio(context), + child: buildPlayerWithControls(chewieController, context), + ), ), - ), - ); + ); + }); } } diff --git a/lib/src/progress_bar.dart b/lib/src/progress_bar.dart new file mode 100644 index 000000000..b07a16e5b --- /dev/null +++ b/lib/src/progress_bar.dart @@ -0,0 +1,276 @@ +import 'package:chewie/chewie.dart'; +import 'package:flutter/material.dart'; +import 'package:video_player/video_player.dart'; + +class VideoProgressBar extends StatefulWidget { + VideoProgressBar( + this.controller, { + ChewieProgressColors? colors, + this.onDragEnd, + this.onDragStart, + this.onDragUpdate, + this.draggableProgressBar = true, + super.key, + required this.barHeight, + required this.handleHeight, + required this.drawShadow, + }) : colors = colors ?? ChewieProgressColors(); + + final VideoPlayerController controller; + final ChewieProgressColors colors; + final Function()? onDragStart; + final Function()? onDragEnd; + final Function()? onDragUpdate; + + final double barHeight; + final double handleHeight; + final bool drawShadow; + final bool draggableProgressBar; + + @override + // ignore: library_private_types_in_public_api + _VideoProgressBarState createState() { + return _VideoProgressBarState(); + } +} + +class _VideoProgressBarState extends State { + void listener() { + if (!mounted) return; + setState(() {}); + } + + bool _controllerWasPlaying = false; + + Offset? _latestDraggableOffset; + + VideoPlayerController get controller => widget.controller; + + @override + void initState() { + super.initState(); + controller.addListener(listener); + } + + @override + void deactivate() { + controller.removeListener(listener); + super.deactivate(); + } + + void _seekToRelativePosition(Offset globalPosition) { + controller.seekTo(context.calcRelativePosition( + controller.value.duration, + globalPosition, + )); + } + + @override + Widget build(BuildContext context) { + final child = Center( + child: StaticProgressBar( + value: controller.value, + colors: widget.colors, + barHeight: widget.barHeight, + handleHeight: widget.handleHeight, + drawShadow: widget.drawShadow, + latestDraggableOffset: _latestDraggableOffset, + ), + ); + + return widget.draggableProgressBar + ? GestureDetector( + onHorizontalDragStart: (DragStartDetails details) { + if (!controller.value.isInitialized) { + return; + } + _controllerWasPlaying = controller.value.isPlaying; + if (_controllerWasPlaying) { + controller.pause(); + } + + widget.onDragStart?.call(); + }, + onHorizontalDragUpdate: (DragUpdateDetails details) { + if (!controller.value.isInitialized) { + return; + } + _latestDraggableOffset = details.globalPosition; + listener(); + + widget.onDragUpdate?.call(); + }, + onHorizontalDragEnd: (DragEndDetails details) { + if (_controllerWasPlaying) { + controller.play(); + } + + if (_latestDraggableOffset != null) { + _seekToRelativePosition(_latestDraggableOffset!); + _latestDraggableOffset = null; + } + + widget.onDragEnd?.call(); + }, + onTapDown: (TapDownDetails details) { + if (!controller.value.isInitialized) { + return; + } + _seekToRelativePosition(details.globalPosition); + }, + child: child, + ) + : child; + } +} + +class StaticProgressBar extends StatelessWidget { + const StaticProgressBar({ + super.key, + required this.value, + required this.colors, + required this.barHeight, + required this.handleHeight, + required this.drawShadow, + this.latestDraggableOffset, + }); + + final Offset? latestDraggableOffset; + final VideoPlayerValue value; + final ChewieProgressColors colors; + + final double barHeight; + final double handleHeight; + final bool drawShadow; + + @override + Widget build(BuildContext context) { + return Container( + height: MediaQuery.of(context).size.height, + width: MediaQuery.of(context).size.width, + color: Colors.transparent, + child: CustomPaint( + painter: _ProgressBarPainter( + value: value, + draggableValue: latestDraggableOffset != null + ? context.calcRelativePosition( + value.duration, + latestDraggableOffset!, + ) + : null, + colors: colors, + barHeight: barHeight, + handleHeight: handleHeight, + drawShadow: drawShadow, + ), + ), + ); + } +} + +class _ProgressBarPainter extends CustomPainter { + _ProgressBarPainter({ + required this.value, + required this.colors, + required this.barHeight, + required this.handleHeight, + required this.drawShadow, + required this.draggableValue, + }); + + VideoPlayerValue value; + ChewieProgressColors colors; + + final double barHeight; + final double handleHeight; + final bool drawShadow; + + /// The value of the draggable progress bar. + /// If null, the progress bar is not being dragged. + final Duration? draggableValue; + + @override + bool shouldRepaint(CustomPainter painter) { + return true; + } + + @override + void paint(Canvas canvas, Size size) { + final baseOffset = size.height / 2 - barHeight / 2; + + canvas.drawRRect( + RRect.fromRectAndRadius( + Rect.fromPoints( + Offset(0.0, baseOffset), + Offset(size.width, baseOffset + barHeight), + ), + const Radius.circular(4.0), + ), + colors.backgroundPaint, + ); + if (!value.isInitialized) { + return; + } + final double playedPartPercent = (draggableValue != null + ? draggableValue!.inMilliseconds + : value.position.inMilliseconds) / + value.duration.inMilliseconds; + final double playedPart = + playedPartPercent > 1 ? size.width : playedPartPercent * size.width; + for (final DurationRange range in value.buffered) { + final double start = range.startFraction(value.duration) * size.width; + final double end = range.endFraction(value.duration) * size.width; + canvas.drawRRect( + RRect.fromRectAndRadius( + Rect.fromPoints( + Offset(start, baseOffset), + Offset(end, baseOffset + barHeight), + ), + const Radius.circular(4.0), + ), + colors.bufferedPaint, + ); + } + canvas.drawRRect( + RRect.fromRectAndRadius( + Rect.fromPoints( + Offset(0.0, baseOffset), + Offset(playedPart, baseOffset + barHeight), + ), + const Radius.circular(4.0), + ), + colors.playedPaint, + ); + + if (drawShadow) { + final Path shadowPath = Path() + ..addOval( + Rect.fromCircle( + center: Offset(playedPart, baseOffset + barHeight / 2), + radius: handleHeight, + ), + ); + + canvas.drawShadow(shadowPath, Colors.black, 0.2, false); + } + + canvas.drawCircle( + Offset(playedPart, baseOffset + barHeight / 2), + handleHeight, + colors.handlePaint, + ); + } +} + +extension RelativePositionExtensions on BuildContext { + Duration calcRelativePosition( + Duration videoDuration, + Offset globalPosition, + ) { + final box = findRenderObject()! as RenderBox; + final Offset tapPos = box.globalToLocal(globalPosition); + final double relative = (tapPos.dx / box.size.width).clamp(0, 1); + final Duration position = videoDuration * relative; + return position; + } +} diff --git a/lib/src/utils.dart b/lib/src/utils.dart deleted file mode 100644 index f1b216fa8..000000000 --- a/lib/src/utils.dart +++ /dev/null @@ -1,22 +0,0 @@ -String formatDuration(Duration position) { - final ms = position.inMilliseconds; - - int seconds = ms ~/ 1000; - final int hours = seconds ~/ 3600; - seconds = seconds % 3600; - var minutes = seconds ~/ 60; - seconds = seconds % 60; - - final hoursString = hours >= 10 ? '$hours' : hours == 0 ? '00' : '0$hours'; - - final minutesString = - minutes >= 10 ? '$minutes' : minutes == 0 ? '00' : '0$minutes'; - - final secondsString = - seconds >= 10 ? '$seconds' : seconds == 0 ? '00' : '0$seconds'; - - final formattedTime = - '${hoursString == '00' ? '' : hoursString + ':'}$minutesString:$secondsString'; - - return formattedTime; -} diff --git a/pubspec.yaml b/pubspec.yaml index 929e43657..8b8441c26 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,23 +1,24 @@ name: chewie description: A video player for Flutter with Cupertino and Material play controls -version: 0.10.4 -homepage: https://github.com/brianegan/chewie +version: 1.11.3 +homepage: https://github.com/fluttercommunity/chewie environment: - sdk: ">=2.7.0 <3.0.0" - flutter: ">=1.20.0 <2.0.0" + sdk: '>=3.6.0 <4.0.0' + flutter: ">=3.27.0" dependencies: - video_player: ">=0.10.12+5 <2.0.0" - cupertino_icons: ">=1.0.0 <2.0.0" - wakelock: ">=0.1.2 <1.0.0" - + cupertino_icons: ^1.0.8 flutter: sdk: flutter + provider: ^6.1.2 + video_player: ^2.9.3 + wakelock_plus: ^1.2.10 dev_dependencies: flutter_test: sdk: flutter + flutter_lints: ^5.0.0 flutter: uses-material-design: true diff --git a/test/uninitialized_controls_state_test.dart b/test/uninitialized_controls_state_test.dart new file mode 100644 index 000000000..0b538784a --- /dev/null +++ b/test/uninitialized_controls_state_test.dart @@ -0,0 +1,115 @@ +import 'package:chewie/chewie.dart'; +import 'package:chewie/src/center_play_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:video_player/video_player.dart'; + +List srcs = [ + "https://assets.mixkit.co/videos/preview/mixkit-spinning-around-the-earth-29351-large.mp4", + "https://assets.mixkit.co/videos/preview/mixkit-daytime-city-traffic-aerial-view-56-large.mp4", + "https://assets.mixkit.co/videos/preview/mixkit-a-girl-blowing-a-bubble-gum-at-an-amusement-park-1226-large.mp4" +]; + +main() { + testWidgets("MaterialControls state test", (WidgetTester tester) async { + // Build our app and trigger a frame. + var videoPlayerController = VideoPlayerController.networkUrl( + Uri.parse(srcs[0]), + ); + var materialControlsKey = GlobalKey(); + var chewieController = ChewieController( + videoPlayerController: videoPlayerController, + autoPlay: false, + looping: false, + customControls: MaterialControls( + key: materialControlsKey, + ), + ); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Chewie( + controller: chewieController, + ), + ), + ), + ); + + await tester.pump(); + + var playButton = find.byType(CenterPlayButton); + expect(playButton, findsOneWidget); + var btn = playButton.first; + var playButtonWidget = tester.widget(btn); + expect(playButtonWidget.isFinished, false); + }); + + testWidgets("CupertinoControls state test", (WidgetTester tester) async { + // Build our app and trigger a frame. + var videoPlayerController = VideoPlayerController.networkUrl( + Uri.parse(srcs[0]), + ); + var materialControlsKey = GlobalKey(); + var chewieController = ChewieController( + videoPlayerController: videoPlayerController, + autoPlay: false, + looping: false, + customControls: CupertinoControls( + key: materialControlsKey, + backgroundColor: Colors.black, + iconColor: Colors.white, + ), + ); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Chewie( + controller: chewieController, + ), + ), + ), + ); + + await tester.pump(); + + var playButton = find.byType(CenterPlayButton); + expect(playButton, findsOneWidget); + var btn = playButton.first; + var playButtonWidget = tester.widget(btn); + expect(playButtonWidget.isFinished, false); + }); + + testWidgets("MaterialDesktopControls state test", + (WidgetTester tester) async { + // Build our app and trigger a frame. + var videoPlayerController = VideoPlayerController.networkUrl( + Uri.parse(srcs[0]), + ); + var materialControlsKey = GlobalKey(); + var chewieController = ChewieController( + videoPlayerController: videoPlayerController, + autoPlay: false, + looping: false, + customControls: MaterialDesktopControls( + key: materialControlsKey, + ), + ); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Chewie( + controller: chewieController, + ), + ), + ), + ); + + await tester.pump(); + + var playButton = find.byType(CenterPlayButton); + expect(playButton, findsOneWidget); + var btn = playButton.first; + var playButtonWidget = tester.widget(btn); + expect(playButtonWidget.isFinished, false); + }); +}