diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..0c9f1e000 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,99 @@ +name: CI + +on: + pull_request: + types: [opened, synchronize, reopened] + paths-ignore: + - '**.md' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + # Define global environment variables for the workflow + # 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. + # Current minimum is set to Flutter 3.44. Make the next version the new minimum once the next + # stable version is released + FLUTTER_VERSION_MINIMUM_DEFAULT: "3.44.7" + FLUTTER_VERSION_LATEST_STABLE_CHANNEL_DEFAULT: "3.x" + +jobs: + setup_matrix: + name: Determine Flutter Test Versions # Name for the setup_matrix job + runs-on: ubuntu-latest + outputs: + flutter_versions_json: ${{ steps.set_versions.outputs.versions_json }} + flutter_version_minimum: ${{ steps.set_versions.outputs.version_min }} + steps: + - name: Determine Flutter versions + id: set_versions + run: | + MIN_VERSION_VALUE="${{ env.FLUTTER_VERSION_MINIMUM_DEFAULT }}" + LATEST_VERSION_VALUE="${{ env.FLUTTER_VERSION_LATEST_STABLE_CHANNEL_DEFAULT }}" + + echo "version_min=$MIN_VERSION_VALUE" >> $GITHUB_OUTPUT + echo "version_latest=$LATEST_VERSION_VALUE" >> $GITHUB_OUTPUT + + VERSIONS_JSON=$(jq -c --null-input '$ARGS.positional' --args "$MIN_VERSION_VALUE" "$LATEST_VERSION_VALUE") + echo "versions_json=$VERSIONS_JSON" >> $GITHUB_OUTPUT + + echo "Determined Min Version: $MIN_VERSION_VALUE" + echo "Determined Latest Version: $LATEST_VERSION_VALUE" + echo "Determined JSON: $VERSIONS_JSON" + + # 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. + needs: setup_matrix # Ensures this job runs after setup_matrix completes + runs-on: ubuntu-latest + env: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + FLUTTER_VERSION_MINIMUM: ${{ needs.setup_matrix.outputs.flutter_version_minimum }} + strategy: + matrix: + flutter-version: ${{ fromJSON(needs.setup_matrix.outputs.flutter_versions_json) }} + fail-fast: false + steps: + - name: 📚 Git Checkout + uses: actions/checkout@v6 + + - 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 + # Only continue on error if this is the job for the MINIMUM Flutter version + # This allows formatting issues to be warnings on older supported versions + # but enforces them on the latest stable or primary development version. + continue-on-error: "${{ matrix.flutter-version == env.FLUTTER_VERSION_MINIMUM }}" + + - 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@v7 + with: + fail_ci_if_error: false + handle_no_reports_found: true + # TODO: Remove the below once we have adequate tests for this library. + continue-on-error: true 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/.gitignore b/.gitignore index ccaa8833f..cc167331f 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,14 @@ build/ .project .classpath -.settings \ No newline at end of file +.settings + +coverage/* + +# AI Agents +.artifacts/ +.agent/ +.cursor/ +.windsurf/ +.aide/ +*.artifact.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 3672146a2..f5644f1e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,315 @@ +## [1.17.2] +* 🛠️ [#911](https://github.com/fluttercommunity/chewie/pull/911): Add documentation about pauseOnBackgroundTap feature. Thanks [Ortes](https://github.com/Ortes). +* ⬆️ [#930](https://github.com/fluttercommunity/chewie/pull/930): Add swipe-down to exit fullscreen. Thanks [Ortes](https://github.com/Ortes). +* 💬 [#948](https://github.com/fluttercommunity/chewie/pull/948): Multi-track subtitle selection. Thanks [Ortes](https://github.com/Ortes). +* 🔊 [#949](https://github.com/fluttercommunity/chewie/pull/949): Multi-track audio selection. Thanks [Ortes](https://github.com/Ortes). +* ⌨️ [#953](https://github.com/fluttercommunity/chewie/pull/953): YouTube-style seek indicator for keyboard seeking. Thanks [Ortes](https://github.com/Ortes). +* 🔄 [#963](https://github.com/fluttercommunity/chewie/pull/963): Add `ChewieController.swapVideoSource` (in-place video source replacement). Thanks [Ortes](https://github.com/Ortes). +* 👆 [#964](https://github.com/fluttercommunity/chewie/pull/964): YouTube-style tap gestures (double-tap seek zones on mobile, double-click fullscreen on desktop). Thanks [Ortes](https://github.com/Ortes). +* ⚙️ [#965](https://github.com/fluttercommunity/chewie/pull/965): Add typed video-quality selection to the options menu of all three skins. Thanks [Ortes](https://github.com/Ortes). + +## [1.17.1] +* 🛠️ [#947](https://github.com/fluttercommunity/chewie/pull/947): Add `disableFullScreenRoute` to let the embedder own fullscreen. Thanks [Ortes](https://github.com/Ortes). +* 🖱️ [#954](https://github.com/fluttercommunity/chewie/pull/954): Auto-hide the mouse cursor while idle in fullscreen. Thanks [Ortes](https://github.com/Ortes). +* ⌨️ [#955](https://github.com/fluttercommunity/chewie/pull/955): Toggle fullscreen with the `F` key on desktop. Thanks [Ortes](https://github.com/Ortes). +* 🕒 [#956](https://github.com/fluttercommunity/chewie/pull/956): Show a hover-time indicator on the progress bar. Thanks [Ortes](https://github.com/Ortes). +* 🛠️ [#957](https://github.com/fluttercommunity/chewie/pull/957): Show the controls when play/pause comes from outside the controls. Thanks [Ortes](https://github.com/Ortes). + +## [1.17.0] +* 📺 [#961](https://github.com/fluttercommunity/chewie/pull/961): Casting / screen-mirroring support. Chewie now ships the cast UI — a cast button in the Material, Material desktop and Cupertino skins, a device picker, and an overlay in place of the video while a session is live — plus the local↔remote handover in both directions. The sender itself is supplied by the app through the new `ChewieCastController` abstraction, so a pure-Flutter package does not pull the Google Cast SDK and its native dependencies into every consumer. New `ChewieController` options: `castController`, `castMedia`, `allowCasting`, `castTranslations` and `castOverlayBuilder`. See the [Casting](README.md#-casting) section. Thanks [a1rwulf](https://github.com/a1rwulf). + +## [1.16.3] +* 🐛 [#970](https://github.com/fluttercommunity/chewie/pull/970): fix(web): fullscreen pops the host route on iPhone Safari (no Fullscreen API). Thanks [Ortes](https://github.com/Ortes). + +## [1.16.2] +* ⬆️ [#959](https://github.com/fluttercommunity/chewie/pull/959): Add YouTube-style chapters to the progress bar. Pass `ChewieController.chapters` to split the bar into chapter segments and show the hovered/scrubbed chapter title above it. Thanks [Ortes](https://github.com/Ortes). +* 🛠️ [#968](https://github.com/fluttercommunity/chewie/pull/968): Reformat `_PlaybackSpeedDialog` to satisfy `dart format`. Thanks [Ortes](https://github.com/Ortes). + +## [1.16.1] +* ✨ [#962](https://github.com/fluttercommunity/chewie/pull/962): Expose `showPlayButton` on `ChewieController` to allow showing or hiding the center play button. Thanks [ibrahim-iqbal](https://github.com/ibrahim-iqbal). + +## [1.16.0+1] +* Fixed lingering `dart lint` issues from previous release. + +## [1.16.0] +* 🛠️ [#966](https://github.com/fluttercommunity/chewie/pull/966): Fix the progress bar handle snapping back to the previous position for the duration of a seek (most visible on iOS, where an exact seek is slow to complete). The requested position is now painted until the controller reports it, and playback resumes only once the seek has landed. Thanks [Ortes](https://github.com/Ortes). +* ⬆️ [#967](https://github.com/fluttercommunity/chewie/pull/967): Flutter 3.47 maintenance. Thanks [diegotori](https://github.com/diegotori). +* **BREAKING CHANGE**: Library now requires at least Flutter and Dart versions `3.44.0` and `3.11` or higher respectively. + +## [1.15.0] +* 🌐 [#946](https://github.com/fluttercommunity/chewie/pull/946): Web: enter the browser's native (OS-level) fullscreen via the Fullscreen API instead of only expanding the Flutter view inside the browser window. Pressing Escape to leave browser fullscreen also exits Chewie's fullscreen. Controlled by the new `ChewieController.useNativeFullScreenOnWeb` flag (defaults to `true`; no effect on non-web platforms). Thanks [Ortes](https://github.com/Ortes). +* 🖱️ [#950](https://github.com/fluttercommunity/chewie/pull/950): Show click cursor on hover over Material controls and progress bar. Thanks [Ortes](https://github.com/Ortes). +* 💬 [#958](https://github.com/fluttercommunity/chewie/pull/958): Render inline markup in subtitle cue text. Thanks [Ortes](https://github.com/Ortes). +* ⬆️ [#960](https://github.com/fluttercommunity/chewie/pull/960): Flutter 3.44 upgrade and CI improvements. Thanks [diegotori](https://github.com/diegotori). +* **BREAKING CHANGE**: Library now requires at least Flutter and Dart versions `3.41.0` and `3.11` or higher respectively. + +## [1.14.1] +* 🛠️ [#945](https://github.com/fluttercommunity/chewie/pull/945): Flutter 3.38 downgrade. Thanks [diegotori](https://github.com/diegotori). +* Library now supports Flutter and Dart versions `3.38.0` and `3.10` or higher respectively, restoring previous compatibility. + +## [1.14.0] +* 🛠️ [#944](https://github.com/fluttercommunity/chewie/pull/944): Flutter 3.41 Upgrade. Thanks [diegotori](https://github.com/diegotori). +* **BREAKING CHANGE**: Library now requires at least Flutter version `3.41.0` or higher due to + `wakelock_plus`'s upgrade to version `1.6.0`, which now requires this minimum Flutter version. + +## [1.13.1] +* 🛠️ [#940](https://github.com/fluttercommunity/chewie/pull/940): fix: add mounted check in _startHideTimer to prevent setState after dispose. Thanks [Cleparr](https://github.com/Cleparr). + +## [1.13.0] +* 🛠️ [#927](https://github.com/fluttercommunity/chewie/pull/927): Fix(web): Keep playback when entering/exiting fullscreen on web. Thanks [victorspringer](https://github.com/victorspringer). +* **BREAKING CHANGE**: Library now requires at least Flutter version `3.32.0` or higher. + +## [1.12.1] +* 🛠️ [#920](https://github.com/fluttercommunity/chewie/pull/920): Fix zoomAndPan not having an effect. Thanks [abalmagd](https://github.com/abalmagd). + +## [1.12.0] +* 🛠️ [#923](https://github.com/fluttercommunity/chewie/pull/923): Flutter 3.29 minimum version. Thanks [diegotori](https://github.com/diegotori). +* **BREAKING CHANGE**: Library now requires at least Flutter version `3.29.0` or higher. + +## [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 +317,6 @@ * Update Flutter SDK * Update `wakelock` dependency - ## 0.10.3+1 * Format using `dartfmt -w .` for pub.dev diff --git a/README.md b/README.md index d7dddfd08..da4727a9d 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,66 @@ # 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. 🔖 [Chapters](#-chapters) +9. 📺 [Casting](#-casting) +10. 🧪 [Example](#-example) +11. ⏪ [Migrating from Chewie < 0.9.0](#-migrating-from-chewie--090) +12. 🗺️ [Roadmap](#%EF%B8%8F-roadmap) +13. ⚠️ [Android warning](#%EF%B8%8F-android-warning) +14. 📱 [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 -## Demo +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](https://github.com/brianegan/chewie/raw/master/assets/chewie_demo.gif) +However, this cannot be guaranteed either due to major changes between Flutter versions or +this library's dependencies. Should that occur, future updates will be released as major or minor +versions as needed. -## Installation +## 🖼️ Preview -In your `pubspec.yaml` file within your Flutter Project: +| MaterialControls | MaterialDesktopControls | +|:-------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------------:| +| ![](https://github.com/brianegan/chewie/raw/master/assets/MaterialControls.png) | ![](https://github.com/brianegan/chewie/raw/master/assets/MaterialDesktopControls.png) | + +### 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 +68,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 +90,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 +100,329 @@ 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`)! + +### `pauseOnBackgroundTap` (Tap Anywhere to Pause/Play) + +The `pauseOnBackgroundTap` feature allows the video to be paused or played by tapping anywhere on the video player area. +This eliminates the need for users to tap the play/pause icon in the center of the video. + +This feature can be enabled by setting the `pauseOnBackgroundTap` property in the ChewieController. +When enabled, tapping anywhere on the video will toggle between play and pause states. + +```dart +final chewieController = ChewieController( + videoPlayerController: videoPlayerController, + pauseOnBackgroundTap: true, // Allow tapping anywhere on the video to pause/play +); +``` + +### 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`, restyle the default subtitle box with `subtitleStyle`, or replace it entirely with 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', +), +``` + +### Markup in Subtitle Text + +Cue text extracted from WebVTT or SubRip files often carries inline markup, and Chewie renders it for you: + +```dart +Subtitle( + index: 0, + start: Duration.zero, + end: const Duration(seconds: 10), + text: 'The law is the law, Mr. Hancock.', +), +``` + +``, ``, `` and `` are applied on top of your text style. The other WebVTT cue tags — ``, ``, ``, ``/`` and timestamp tags — are dropped while their text is kept, and escapes such as `&` are decoded. A tag that opens on one line and closes on the next works too. + +Parsing is lenient, so cue text is never mangled: `5 < 10` and `<3` are shown as written, an unclosed tag simply runs to the end of the cue, and a stray closing tag is ignored. If you would rather show cue text exactly as it arrives, set `subtitleStyle: SubtitleStyle(renderMarkup: false)`. + +### Styling Subtitles + +`subtitleStyle` changes how the default subtitle box looks without giving up markup rendering: + +```dart +ChewieController( + videoPlayerController: _videoPlayerController, + subtitleStyle: const SubtitleStyle( + textStyle: TextStyle(fontSize: 22, color: Colors.amber), + textAlign: TextAlign.center, + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration(color: Colors.black54), + ), +); +``` + +Leaving `textStyle.color` unset inherits the colour from the surrounding `DefaultTextStyle`, which is what Chewie does by default. + +### Customizing Subtitles + +Reach for `subtitleBuilder` when you need to build the whole widget yourself. It receives the cue exactly as you supplied it — markup and all — and Chewie's own rendering, including `subtitleStyle`, is skipped. Run the cue through `parseSubtitleMarkup` to keep markup working: + +```dart +subtitleBuilder: (context, subtitle) => Container( + padding: const EdgeInsets.all(10.0), + child: Text.rich( + subtitle is String + ? parseSubtitleMarkup( + subtitle, + style: const TextStyle(color: Colors.white), + ) + : TextSpan(text: subtitle.toString()), + ), +), +``` + +## 🔖 Chapters + +Pass a list of `ChewieChapter`s, sorted by ascending start time, to draw the progress bar as YouTube-style chapter segments. Hovering or scrubbing the bar shows the pointed chapter's title above it: + +```dart +final chewieController = ChewieController( + videoPlayerController: videoPlayerController, + chapters: const [ + ChewieChapter(title: 'Intro', start: Duration.zero), + ChewieChapter(title: 'First topic', start: Duration(minutes: 1)), + ChewieChapter(title: 'Conclusion', start: Duration(minutes: 4)), + ], +); +``` + +When `chapters` is empty (the default), the progress bar is rendered as a single continuous segment, as before. + +## 📺 Casting + +Chewie can hand playback over to a cast receiver — a Chromecast, an Android TV, +anything you can talk to — and drive it from the same controls. + +What Chewie provides is the UI and the handover: a cast button in every skin, a +device picker, an overlay in place of the video while a session is live, and the +local↔remote transitions in both directions. What it deliberately does **not** +provide is a sender. A pure-Flutter video player package has no business +dragging the Google Cast SDK, its native dependencies and its permission +requirements into every app that just wants play controls — so you supply the +backend, and Chewie stays agnostic about which one. + +### Wiring it up + +Implement `ChewieCastController` against the sender of your choice, then hand it +to the `ChewieController` along with the media the receiver should fetch: + +```dart +final castController = MyCastController(); // your ChewieCastController + +_chewieController = ChewieController( + videoPlayerController: _videoPlayerController, + castController: castController, + castMedia: const CastMedia( + url: 'https://example.com/video.mp4', + mimeType: 'video/mp4', + title: 'Big Buck Bunny', + ), +); +``` + +`castMedia` is required whenever `castController` is set. Chewie cannot derive +it from the `VideoPlayerController`: the receiver fetches the stream itself, so +the URL has to be reachable **from the TV**, and a `file://` or asset source has +no remote equivalent at all. + +### What happens on connect + +When your controller moves its `connectionState` to +`CastConnectionState.connected`, Chewie: + +1. pauses the local player and notes where it was, +2. calls `load(castMedia, startAt: , autoPlay: )`, +3. replaces the video surface with the casting overlay, +4. routes play, pause, seek, volume and the progress bar to the receiver. + +On `disconnected` it runs the same trip in reverse, seeking the local player to +wherever the receiver got to and resuming if it was playing. Chewie listens to +your controller but never disposes it, so one instance can serve many videos. + +### Implementing a backend + +`ChewieCastController` is a `ChangeNotifier` with three groups of members: +discovery (`devices`, `isDiscovering`, `startDiscovery`, `stopDiscovery`), +session (`connectionState`, `connectedDevice`, `connect`, `disconnect`) and +remote playback (`value`, `load`, `play`, `pause`, `seekTo`, `setVolume`, +`setPlaybackSpeed`). Call `notifyListeners()` whenever any of them change — +every piece of cast UI rebuilds off those notifications. + +`value` is a `VideoPlayerValue`, the same type the local player reports, which is +what lets one set of controls render a remote session identically to a local +one. Only `duration`, `position`, `buffered`, `isPlaying`, `isBuffering`, +`isInitialized`, `volume` and `playbackSpeed` are read; leave the rest at their +defaults. + +The example app ships a `DemoCastController` that fakes discovery, a connection +delay and a ticking receiver, so you can see the whole flow without a +Chromecast on the desk. + +### Options + +| Option | Description | +| --- | --- | +| `castController` | Your backend. Casting is off entirely when this is null. | +| `castMedia` | What the receiver should play. Required alongside `castController`. | +| `allowCasting` | Hides the cast button without tearing the backend out. Defaults to `true`. | +| `castTranslations` | Strings for the button, picker and overlay. | +| `castOverlayBuilder` | Replaces the default "Casting to …" overlay. | +| `additionalControls` | Extra widgets for the control bar itself, for controls that cannot be an options-sheet row. | + +### Extra control-bar buttons + +`additionalOptions` adds rows to the options sheet. When a control has to be a +*widget* rather than a menu entry — a platform view, say — put it in the bar +itself: + +```dart +ChewieController( + videoPlayerController: videoPlayerController, + additionalControls: (context) => const [AirPlayButton()], +); +``` + +They sit alongside the built-in buttons and inherit the bar's show/hide +behaviour, so they fade with the rest of the controls instead of floating over +the video. + +The motivating case is AirPlay: Apple exposes no way to select a route +programmatically, so the button must be UIKit's own `AVRoutePickerView`. + +## 🧪 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 +446,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 +- [x] Screen-Mirroring / Casting (backend-agnostic; see [Casting](#-casting)) + + +## ⚠️ 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..cfe270177 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,18 +1,24 @@ +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 + - build/** + - android/** + - ios/** + - web/** + - windows/** + - macos/** + - linux/** + 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..9843e6bb6 --- /dev/null +++ b/example/analysis_options.yaml @@ -0,0 +1,19 @@ +include: package:flutter_lints/flutter.yaml + +analyzer: + language: + strict-raw-types: true + exclude: + - lib/generated_plugin_registrant.dart + - build/** + - android/** + - ios/** + - web/** + - windows/** + - macos/** + - linux/** + +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..84fedd3aa 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 35 - 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..638b434e2 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,22 +3,30 @@ 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 */; }; + 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 +41,25 @@ /* 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 = ""; }; + 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 = ""; }; 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 = ""; }; + 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 +67,47 @@ 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 = ( ); 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 */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 22109C890749BA0B4E7C88B0 /* Frameworks */ = { isa = PBXGroup; children = ( - 891E43F580F30980509C464D /* libPods-Runner.a */, + 161F52A253A901BB69307277 /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; }; + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -89,8 +125,9 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, - E60E3BF33DDF10E510884550 /* Pods */, - 345CF767F333A6932C0BBB1C /* Frameworks */, + 331C8082294A63A400263BE5 /* RunnerTests */, + 07E76744EA73A063120BFE5C /* Pods */, + 22109C890749BA0B4E7C88B0 /* Frameworks */, ); sourceTree = ""; }; @@ -98,6 +135,7 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -105,51 +143,51 @@ 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 = ( + 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 +204,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 +232,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,48 +259,53 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + 2261669A503474CE180D7658 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); - inputPaths = ( + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - name = "Thin Binary"; - outputPaths = ( + 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 = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; }; - 8882ECDD31D097CE5A94142C /* [CP] Embed Pods Frameworks */ = { + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${PODS_ROOT}/../Flutter/Flutter.framework", + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); - name = "[CP] Embed Pods Frameworks"; + name = "Thin Binary"; outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 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 +316,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -276,18 +332,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 +379,132 @@ /* 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; + 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; + 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; + 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 +514,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 +534,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 +549,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 +561,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 +571,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 +591,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 +600,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 +615,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 +638,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 +684,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..e3773d42e 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ - - - - + + + + + + @@ -63,11 +73,9 @@ ReferencedContainer = "container:Runner.xcodeproj"> - - -#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..65d2a98bd --- /dev/null +++ b/example/lib/app/app.dart @@ -0,0 +1,460 @@ +import 'package:chewie/chewie.dart'; +import 'package:chewie_example/app/demo_cast_controller.dart'; +import 'package:chewie_example/app/demo_control.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; + + /// A simulated cast backend. Chewie ships no sender of its own, so the app + /// supplies one; see [DemoCastController] for what implementing it involves. + /// The app owns it, which is why it outlives the ChewieControllers below. + final DemoCastController _castController = DemoCastController(); + + @override + void initState() { + super.initState(); + initializePlayer(); + } + + @override + void dispose() { + _videoPlayerController1.dispose(); + _videoPlayerController2.dispose(); + // After a quality change the active controller is the one chewie holds + // (dispose() is idempotent, so overlap with the lines above is fine). + _chewieController?.videoPlayerController.dispose(); + _chewieController?.dispose(); + _castController.dispose(); + super.dispose(); + } + + // The commondatastorage.googleapis.com samples this used to point at now + // return 403, which surfaces on iOS as the unhelpful "You do not have + // permission to access the requested resource" (OSStatus -12660). + List srcs = [ + "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4", + "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8", + ]; + + bool _hasPlayers = false; + + Future initializePlayer() async { + // Hold the outgoing players so they can be torn down once the new ones are + // on screen. Without this every source switch leaks two initialized + // AVPlayers, and with external playback enabled each one is a candidate + // for an AirPlay route - so picking a route can hand the TV a stale video + // rather than the one being watched. + final previous = _hasPlayers + ? [ + _videoPlayerController1, + _videoPlayerController2, + ] + : const []; + + _videoPlayerController1 = VideoPlayerController.networkUrl( + Uri.parse(srcs[currPlayIndex]), + ); + _videoPlayerController2 = VideoPlayerController.networkUrl( + Uri.parse(srcs[currPlayIndex]), + ); + await Future.wait([ + _videoPlayerController1.initialize(), + _videoPlayerController2.initialize(), + ]); + _hasPlayers = true; + _createChewieController(); + setState(() {}); + + for (final controller in previous) { + await controller.dispose(); + } + } + + 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), + // ), + ), + Subtitle( + index: 0, + start: const Duration(seconds: 20), + end: const Duration(seconds: 30), + // Markup like this turns up in real WebVTT and SubRip files, and is + // rendered without any help from a subtitleBuilder. + text: + 'Cue text can be italic, bold or ' + 'coloured.', + ), + ]; + + _chewieController = ChewieController( + videoPlayerController: _videoPlayerController1, + autoPlay: true, + zoomAndPan: true, + looping: true, + + // Casting. The receiver fetches the URL itself, so it has to be one the + // TV can reach — not the local data source — and served with CORS + // headers, which the Google Default Media Receiver requires. + castController: _castController, + castMedia: CastMedia( + url: srcs[currPlayIndex], + mimeType: srcs[currPlayIndex].endsWith('.m3u8') + ? 'application/x-mpegURL' + : 'video/mp4', + title: 'Chewie demo', + subtitle: 'Playing from the example app', + ), + progressIndicatorDelay: bufferDelay != null + ? Duration(milliseconds: bufferDelay!) + : null, + + // A control the app supplies, sitting in the bar beside Chewie's own. + // See [DemoBookmarkButton] for how it matches whichever skin it is in. + additionalControls: (_) => const [DemoBookmarkButton()], + // A real app would list renditions of the same video ('480p', '1080p', + // ...); the demo uses distinct videos so the switch is visible. The id + // is opaque to chewie — here it carries the URL of each rendition. + videoQualities: [ + for (final (index, src) in srcs.indexed) + VideoQuality(id: src, label: 'Video ${index + 1}'), + ], + activeVideoQualityId: srcs[currPlayIndex], + onVideoQualityChanged: (quality) { + _chewieController?.swapVideoSource( + VideoPlayerController.networkUrl(Uri.parse(quality.id as String)), + ); + }, + + additionalOptions: (context) { + return [ + OptionItem( + onTap: (context) => toggleVideo(), + iconData: Icons.live_tv_sharp, + title: 'Toggle Video Src', + ), + ]; + }, + subtitle: Subtitles(subtitles), + showSubtitles: true, + subtitleStyle: const SubtitleStyle( + textStyle: TextStyle(fontSize: 20, color: Colors.white), + ), + + // subtitleBuilder replaces the default box entirely, so subtitleStyle no + // longer applies and cue markup is yours to handle — parseSubtitleMarkup + // is the same parser chewie uses: + // + // subtitleBuilder: (context, dynamic subtitle) => Container( + // padding: const EdgeInsets.all(10.0), + // child: Text.rich( + // subtitle is InlineSpan + // ? subtitle + // : parseSubtitleMarkup( + // subtitle.toString(), + // style: const TextStyle(color: Colors.white), + // ), + // ), + // ), + 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/demo_cast_controller.dart b/example/lib/app/demo_cast_controller.dart new file mode 100644 index 000000000..d506c61ad --- /dev/null +++ b/example/lib/app/demo_cast_controller.dart @@ -0,0 +1,182 @@ +import 'dart:async'; + +import 'package:chewie/chewie.dart'; +import 'package:video_player/video_player.dart'; + +/// A simulated cast backend, so the example demonstrates the casting UI +/// end-to-end without pulling the Google Cast SDK into a demo app. +/// +/// It fakes discovery, a connection delay and a receiver whose position ticks +/// forward on its own. Swap it for a real [ChewieCastController] — one backed +/// by the Cast SDK, a Dart CASTv2 sender, or anything else — and nothing else +/// in the app has to change. +class DemoCastController extends ChewieCastController { + static const _fakeDevices = [ + CastDevice( + id: 'living-room', + name: 'Living Room TV', + modelName: 'Chromecast Ultra', + ), + CastDevice(id: 'kitchen', name: 'Kitchen Display', modelName: 'Nest Hub'), + ]; + + final List _devices = []; + + CastConnectionState _connectionState = CastConnectionState.disconnected; + CastDevice? _connectedDevice; + bool _isDiscovering = false; + String? _errorDescription; + VideoPlayerValue _value = const VideoPlayerValue.uninitialized(); + + Timer? _discoveryTimer; + Timer? _connectTimer; + Timer? _tickTimer; + + @override + List get devices => List.unmodifiable(_devices); + + @override + bool get isDiscovering => _isDiscovering; + + @override + CastConnectionState get connectionState => _connectionState; + + @override + CastDevice? get connectedDevice => _connectedDevice; + + @override + String? get errorDescription => _errorDescription; + + @override + VideoPlayerValue get value => _value; + + @override + Future startDiscovery() async { + _isDiscovering = true; + _devices.clear(); + notifyListeners(); + + // Trickle the devices in, the way a real mDNS scan resolves them. + _discoveryTimer?.cancel(); + var index = 0; + _discoveryTimer = Timer.periodic(const Duration(milliseconds: 600), ( + timer, + ) { + if (index >= _fakeDevices.length) { + timer.cancel(); + _isDiscovering = false; + notifyListeners(); + return; + } + _devices.add(_fakeDevices[index++]); + notifyListeners(); + }); + } + + @override + Future stopDiscovery() async { + _discoveryTimer?.cancel(); + _discoveryTimer = null; + _isDiscovering = false; + notifyListeners(); + } + + @override + Future connect(CastDevice device) async { + _errorDescription = null; + _connectedDevice = device; + _connectionState = CastConnectionState.connecting; + notifyListeners(); + + // Real receivers take a moment to accept a session; Chewie shows the + // pulsing cast button until this lands. + _connectTimer?.cancel(); + _connectTimer = Timer(const Duration(seconds: 2), () { + _connectionState = CastConnectionState.connected; + notifyListeners(); + }); + } + + @override + Future disconnect() async { + _connectTimer?.cancel(); + _tickTimer?.cancel(); + + _connectionState = CastConnectionState.disconnecting; + notifyListeners(); + + _connectionState = CastConnectionState.disconnected; + _connectedDevice = null; + notifyListeners(); + } + + @override + Future load( + CastMedia media, { + Duration startAt = Duration.zero, + bool autoPlay = true, + }) async { + _value = VideoPlayerValue( + // A real receiver reports the duration once it has parsed the stream; + // pretend we already know it. + duration: const Duration(minutes: 10), + position: startAt, + isInitialized: true, + isPlaying: autoPlay, + ); + notifyListeners(); + + if (autoPlay) { + _startTicking(); + } + } + + @override + Future play() async { + _value = _value.copyWith(isPlaying: true); + _startTicking(); + notifyListeners(); + } + + @override + Future pause() async { + _tickTimer?.cancel(); + _value = _value.copyWith(isPlaying: false); + notifyListeners(); + } + + @override + Future seekTo(Duration position) async { + _value = _value.copyWith(position: position); + notifyListeners(); + } + + @override + Future setVolume(double volume) async { + _value = _value.copyWith(volume: volume); + notifyListeners(); + } + + @override + void dispose() { + _discoveryTimer?.cancel(); + _connectTimer?.cancel(); + _tickTimer?.cancel(); + super.dispose(); + } + + /// Walks the fake receiver's position forward once a second. + void _startTicking() { + _tickTimer?.cancel(); + _tickTimer = Timer.periodic(const Duration(seconds: 1), (_) { + final next = _value.position + const Duration(seconds: 1); + if (next >= _value.duration) { + _tickTimer?.cancel(); + _value = _value.copyWith(position: _value.duration, isPlaying: false); + } else { + _value = _value.copyWith(position: next); + } + notifyListeners(); + }); + } +} diff --git a/example/lib/app/demo_control.dart b/example/lib/app/demo_control.dart new file mode 100644 index 000000000..5c935b08f --- /dev/null +++ b/example/lib/app/demo_control.dart @@ -0,0 +1,54 @@ +import 'package:chewie/chewie.dart'; +import 'package:flutter/material.dart'; + +/// A control the app hands to Chewie's bar, demonstrating +/// [ChewieController.additionalControls] and [ChewieControlStyle]. +/// +/// What it does is deliberately trivial — it toggles a bookmark nothing reads. +/// What it shows is the pattern a real control follows: take the surrounding +/// bar's size, tint, padding and chrome rather than hard-coding values that +/// will suit one skin and look wrong in the others. +/// +/// Worth trying on all three skins. The same widget draws a 24px glyph on the +/// Material bars, a 16px one on Cupertino's frosted pill, and takes a 48x48 +/// tap target on desktop, without being told which bar it landed in. It also +/// fades with the rest of the controls, because it sits inside them. +class DemoBookmarkButton extends StatefulWidget { + const DemoBookmarkButton({super.key}); + + @override + State createState() => _DemoBookmarkButtonState(); +} + +class _DemoBookmarkButtonState extends State { + bool _bookmarked = false; + + @override + Widget build(BuildContext context) { + final style = ChewieControlStyle.maybeOf(context); + + final Widget button = GestureDetector( + onTap: () => setState(() => _bookmarked = !_bookmarked), + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: Padding( + // The bar's padding rather than one of our choosing. A bar is often + // shorter than its nominal height once the progress bar has taken + // its share, so padding it does not itself use leaves the glyph less + // room than it needs and it paints outside its box. + padding: style?.padding ?? const EdgeInsets.all(8), + child: Icon( + _bookmarked ? Icons.bookmark : Icons.bookmark_border, + size: style?.iconSize ?? 24, + color: _bookmarked + ? Theme.of(context).colorScheme.primary + : style?.iconColor ?? Colors.white, + ), + ), + ), + ); + + // Whatever the bar wraps its own buttons in. + return style?.decorate(button) ?? button; + } +} 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..fd774aa85 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.12.0 <4.0.0' + flutter: ">=3.44.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.14.0 dev_dependencies: flutter_test: sdk: flutter - + flutter_lints: ^6.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..f3c0c64cd 100644 --- a/lib/chewie.dart +++ b/lib/chewie.dart @@ -1,4 +1,11 @@ -library chewie; +library; +export 'src/cast/index.dart'; 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'; +export 'src/subtitle_markup.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/cast/cast_connection_state.dart b/lib/src/cast/cast_connection_state.dart new file mode 100644 index 000000000..cc00f272c --- /dev/null +++ b/lib/src/cast/cast_connection_state.dart @@ -0,0 +1,29 @@ +/// Where a cast session currently is between "nothing" and "playing on the TV". +enum CastConnectionState { + /// No session. The cast button is idle and playback is local. + disconnected, + + /// A session is being established. The cast button pulses; playback is still + /// local until [connected] arrives. + connecting, + + /// A session is live. Chewie's controls drive the receiver, and the video + /// surface is replaced by the casting overlay. + connected, + + /// The session is being torn down. Playback returns to local once + /// [disconnected] arrives. + disconnecting, +} + +extension CastConnectionStateX on CastConnectionState { + bool get isConnected => this == CastConnectionState.connected; + + bool get isDisconnected => this == CastConnectionState.disconnected; + + /// True while a session is being set up or torn down, i.e. neither fully on + /// nor fully off. Useful for showing a busy affordance. + bool get isTransitioning => + this == CastConnectionState.connecting || + this == CastConnectionState.disconnecting; +} diff --git a/lib/src/cast/cast_device.dart b/lib/src/cast/cast_device.dart new file mode 100644 index 000000000..2b12c5e9b --- /dev/null +++ b/lib/src/cast/cast_device.dart @@ -0,0 +1,34 @@ +import 'package:flutter/foundation.dart'; + +/// A playback target Chewie can hand the video off to — a Chromecast, an +/// Android TV, a smart TV speaker group. +/// +/// Discovered and reported by a [ChewieCastController]; Chewie only ever +/// displays these and passes them back to `connect`. +@immutable +class CastDevice { + const CastDevice({required this.id, required this.name, this.modelName}); + + /// Stable identifier for the device, unique among the devices a single + /// backend reports. Equality is based on this alone, so it has to survive + /// a device dropping out of discovery and coming back. + final String id; + + /// The name to show the user, e.g. `Living Room TV`. + final String name; + + /// The hardware model, e.g. `Chromecast Ultra`. Shown as a subtitle in the + /// device picker when present. + final String? modelName; + + @override + bool operator ==(Object other) => + identical(this, other) || (other is CastDevice && other.id == id); + + @override + int get hashCode => id.hashCode; + + @override + String toString() => + 'CastDevice(id: $id, name: $name, modelName: $modelName)'; +} diff --git a/lib/src/cast/cast_media.dart b/lib/src/cast/cast_media.dart new file mode 100644 index 000000000..e1158ea3f --- /dev/null +++ b/lib/src/cast/cast_media.dart @@ -0,0 +1,96 @@ +import 'package:flutter/foundation.dart'; + +/// What Chewie asks the cast backend to play on the remote device. +/// +/// Chewie cannot derive this from the `VideoPlayerController` on its own: the +/// receiver needs an absolute URL it can reach itself over the network, plus +/// metadata the local player never has (title, artwork). A `file://` or asset +/// data source has no remote equivalent at all, which is why this is supplied +/// explicitly rather than guessed. +@immutable +class CastMedia { + const CastMedia({ + required this.url, + this.mimeType, + this.title, + this.subtitle, + this.posterUrl, + this.headers = const {}, + this.isLive = false, + }); + + /// Absolute URL of the stream, reachable from the receiver device — not from + /// the phone. `localhost` and LAN-only URLs will fail on the receiver. + final String url; + + /// e.g. `video/mp4`, `application/x-mpegURL`. Backends that need a content + /// type and get none here typically fall back to sniffing the extension. + final String? mimeType; + + /// Title shown on the TV while casting. + final String? title; + + /// Secondary line shown on the TV, e.g. a series or chapter name. + final String? subtitle; + + /// Artwork shown on the TV, usually while audio-only or buffering. + final String? posterUrl; + + /// Extra HTTP headers the receiver should send when fetching [url], for + /// signed or authenticated streams. Not every backend supports these. + final Map headers; + + /// Whether this is a live stream. Receivers use it to pick a live UI and to + /// skip seek affordances. + final bool isLive; + + CastMedia copyWith({ + String? url, + String? mimeType, + String? title, + String? subtitle, + String? posterUrl, + Map? headers, + bool? isLive, + }) { + return CastMedia( + url: url ?? this.url, + mimeType: mimeType ?? this.mimeType, + title: title ?? this.title, + subtitle: subtitle ?? this.subtitle, + posterUrl: posterUrl ?? this.posterUrl, + headers: headers ?? this.headers, + isLive: isLive ?? this.isLive, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is CastMedia && + other.url == url && + other.mimeType == mimeType && + other.title == title && + other.subtitle == subtitle && + other.posterUrl == posterUrl && + other.isLive == isLive && + mapEquals(other.headers, headers); + } + + @override + int get hashCode => Object.hash( + url, + mimeType, + title, + subtitle, + posterUrl, + isLive, + Object.hashAllUnordered( + headers.entries.map((e) => Object.hash(e.key, e.value)), + ), + ); + + @override + String toString() => 'CastMedia(url: $url, title: $title)'; +} diff --git a/lib/src/cast/chewie_cast_controller.dart b/lib/src/cast/chewie_cast_controller.dart new file mode 100644 index 000000000..f71a4efb0 --- /dev/null +++ b/lib/src/cast/chewie_cast_controller.dart @@ -0,0 +1,132 @@ +import 'package:chewie/src/cast/cast_connection_state.dart'; +import 'package:chewie/src/cast/cast_device.dart'; +import 'package:chewie/src/cast/cast_media.dart'; +import 'package:flutter/foundation.dart'; +import 'package:video_player/video_player.dart'; + +/// The seam between Chewie and whatever actually talks to a cast receiver. +/// +/// Chewie ships the UI and the local/remote handover; it deliberately ships no +/// sender implementation, so that a pure-Flutter video player package does not +/// drag the Google Cast SDK — and its native, mobile-only, permission-hungry +/// dependencies — into every app that just wants play controls. +/// +/// Implement this against the sender of your choice (the Cast SDK bindings, a +/// pure-Dart CASTv2 sender, AirPlay, DLNA — Chewie does not care which), then +/// hand the instance to `ChewieController.castController`. +/// +/// The contract, in short: +/// +/// * Call [notifyListeners] whenever [devices], [connectionState], +/// [connectedDevice], [value] or [errorDescription] change. Chewie drives +/// every visible piece of cast UI off those notifications. +/// * [connectionState] is the single source of truth for whether Chewie is +/// casting. Moving it to [CastConnectionState.connected] is what triggers the +/// handover: Chewie pauses the local player and calls [load] with the +/// position it left off at. Moving it back to +/// [CastConnectionState.disconnected] hands playback back, seeking the local +/// player to wherever the receiver got to. +/// * You own this object's lifetime. Chewie listens to it but never disposes +/// it, so one controller can outlive many [ChewieController]s and survive +/// navigation between videos. +abstract class ChewieCastController extends ChangeNotifier { + /// Receivers currently known to be on the network. + /// + /// May be empty while discovery is still running — the picker shows a + /// searching state rather than "none found" as long as [isDiscovering] is + /// true. + List get devices; + + /// Whether a scan for receivers is in progress. + bool get isDiscovering; + + /// Where the session currently is. See [CastConnectionState]. + CastConnectionState get connectionState; + + /// The receiver being cast to, or null when there is no session. + /// + /// Should be non-null from [CastConnectionState.connecting] onwards so the + /// UI can name the device it is connecting to. + CastDevice? get connectedDevice; + + /// The last error from the backend, e.g. a failed connect or a receiver that + /// refused the media. Cleared on the next successful operation. + String? get errorDescription; + + /// Playback state of the *receiver*, in the same shape the local player + /// reports so Chewie's controls, progress bar and time labels render a + /// remote session exactly as they render a local one. + /// + /// Only a handful of fields are read: `duration`, `position`, `buffered`, + /// `isPlaying`, `isBuffering`, `isInitialized`, `volume`, `playbackSpeed` + /// and `errorDescription`. Everything else can be left at its default — + /// there is no texture behind a cast session, so `size` in particular is + /// meaningless here. + /// + /// Before any media is loaded, return `VideoPlayerValue.uninitialized()`. + VideoPlayerValue get value; + + /// The media the receiver currently holds, when the backend can say. + /// + /// Chewie uses this to avoid reloading media a receiver is already playing. + /// Without it, rebuilding a [ChewieController] mid-session — which apps do + /// routinely, via `copyWith` or when switching video — issues a fresh `load` + /// and restarts playback on the TV for no reason. + /// + /// Returning null, the default, means "cannot say", and Chewie loads. + CastMedia? get currentMedia => null; + + /// Whether a session is live. Convenience over [connectionState]. + bool get isConnected => connectionState.isConnected; + + /// Start scanning for receivers. + /// + /// Chewie calls this when the user opens the device picker. Implementations + /// that discover continuously in the background can make it a no-op. + Future startDiscovery(); + + /// Stop scanning. Chewie calls this when the picker closes, so discovery + /// does not keep the radio busy for the life of the app. + Future stopDiscovery(); + + /// Open a session with [device]. + /// + /// Move [connectionState] to [CastConnectionState.connecting] immediately and + /// to [CastConnectionState.connected] once the receiver is ready for media — + /// Chewie calls [load] off the back of that transition, so connecting early + /// means loading before the receiver can accept it. + Future connect(CastDevice device); + + /// Tear the session down and return playback to the device. + Future disconnect(); + + /// Play [media] on the receiver, starting at [startAt]. + /// + /// Called by Chewie on connect and whenever the media changes mid-session. + /// [autoPlay] mirrors whether the local player was playing at handover, so + /// casting a paused video leaves it paused on the TV. + Future load( + CastMedia media, { + Duration startAt = Duration.zero, + bool autoPlay = true, + }); + + /// Resume the receiver. + Future play(); + + /// Pause the receiver. + Future pause(); + + /// Seek the receiver to [position]. + Future seekTo(Duration position); + + /// Set the receiver's volume, 0.0 to 1.0. + Future setVolume(double volume); + + /// Set the receiver's playback speed. + /// + /// Not every receiver supports this. The default implementation does + /// nothing, which leaves Chewie's speed menu inert rather than broken while + /// casting. + Future setPlaybackSpeed(double speed) async {} +} diff --git a/lib/src/cast/chewie_playback_target.dart b/lib/src/cast/chewie_playback_target.dart new file mode 100644 index 000000000..9b9c65f67 --- /dev/null +++ b/lib/src/cast/chewie_playback_target.dart @@ -0,0 +1,121 @@ +import 'package:chewie/src/cast/chewie_cast_controller.dart'; +import 'package:flutter/foundation.dart'; +import 'package:video_player/video_player.dart'; + +/// Whatever Chewie's controls are currently driving: the local video player, +/// or a cast receiver. +/// +/// The controls read [value] and call the transport methods without caring +/// which of the two is behind them, so a single set of skins serves both. It +/// is a [Listenable], and the active target is the one to listen to for +/// playback updates. +abstract class ChewiePlaybackTarget implements Listenable { + /// Current playback state, in `video_player`'s shape whether it came from + /// the local player or a receiver. + VideoPlayerValue get value; + + /// True when this target is a cast receiver rather than the local player. + bool get isRemote; + + Future play(); + + Future pause(); + + Future seekTo(Duration position); + + Future setVolume(double volume); + + Future setPlaybackSpeed(double speed); +} + +/// The local [VideoPlayerController] as a playback target. +class LocalPlaybackTarget implements ChewiePlaybackTarget { + const LocalPlaybackTarget(this.controller); + + final VideoPlayerController controller; + + @override + VideoPlayerValue get value => controller.value; + + @override + bool get isRemote => false; + + @override + void addListener(VoidCallback listener) => controller.addListener(listener); + + @override + void removeListener(VoidCallback listener) => + controller.removeListener(listener); + + @override + Future play() => controller.play(); + + @override + Future pause() => controller.pause(); + + @override + Future seekTo(Duration position) => controller.seekTo(position); + + @override + Future setVolume(double volume) => controller.setVolume(volume); + + @override + Future setPlaybackSpeed(double speed) => + controller.setPlaybackSpeed(speed); + + // Two wrappers around the same player are the same target. Widgets compare + // targets to decide whether to move their listeners, and they get a fresh + // wrapper on every build. + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalPlaybackTarget && other.controller == controller); + + @override + int get hashCode => controller.hashCode; +} + +/// A [ChewieCastController] as a playback target, used while a session is live. +class CastPlaybackTarget implements ChewiePlaybackTarget { + const CastPlaybackTarget(this.castController); + + final ChewieCastController castController; + + @override + VideoPlayerValue get value => castController.value; + + @override + bool get isRemote => true; + + @override + void addListener(VoidCallback listener) => + castController.addListener(listener); + + @override + void removeListener(VoidCallback listener) => + castController.removeListener(listener); + + @override + Future play() => castController.play(); + + @override + Future pause() => castController.pause(); + + @override + Future seekTo(Duration position) => castController.seekTo(position); + + @override + Future setVolume(double volume) => castController.setVolume(volume); + + @override + Future setPlaybackSpeed(double speed) => + castController.setPlaybackSpeed(speed); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is CastPlaybackTarget && other.castController == castController); + + @override + int get hashCode => castController.hashCode; +} diff --git a/lib/src/cast/index.dart b/lib/src/cast/index.dart new file mode 100644 index 000000000..1db02c157 --- /dev/null +++ b/lib/src/cast/index.dart @@ -0,0 +1,10 @@ +export 'cast_connection_state.dart'; +export 'cast_device.dart'; +export 'cast_media.dart'; +export 'chewie_cast_controller.dart'; +export 'chewie_playback_target.dart'; +export 'widgets/cast_button.dart'; +export 'widgets/cast_devices_dialog.dart'; +export 'widgets/cast_overlay.dart'; +export 'widgets/cupertino_cast_button.dart'; +export 'widgets/cupertino_cast_devices_sheet.dart'; diff --git a/lib/src/cast/widgets/cast_button.dart b/lib/src/cast/widgets/cast_button.dart new file mode 100644 index 000000000..4e01f416f --- /dev/null +++ b/lib/src/cast/widgets/cast_button.dart @@ -0,0 +1,177 @@ +import 'package:chewie/src/cast/cast_connection_state.dart'; +import 'package:chewie/src/cast/chewie_cast_controller.dart'; +import 'package:chewie/src/cast/widgets/cast_devices_dialog.dart'; +import 'package:chewie/src/cast/widgets/cupertino_cast_devices_sheet.dart'; +import 'package:chewie/src/models/cast_translations.dart'; +import 'package:flutter/cupertino.dart' show showCupertinoModalPopup; +import 'package:flutter/material.dart'; + +/// How [CastButton] presents its device picker. +enum CastPickerStyle { + /// A Material modal bottom sheet of `ListTile`s. + material, + + /// A `CupertinoActionSheet`, as iOS presents a choice. + cupertino, +} + +/// The cast button for Chewie's control bars. +/// +/// Shows the idle cast glyph when there is no session, a pulsing one while +/// connecting, and the connected glyph once the receiver has the stream. +/// Tapping it opens the device picker; discovery runs only while that sheet is +/// open, so an idle player is not scanning the network forever. +class CastButton extends StatefulWidget { + const CastButton({ + required this.castController, + required this.translations, + this.iconColor = Colors.white, + this.iconSize, + this.padding, + this.useRootNavigator = true, + this.pickerStyle = CastPickerStyle.material, + this.cancelButtonText, + this.onMenuOpened, + this.onMenuClosed, + super.key, + }); + + final ChewieCastController castController; + final CastTranslations translations; + final Color iconColor; + final double? iconSize; + final EdgeInsets? padding; + final bool useRootNavigator; + + /// Which picker the button opens; set by the skin that hosts it. + final CastPickerStyle pickerStyle; + final String? cancelButtonText; + + /// Called as the picker opens, so the skin can hold its hide timer. + final VoidCallback? onMenuOpened; + + /// Called once the picker is dismissed. + final VoidCallback? onMenuClosed; + + @override + State createState() => _CastButtonState(); +} + +class _CastButtonState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _pulseController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 900), + ); + + @override + void initState() { + super.initState(); + widget.castController.addListener(_onCastChanged); + _syncPulse(); + } + + @override + void didUpdateWidget(CastButton oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.castController != widget.castController) { + oldWidget.castController.removeListener(_onCastChanged); + widget.castController.addListener(_onCastChanged); + _syncPulse(); + } + } + + @override + void dispose() { + widget.castController.removeListener(_onCastChanged); + _pulseController.dispose(); + super.dispose(); + } + + void _onCastChanged() { + if (!mounted) return; + setState(_syncPulse); + } + + void _syncPulse() { + if (widget.castController.connectionState.isTransitioning) { + if (!_pulseController.isAnimating) { + _pulseController.repeat(reverse: true); + } + } else if (_pulseController.isAnimating) { + _pulseController + ..stop() + ..value = 1.0; + } + } + + Future _openPicker() async { + widget.onMenuOpened?.call(); + // Only scan while the user is looking at the list. + await widget.castController.startDiscovery(); + + if (!mounted) { + // Never leave a scan running for a picker that will never open. + await widget.castController.stopDiscovery(); + return; + } + switch (widget.pickerStyle) { + case CastPickerStyle.material: + await showModalBottomSheet( + context: context, + isScrollControlled: true, + useRootNavigator: widget.useRootNavigator, + builder: (_) => CastDevicesDialog( + castController: widget.castController, + translations: widget.translations, + cancelButtonText: widget.cancelButtonText, + ), + ); + case CastPickerStyle.cupertino: + await showCupertinoModalPopup( + context: context, + useRootNavigator: widget.useRootNavigator, + builder: (_) => CupertinoCastDevicesSheet( + castController: widget.castController, + translations: widget.translations, + cancelButtonText: widget.cancelButtonText, + ), + ); + } + + await widget.castController.stopDiscovery(); + widget.onMenuClosed?.call(); + } + + @override + Widget build(BuildContext context) { + final state = widget.castController.connectionState; + final icon = Icon( + state.isConnected ? Icons.cast_connected : Icons.cast, + color: widget.iconColor, + size: widget.iconSize, + ); + + return Tooltip( + message: widget.translations.castButtonTooltip, + child: GestureDetector( + onTap: _openPicker, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: Padding( + padding: widget.padding ?? const EdgeInsets.all(12.0), + child: state.isTransitioning + ? FadeTransition( + opacity: Tween( + begin: 0.35, + end: 1.0, + ).animate(_pulseController), + child: icon, + ) + : icon, + ), + ), + ), + ); + } +} diff --git a/lib/src/cast/widgets/cast_devices_dialog.dart b/lib/src/cast/widgets/cast_devices_dialog.dart new file mode 100644 index 000000000..1300f1671 --- /dev/null +++ b/lib/src/cast/widgets/cast_devices_dialog.dart @@ -0,0 +1,136 @@ +import 'package:chewie/src/cast/cast_connection_state.dart'; +import 'package:chewie/src/cast/cast_device.dart'; +import 'package:chewie/src/cast/chewie_cast_controller.dart'; +import 'package:chewie/src/models/cast_translations.dart'; +import 'package:flutter/material.dart'; + +/// The device picker, shown as a modal bottom sheet when the cast button is +/// tapped. +/// +/// Rebuilds off the [ChewieCastController] so devices appearing and +/// disappearing mid-scan are reflected live. +class CastDevicesDialog extends StatelessWidget { + const CastDevicesDialog({ + required this.castController, + required this.translations, + this.cancelButtonText, + super.key, + }); + + final ChewieCastController castController; + final CastTranslations translations; + final String? cancelButtonText; + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: castController, + builder: (context, _) { + final devices = castController.devices; + final connected = castController.connectedDevice; + + return SafeArea( + top: false, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(24, 20, 24, 8), + child: Text( + translations.devicesTitle, + style: Theme.of(context).textTheme.titleMedium, + ), + ), + if (devices.isEmpty) _buildEmptyState(context), + for (final device in devices) + _buildDeviceTile(context, device, connected), + if (castController.connectionState.isConnected || + castController.connectionState.isTransitioning) + ListTile( + leading: const Icon(Icons.cast_connected), + title: Text(translations.stopCastingText), + onTap: () { + Navigator.pop(context); + castController.disconnect(); + }, + ), + if (cancelButtonText != null) + ListTile( + title: Text(cancelButtonText!), + onTap: () => Navigator.pop(context), + ), + ], + ), + ), + ); + }, + ); + } + + Widget _buildEmptyState(BuildContext context) { + // While a scan is running, an empty list means "not found yet", not "none + // exist" — saying "no devices found" there would be wrong for the first + // couple of seconds of every scan. + final searching = castController.isDiscovering; + + return Padding( + padding: const EdgeInsets.fromLTRB(24, 12, 24, 24), + child: Row( + children: [ + if (searching) + const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + else + const Icon(Icons.cast, size: 16), + const SizedBox(width: 12), + Flexible( + child: Text( + searching + ? translations.searchingText + : translations.noDevicesText, + ), + ), + ], + ), + ); + } + + Widget _buildDeviceTile( + BuildContext context, + CastDevice device, + CastDevice? connected, + ) { + final isCurrent = device == connected; + final isConnecting = + isCurrent && castController.connectionState.isTransitioning; + + return ListTile( + leading: Icon(isCurrent ? Icons.cast_connected : Icons.cast), + title: Text(device.name), + subtitle: isConnecting + ? Text(translations.connectingText) + : device.modelName != null + ? Text(device.modelName!) + : null, + trailing: isConnecting + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : null, + selected: isCurrent, + onTap: isCurrent + ? null + : () { + Navigator.pop(context); + castController.connect(device); + }, + ); + } +} diff --git a/lib/src/cast/widgets/cast_overlay.dart b/lib/src/cast/widgets/cast_overlay.dart new file mode 100644 index 000000000..104a567f5 --- /dev/null +++ b/lib/src/cast/widgets/cast_overlay.dart @@ -0,0 +1,74 @@ +import 'package:chewie/src/cast/cast_device.dart'; +import 'package:chewie/src/models/cast_translations.dart'; +import 'package:flutter/material.dart'; + +/// What Chewie shows in place of the video surface while a session is live. +/// +/// The local player is paused during a session, so the texture underneath is a +/// frozen frame at best and black at worst; replacing it makes it obvious the +/// picture moved to the TV rather than that playback broke. +/// +/// Shown from the moment a device is picked rather than once the receiver has +/// the video, so tapping one has visible consequences straight away; see +/// [connecting]. +/// +/// Override it with `ChewieController.castOverlayBuilder` when the app has its +/// own idea of what casting should look like. +class CastOverlay extends StatelessWidget { + const CastOverlay({ + required this.device, + required this.translations, + this.connecting = false, + super.key, + }); + + final CastDevice? device; + final CastTranslations translations; + + /// Whether the session is still being set up. + /// + /// Says so rather than claiming the video is already on the television, + /// which for a receiver that takes its time would be a lie for several + /// seconds - and a worse one if the connection then fails. + final bool connecting; + + @override + Widget build(BuildContext context) { + final name = device?.name; + final String label; + if (connecting) { + label = name != null + ? '${translations.connectingText} $name' + : translations.connectingText; + } else { + label = name != null + ? '${translations.castingToText} $name' + : translations.castButtonTooltip; + } + + return ColoredBox( + color: Colors.black, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + connecting ? Icons.cast : Icons.cast_connected, + color: Colors.white70, + size: 48, + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Text( + label, + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.white70, fontSize: 16), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/cast/widgets/cupertino_cast_button.dart b/lib/src/cast/widgets/cupertino_cast_button.dart new file mode 100644 index 000000000..f91087321 --- /dev/null +++ b/lib/src/cast/widgets/cupertino_cast_button.dart @@ -0,0 +1,74 @@ +import 'package:chewie/src/cast/chewie_cast_controller.dart'; +import 'package:chewie/src/cast/widgets/cast_button.dart'; +import 'package:chewie/src/cupertino/widgets/cupertino_frosted_pill.dart'; +import 'package:chewie/src/models/cast_translations.dart'; +import 'package:flutter/material.dart'; + +/// [CastButton] wrapped in the chrome Cupertino's control bar expects. +/// +/// The Cupertino skin gives each top-bar button its own blurred, rounded +/// background rather than sharing one behind the whole bar, so the cast button +/// has to bring its own. It also fades itself: Cupertino has no shared opacity +/// wrapper, and without [hidden] the cast icon stayed on screen after the rest +/// of the controls had gone, looking like it was not part of them. +class CupertinoCastButton extends StatelessWidget { + const CupertinoCastButton({ + required this.castController, + required this.translations, + required this.backgroundColor, + required this.barHeight, + required this.buttonPadding, + this.hidden = false, + this.iconColor = Colors.white, + this.useRootNavigator = true, + this.cancelButtonText, + this.onMenuOpened, + this.onMenuClosed, + super.key, + }); + + final ChewieCastController castController; + final CastTranslations translations; + + /// Fill behind the blur, matching the bar's other buttons. + final Color backgroundColor; + final double barHeight; + final double buttonPadding; + + /// Whether the controls are currently hidden; fades the button with them. + final bool hidden; + + final Color iconColor; + final bool useRootNavigator; + final String? cancelButtonText; + + /// Called as the picker opens, so the skin can hold its hide timer. + final VoidCallback? onMenuOpened; + + /// Called once the picker is dismissed. + final VoidCallback? onMenuClosed; + + @override + Widget build(BuildContext context) { + return AnimatedOpacity( + opacity: hidden ? 0.0 : 1.0, + duration: const Duration(milliseconds: 300), + child: CupertinoFrostedPill( + backgroundColor: backgroundColor, + barHeight: barHeight, + child: CastButton( + castController: castController, + translations: translations, + useRootNavigator: useRootNavigator, + pickerStyle: CastPickerStyle.cupertino, + cancelButtonText: cancelButtonText, + iconColor: iconColor, + iconSize: 16, + padding: EdgeInsets.symmetric(horizontal: buttonPadding), + onMenuOpened: onMenuOpened, + onMenuClosed: onMenuClosed, + ), + ), + ); + } +} diff --git a/lib/src/cast/widgets/cupertino_cast_devices_sheet.dart b/lib/src/cast/widgets/cupertino_cast_devices_sheet.dart new file mode 100644 index 000000000..f50599314 --- /dev/null +++ b/lib/src/cast/widgets/cupertino_cast_devices_sheet.dart @@ -0,0 +1,140 @@ +import 'package:chewie/src/cast/cast_connection_state.dart'; +import 'package:chewie/src/cast/cast_device.dart'; +import 'package:chewie/src/cast/chewie_cast_controller.dart'; +import 'package:chewie/src/models/cast_translations.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart' show Icons; + +/// The device picker as a [CupertinoActionSheet], for the Cupertino skin. +/// +/// Same content as `CastDevicesDialog`, presented the way iOS presents a +/// choice: an action sheet from the bottom rather than a Material modal sheet +/// of `ListTile`s. +/// +/// Rebuilds off the [ChewieCastController] so devices appearing and +/// disappearing mid-scan are reflected live. +class CupertinoCastDevicesSheet extends StatelessWidget { + const CupertinoCastDevicesSheet({ + required this.castController, + required this.translations, + this.cancelButtonText, + super.key, + }); + + final ChewieCastController castController; + final CastTranslations translations; + final String? cancelButtonText; + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: castController, + builder: (context, _) { + final devices = castController.devices; + final connected = castController.connectedDevice; + final state = castController.connectionState; + + return CupertinoActionSheet( + title: Text(translations.devicesTitle), + message: devices.isEmpty ? _buildEmptyState() : null, + actions: [ + for (final device in devices) + _buildDeviceAction(context, device, connected), + if (state.isConnected || state.isTransitioning) + CupertinoActionSheetAction( + isDestructiveAction: true, + onPressed: () { + Navigator.pop(context); + castController.disconnect(); + }, + child: Text(translations.stopCastingText), + ), + ], + cancelButton: cancelButtonText == null + ? null + : CupertinoActionSheetAction( + isDefaultAction: true, + onPressed: () => Navigator.pop(context), + child: Text(cancelButtonText!), + ), + ); + }, + ); + } + + Widget _buildEmptyState() { + // While a scan is running, an empty list means "not found yet", not "none + // exist" — saying "no devices found" there would be wrong for the first + // couple of seconds of every scan. + final searching = castController.isDiscovering; + + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (searching) + const CupertinoActivityIndicator(radius: 8) + else + const Icon(Icons.cast, size: 16), + const SizedBox(width: 12), + Flexible( + child: Text( + searching ? translations.searchingText : translations.noDevicesText, + ), + ), + ], + ); + } + + Widget _buildDeviceAction( + BuildContext context, + CastDevice device, + CastDevice? connected, + ) { + final isCurrent = device == connected; + final isConnecting = + isCurrent && castController.connectionState.isTransitioning; + + // The glyph matches the cast button's own, rather than switching to + // CupertinoIcons, so the sheet reads as belonging to the button that + // opened it. + final subtitle = isConnecting + ? translations.connectingText + : device.modelName; + + return CupertinoActionSheetAction( + // Connecting to the device already selected would restart the session. + onPressed: isCurrent + ? () {} + : () { + Navigator.pop(context); + castController.connect(device); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(isCurrent ? Icons.cast_connected : Icons.cast, size: 18), + const SizedBox(width: 10), + Flexible( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(device.name), + if (subtitle != null) + Text( + subtitle, + style: CupertinoTheme.of( + context, + ).textTheme.tabLabelTextStyle, + ), + ], + ), + ), + if (isConnecting) ...[ + const SizedBox(width: 10), + const CupertinoActivityIndicator(radius: 8), + ], + ], + ), + ); + } +} diff --git a/lib/src/center_play_button.dart b/lib/src/center_play_button.dart new file mode 100644 index 000000000..3a9261c98 --- /dev/null +++ b/lib/src/center_play_button.dart @@ -0,0 +1,52 @@ +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..44944e163 100644 --- a/lib/src/chewie_player.dart +++ b/lib/src/chewie_player.dart @@ -1,30 +1,44 @@ import 'dart:async'; +import 'package:chewie/src/cast/cast_connection_state.dart'; +import 'package:chewie/src/cast/cast_device.dart'; +import 'package:chewie/src/cast/cast_media.dart'; +import 'package:chewie/src/cast/chewie_cast_controller.dart'; +import 'package:chewie/src/cast/chewie_playback_target.dart'; import 'package:chewie/src/chewie_progress_colors.dart'; +import 'package:chewie/src/models/chewie_chapter.dart'; +import 'web_fullscreen.dart'; +import 'package:chewie/src/models/cast_translations.dart'; +import 'package:chewie/src/models/audio_track.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/models/subtitle_style.dart'; +import 'package:chewie/src/models/video_quality.dart'; +import 'package:chewie/src/models/subtitle_track.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'; - -typedef Widget ChewieRoutePageBuilder( - BuildContext context, - Animation animation, - Animation secondaryAnimation, - _ChewieControllerProvider controllerProvider, -); +import 'package:wakelock_plus/wakelock_plus.dart'; + +typedef ChewieRoutePageBuilder = + Widget Function( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ChewieControllerProvider controllerProvider, + ); /// A Video Player with Material and Cupertino skins. /// /// `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; @@ -37,57 +51,149 @@ class Chewie extends StatefulWidget { class ChewieState extends State { bool _isFullScreen = false; + bool _wasPlayingBeforeFullScreen = false; + bool _resumeAppliedInFullScreen = false; + + bool get isControllerFullScreen => widget.controller.isFullScreen; + late PlayerNotifier notifier; + late final void Function() _browserFsExitHandler; + + /// Forces the video-element branch on or off, for tests. + /// + /// [kIsWeb] is a compile-time constant, so the branch below is unreachable + /// from a VM test without an override, in the spirit of + /// `debugDefaultTargetPlatformOverride`. Reset it to null when done. + @visibleForTesting + static bool? debugUsesVideoElementFullScreen; + + /// Browsers without the Fullscreen API (iPhone Safari) cannot host the + /// fullscreen route: `requestFullscreen` throws before the route is pushed, + /// and the next exit request pops the page underneath instead. On those + /// browsers the video element's own player is used and no route is involved. + bool get _usesVideoElementFullScreen => + debugUsesVideoElementFullScreen ?? + (kIsWeb && + widget.controller.useNativeFullScreenOnWeb && + !browserFullscreenSupported); + + // ignore: invalid_use_of_visible_for_testing_member + int get _playerId => widget.controller.videoPlayerController.playerId; @override void initState() { super.initState(); widget.controller.addListener(listener); + notifier = PlayerNotifier.init(); + // When the user presses Escape, the browser exits its native fullscreen + // without Chewie knowing. Detect this and collapse the fullscreen route. + _browserFsExitHandler = () { + if (!browserInFullscreen && _isFullScreen) { + widget.controller.exitFullScreen(); + } + }; + if (widget.controller.useNativeFullScreenOnWeb) { + addBrowserFullscreenChangeListener(_browserFsExitHandler); + } } @override void dispose() { + if (widget.controller.useNativeFullScreenOnWeb) { + removeBrowserFullscreenChangeListener(_browserFsExitHandler); + } + if (_usesVideoElementFullScreen) { + disposeVideoElementFullscreen(_playerId); + } widget.controller.removeListener(listener); + notifier.dispose(); super.dispose(); } @override void didUpdateWidget(Chewie oldWidget) { if (oldWidget.controller != widget.controller) { + oldWidget.controller.removeListener(listener); 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 (widget.controller.disableFullScreenRoute) { + // The host app owns the fullscreen presentation; never push/pop a route + // (which on web reparents the