diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 000000000..81deb5bc0
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,96 @@
+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.29. Make this the new minimum once the next
+  # stable version is released
+  FLUTTER_VERSION_MINIMUM_DEFAULT: "3.29.3"
+  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@v4
+
+      - name: 🐦 Setup Flutter
+        uses: subosito/flutter-action@v2
+        with:
+          flutter-version: ${{ matrix.flutter-version }}
+          channel: stable
+          cache: true
+          cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }}
+
+      - name: 📦 Install Dependencies
+        run: flutter packages get
+
+      - name: ✨ Check Formatting
+        run: dart format --set-exit-if-changed lib
+        # 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@v5
+        # 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..5a47f7877 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,4 +45,6 @@ build/
 
 .project
 .classpath
-.settings
\ No newline at end of file
+.settings
+
+coverage/*
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3672146a2..73875e262 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,254 @@
+## [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<double> animation,
+      Animation<double> secondaryAnimation,
+      _ChewieControllerProvider controllerProvider,
+  );
+  ```
+  
+  It is now:
+  
+  ```dart
+  typedef ChewieRoutePageBuilder = Widget Function(
+  	  BuildContext context,
+  	  Animation<double> animation,
+      Animation<double> 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 +256,6 @@
 * Update Flutter SDK
 * Update `wakelock` dependency
 
-
 ## 0.10.3+1
 
 * Format using `dartfmt -w .` for pub.dev
@@ -138,3 +388,4 @@ Initial version of Chewie, the video player with a heart of gold.
   * Includes Material Player Controls
   * Includes Cupertino Player Controls
   * Spike version: Focus on good looking UI. Internal code is sloppy, needs a refactor and tests
+
diff --git a/README.md b/README.md
index d7dddfd08..a6bcb1e4b 100644
--- a/README.md
+++ b/README.md
@@ -1,18 +1,63 @@
 # chewie
-[![Version](https://img.shields.io/badge/pub-v0.10.4-blue)](https://pub.dev/packages/chewie)
+
+[![Flutter Community: chewie](https://fluttercommunity.dev/_github/header/chewie)](https://github.com/fluttercommunity/community)
+
+[![Version](https://img.shields.io/pub/v/chewie.svg)](https://pub.dev/packages/chewie)
+![CI](https://github.com/brianegan/chewie/workflows/CI/badge.svg)
 [![Generic badge](https://img.shields.io/badge/platform-android%20|%20ios%20|%20web%20-blue.svg)](https://pub.dev/packages/chewie)
 
 The video player for Flutter with a heart of gold. 
 
-The [`video_player`](https://pub.dartlang.org/packages/video_player) plugin provides low-level access to video playback. Chewie uses the `video_player` under the hood and wraps it in a friendly Material or Cupertino UI!
+The [`video_player`](https://pub.dartlang.org/packages/video_player) plugin provides low-level 
+access to video playback. 
+
+Chewie uses the `video_player` under the hood and wraps it in a friendly Material or Cupertino UI!
+
+## Table of Contents
+1.  🚨 [IMPORTANT!!! (READ THIS FIRST)](#-important-read-this-first)
+2.  🔀 [Flutter Version Compatibility](#-flutter-version-compatibility)
+3.  🖼️ [Preview](#%EF%B8%8F-preview)
+4.  ⬇️ [Installation](#%EF%B8%8F-installation)
+5.  🕹️ [Using it](#%EF%B8%8F-using-it)
+6.  ⚙️ [Options](#%EF%B8%8F-options)
+7.  🔡 [Subtitles](#-subtitles)
+8.  🧪 [Example](#-example)
+9.  ⏪ [Migrating from Chewie < 0.9.0](#-migrating-from-chewie--090)
+10. 🗺️ [Roadmap](#%EF%B8%8F-roadmap)
+11. ⚠️ [Android warning](#%EF%B8%8F-android-warning)
+12. 📱 [iOS warning](#-ios-warning)
+
+
+## 🚨 IMPORTANT!!! (READ THIS FIRST)
+This library is __NOT__ responsible for any issues caused by `video_player`, since it's merely a UI 
+layer on top of it. 
+
+In other words, if you see any `PlatformException`s being thrown in your app due to video playback,
+they are exclusive to the `video_player` library. 
+
+Instead, please raise an issue related to it with the [Flutter Team](https://github.com/flutter/flutter/issues/new/choose).
+
+## 🔀 Flutter Version Compatibility
+
+This library will at the very least make a solid effort to support the second most recent version 
+of Flutter released. In other words, it will adopt `N-1` version support at
+the bare minimum.
 
-## Demo
+However, this cannot be guaranteed due to major changes between Flutter versions. Should that occur,
+future updates will be released as major or minor versions as needed.
 
-![Demo](https://github.com/brianegan/chewie/raw/master/assets/chewie_demo.gif)
+## 🖼️ Preview
 
-## Installation
+|                                MaterialControls                                 |                                MaterialDesktopControls                                 |
+|:-------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------------:|
+| ![](https://github.com/brianegan/chewie/raw/master/assets/MaterialControls.png) | ![](https://github.com/brianegan/chewie/raw/master/assets/MaterialDesktopControls.png) |
 
-In your `pubspec.yaml` file within your Flutter Project: 
+### CupertinoControls
+![](https://github.com/brianegan/chewie/raw/master/assets/CupertinoControls.png)
+
+## ⬇️ Installation
+
+In your `pubspec.yaml` file within your Flutter Project add `chewie` and `video_player` under dependencies:
 
 ```yaml
 dependencies:
@@ -20,12 +65,14 @@ dependencies:
   video_player: <latest_version>
 ```
 
-## Use it
+## 🕹️ Using it
 
 ```dart
 import 'package:chewie/chewie.dart';
-final videoPlayerController = VideoPlayerController.network(
-    'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4');
+import 'package:video_player/video_player.dart';
+
+final videoPlayerController = VideoPlayerController.networkUrl(Uri.parse(
+    'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4'));
 
 await videoPlayerController.initialize();
 
@@ -40,7 +87,7 @@ final playerWidget = Chewie(
 );
 ```
 
-Please make sure to dispose both controller widgets after use. For example by overriding the dispose method of the a `StatefulWidget`:
+Please make sure to dispose both controller widgets after use. For example, by overriding the dispose method of the a `StatefulWidget`:
 ```dart
 @override
 void dispose() {
@@ -50,12 +97,147 @@ void dispose() {
 }
 ```
 
-## Example
+## ⚙️ Options
+
+![](https://github.com/brianegan/chewie/raw/master/assets/Options.png)
+
+Chewie has some options which control the video. These options appear by default in a `showModalBottomSheet` (similar to YT). By default, Chewie passes  `Playback speed` and `Subtitles` options as an `OptionItem`.
+
+To add additional options, just add these lines to your `ChewieController`:
+
+```dart
+additionalOptions: (context) {
+  return <OptionItem>[
+    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<void>(
+    context: context,
+    builder: (ctx) {
+      return AlertDialog(
+        content: ListView.builder(
+          itemCount: defaultOptions.length,
+          itemBuilder: (_, i) => ActionChip(
+            label: Text(defaultOptions[i].title),
+            onPressed: () =>
+                defaultOptions[i].onTap!(),
+          ),
+        ),
+      );
+    },
+  );
+},
+```
+
+Your `additionalOptions` are already included here (if you provided `additionalOptions`)!
+
+### Translations
+
+What is an option without proper translation? 
+
+To add your translation strings add:
+
+```dart
+optionsTranslation: OptionsTranslation(
+  playbackSpeedButtonText: 'Wiedergabegeschwindigkeit',
+  subtitlesButtonText: 'Untertitel',
+  cancelButtonText: 'Abbrechen',
+),
+```
+
+## 🔡 Subtitles
+
+> Since version 1.1.0, Chewie supports subtitles.
+
+Chewie allows you to enhance the video playback experience with text overlays. You can add a `List<Subtitle>` to your `ChewieController` and fully customize their appearance using the `subtitleBuilder` function.
+
+### Showing Subtitles by Default
+
+Chewie provides the `showSubtitles` flag, allowing you to control whether subtitles are displayed automatically when the video starts. By default, this flag is set to `false`.
+
+### Adding Subtitles
+
+Here’s an example of how to add subtitles to your `ChewieController`:
+
+```dart
+ChewieController(
+  videoPlayerController: _videoPlayerController,
+  autoPlay: true,
+  looping: true,
+  subtitle: Subtitles([
+    Subtitle(
+      index: 0,
+      start: Duration.zero,
+      end: const Duration(seconds: 10),
+      text: 'Hello from subtitles',
+    ),
+    Subtitle(
+      index: 1,
+      start: const Duration(seconds: 10),
+      end: const Duration(seconds: 20),
+      text: 'What’s up? :)',
+    ),
+  ]),
+  showSubtitles: true, // Automatically display subtitles
+  subtitleBuilder: (context, subtitle) => Container(
+    padding: const EdgeInsets.all(10.0),
+    child: Text(
+      subtitle,
+      style: const TextStyle(color: Colors.white),
+    ),
+  ),
+);
+```
+
+### Subtitle Structure
+
+The `Subtitle` model contains the following key attributes:
+
+- **`index`**: A unique identifier for the subtitle, useful for database integration.
+- **`start`**: The starting point of the subtitle, defined as a `Duration`.
+- **`end`**: The ending point of the subtitle, defined as a `Duration`.
+- **`text`**: The subtitle text that will be displayed.
+
+For example, if your video is 10 minutes long and you want to add a subtitle that appears between `00:00` and `00:10`, you can define it like this:
+
+```dart
+Subtitle(
+  index: 0,
+  start: Duration.zero,
+  end: const Duration(seconds: 10),
+  text: 'Hello from subtitles',
+),
+```
+
+### Customizing Subtitles
+
+Use the `subtitleBuilder` function to customize how subtitles are rendered, allowing you to modify text styles, add padding, or apply other customizations to your subtitles.
+
+## 🧪 Example
 
 Please run the app in the [`example/`](https://github.com/brianegan/chewie/tree/master/example) folder to start playing!
 
-## Migrating from Chewie < 0.9.0
-Instead of passing the `VideoPlayerController` and your options to the `Chewie` widget you now pass them to the `ChewieController` and pass that latter to the `Chewie` widget.
+## ⏪ Migrating from Chewie < 0.9.0
+
+Instead of passing the `VideoPlayerController` and your options to the `Chewie` widget you now pass them to the `ChewieController` and pass that later to the `Chewie` widget.
 
 ```dart
 final playerWidget = Chewie(
@@ -79,9 +261,73 @@ final playerWidget = Chewie(
 );
 ```
 
-## iOS warning
+## 🗺️ Roadmap
+
+- [x] MaterialUI
+- [x] MaterialDesktopUI
+- [x] CupertinoUI
+- [x] Options with translations
+- [x] Subtitles
+- [x] CustomControls
+- [x] Auto-Rotate on FullScreen depending on Source Aspect-Ratio
+- [x] Live-Stream and UI
+- [x] AutoPlay
+- [x] Placeholder
+- [x] Looping
+- [x] Start video at
+- [x] Custom Progress-Bar colors
+- [x] Custom Overlay
+- [x] Allow Sleep (Wakelock)
+- [x] Playbackspeed Control 
+- [x] Custom Route-Pagebuilder
+- [x] Custom Device-Orientation and SystemOverlay before and after fullscreen
+- [x] Custom ErrorBuilder
+- [ ] Support different resolutions of video
+- [ ] Re-design State-Manager with Provider
+- [ ] Screen-Mirroring / Casting (Google Chromecast)
+
+
+## ⚠️ Android warning
+
+There is an open [issue](https://github.com/flutter/flutter/issues/165149) that the buffering state of a video is not reported correctly. With this, the loading state is always triggered, hiding controls to play, pause or seek the video. A workaround was implemented until this is fixed, however it can't be perfect and still hides controls if seeking backwards while the video is paused, as a result of lack of correct buffering information (see #912).
+
+Add the following to partly fix this behavior:
+
+```dart
+  // Your init code can be above
+  videoController.addListener(yourListeningMethod);
+
+  // ...
+
+  bool wasPlayingBefore = false;
+  void yourListeningMethod() {
+    if (!videoController.value.isPlaying && !wasPlayingBefore) {
+      // -> Workaround if seekTo another position while it was paused before.
+      //    On Android this might lead to infinite loading, so just play the
+      //    video again.
+      videoController.play();
+    }
+
+    wasPlayingBefore = videoController.value.isPlaying;
+
+  // ...
+  }
+```
+
+You can also disable the loading spinner entirely to fix this problem in a more _complete_ way, however will remove the loading indicator if a video is buffering.
+
+```dart
+_chewieController = ChewieController(
+  videoPlayerController: _videoPlayerController,
+  progressIndicatorDelay: Platform.isAndroid ? const Duration(days: 1) : null,
+);
+```
+
+## 📱 iOS warning 
+
+The video_player plugin used by chewie will only work in iOS simulators if you are on flutter 1.26.0 or above. You may need to switch to the beta channel `flutter channel beta`
+Please refer to this [issue](https://github.com/flutter/flutter/issues/14647).
 
-The video player plugin used by chewie is not functional on iOS simulators. An iOS device must be used during development/testing. Please refer to this [issue](https://github.com/flutter/flutter/issues/14647).
 
 
 ```
diff --git a/analysis_options.yaml b/analysis_options.yaml
index def4217f1..9f9f8bd14 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -1,18 +1,17 @@
+include: package:flutter_lints/flutter.yaml
+
 analyzer:
-  strong-mode:
-    implicit-casts: false
-    implicit-dynamic: false
+  language:
+    strict-raw-types: true
+  exclude:
+    - lib/generated_plugin_registrant.dart
+  errors:
+    # allow self-reference to deprecated members (we do this because otherwise we have
+    # to annotate every member in every test, assert, etc, when we deprecate something)
+    deprecated_member_use_from_same_package: ignore
 
 linter:
   rules:
-    - await_only_futures
-    - cancel_subscriptions
-    - close_sinks
-    - hash_and_equals
-    - iterable_contains_unrelated_type
-    - list_remove_unrelated_type
-    - sort_constructors_first
-    - test_types_in_equals
-    - unnecessary_new
-    - unrelated_type_equality_checks
-    - valid_regexps
+    close_sinks: true
+    sort_constructors_first: true
+    sort_pub_dependencies: false
diff --git a/assets/CupertinoControls.png b/assets/CupertinoControls.png
new file mode 100644
index 000000000..7723a3ae0
Binary files /dev/null and b/assets/CupertinoControls.png differ
diff --git a/assets/MaterialControls.png b/assets/MaterialControls.png
new file mode 100644
index 000000000..05f7fb8f0
Binary files /dev/null and b/assets/MaterialControls.png differ
diff --git a/assets/MaterialDesktopControls.png b/assets/MaterialDesktopControls.png
new file mode 100644
index 000000000..cdb982161
Binary files /dev/null and b/assets/MaterialDesktopControls.png differ
diff --git a/assets/Options.png b/assets/Options.png
new file mode 100644
index 000000000..ee4e0c1d3
Binary files /dev/null and b/assets/Options.png differ
diff --git a/assets/chewie_demo.gif b/assets/chewie_demo.gif
deleted file mode 100644
index c39e5fe7d..000000000
Binary files a/assets/chewie_demo.gif and /dev/null differ
diff --git a/devtools_options.yaml b/devtools_options.yaml
new file mode 100644
index 000000000..fa0b357c4
--- /dev/null
+++ b/devtools_options.yaml
@@ -0,0 +1,3 @@
+description: This file stores settings for Dart & Flutter DevTools.
+documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
+extensions:
diff --git a/example/.metadata b/example/.metadata
index 0aad8a1ad..5e2646bb9 100644
--- a/example/.metadata
+++ b/example/.metadata
@@ -4,5 +4,27 @@
 # This file should be version controlled and should not be manually edited.
 
 version:
-  revision: ab4506cad2a860e1cb6186c0957eeb86024a7c6b
-  channel: dev
+  revision: "b0850beeb25f6d5b10426284f506557f66181b36"
+  channel: "stable"
+
+project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+  platforms:
+    - platform: root
+      create_revision: b0850beeb25f6d5b10426284f506557f66181b36
+      base_revision: b0850beeb25f6d5b10426284f506557f66181b36
+    - platform: ios
+      create_revision: b0850beeb25f6d5b10426284f506557f66181b36
+      base_revision: b0850beeb25f6d5b10426284f506557f66181b36
+
+  # User provided section
+
+  # List of Local paths (relative to this file) that should be
+  # ignored by the migrate tool.
+  #
+  # Files that are not part of the templates will be ignored by default.
+  unmanaged_files:
+    - 'lib/main.dart'
+    - 'ios/Runner.xcodeproj/project.pbxproj'
diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml
new file mode 100644
index 000000000..3e67ba6df
--- /dev/null
+++ b/example/analysis_options.yaml
@@ -0,0 +1,12 @@
+include: package:flutter_lints/flutter.yaml
+
+analyzer:
+  language:
+    strict-raw-types: true
+  exclude:
+    - lib/generated_plugin_registrant.dart
+
+linter:
+  rules:
+    close_sinks: true
+    sort_constructors_first: true
diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle
index 72fecefc7..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 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.example.chewieexample">
+    package="com.example.example">
     <!-- Flutter needs it to communicate with the running application
          to allow setting breakpoints, to provide hot reload, etc.
     -->
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 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.example.chewieexample">
-
-    <uses-permission android:name="android.permission.INTERNET"/>
-
+    package="com.example.example">
    <application
-        android:label="chewie_example"
+        android:label="example"
         android:icon="@mipmap/ic_launcher">
         <activity
             android:name=".MainActivity"
@@ -12,7 +9,8 @@
             android:theme="@style/LaunchTheme"
             android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
             android:hardwareAccelerated="true"
-            android:windowSoftInputMode="adjustResize">
+            android:windowSoftInputMode="adjustResize"
+            android:exported="true">
             <!-- Specifies an Android theme to apply to this Activity as soon as
                  the Android process has started. This theme is visible to the user
                  while the Flutter UI initializes. After that, this theme continues
diff --git a/example/android/app/src/main/kotlin/com/example/chewieexample/MainActivity.kt b/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt
similarity index 73%
rename from example/android/app/src/main/kotlin/com/example/chewieexample/MainActivity.kt
rename to example/android/app/src/main/kotlin/com/example/example/MainActivity.kt
index 237ee7d9f..e793a000d 100644
--- a/example/android/app/src/main/kotlin/com/example/chewieexample/MainActivity.kt
+++ b/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt
@@ -1,4 +1,4 @@
-package com.example.chewieexample
+package com.example.example
 
 import io.flutter.embedding.android.FlutterActivity
 
diff --git a/example/android/app/src/main/res/drawable-v21/launch_background.xml b/example/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 000000000..f74085f3f
--- /dev/null
+++ b/example/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Modify this file to customize your launch splash screen -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="?android:colorBackground" />
+
+    <!-- You can insert your own image assets here -->
+    <!-- <item>
+        <bitmap
+            android:gravity="center"
+            android:src="@mipmap/launch_image" />
+    </item> -->
+</layer-list>
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 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- Modify this file to customize your launch splash screen -->
 <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:drawable="?android:colorBackground" />
+    <item android:drawable="@android:color/white" />
 
     <!-- You can insert your own image assets here -->
     <!-- <item>
diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml
index c919106e6..c208884f3 100644
--- a/example/android/app/src/profile/AndroidManifest.xml
+++ b/example/android/app/src/profile/AndroidManifest.xml
@@ -1,5 +1,5 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.example.chewieexample">
+    package="com.example.example">
     <!-- Flutter needs it to communicate with the running application
          to allow setting breakpoints, to provide hot reload, etc.
     -->
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 @@
   <string>????</string>
   <key>CFBundleVersion</key>
   <string>1.0</string>
-  <key>UIRequiredDeviceCapabilities</key>
-  <array>
-    <string>arm64</string>
-  </array>
   <key>MinimumOSVersion</key>
-  <string>8.0</string>
+  <string>12.0</string>
 </dict>
 </plist>
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 = "<group>"; };
 		1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
 		1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
+		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 = "<group>"; };
+		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 = "<group>"; };
+		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 = "<group>"; };
+		74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
+		74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
 		7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
-		7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
-		7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
-		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 = "<group>"; };
 		9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
 		9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
 		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 = "<group>"; };
 		97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
 		97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
 		97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
 		97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
-		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 = "<group>"; };
+		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 = "<group>"; };
 /* 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 = "<group>";
+		};
+		22109C890749BA0B4E7C88B0 /* Frameworks */ = {
 			isa = PBXGroup;
 			children = (
-				891E43F580F30980509C464D /* libPods-Runner.a */,
+				161F52A253A901BB69307277 /* Pods_Runner.framework */,
 			);
 			name = Frameworks;
 			sourceTree = "<group>";
 		};
+		331C8082294A63A400263BE5 /* RunnerTests */ = {
+			isa = PBXGroup;
+			children = (
+				331C807B294A618700263BE5 /* RunnerTests.swift */,
+			);
+			path = RunnerTests;
+			sourceTree = "<group>";
+		};
 		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 = "<group>";
 		};
@@ -98,6 +135,7 @@
 			isa = PBXGroup;
 			children = (
 				97C146EE1CF9000F007C117D /* Runner.app */,
+				331C8081294A63A400263BE5 /* RunnerTests.xctest */,
 			);
 			name = Products;
 			sourceTree = "<group>";
@@ -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 = "<group>";
 		};
-		97C146F11CF9000F007C117D /* Supporting Files */ = {
-			isa = PBXGroup;
-			children = (
-				97C146F21CF9000F007C117D /* main.m */,
-			);
-			name = "Supporting Files";
-			sourceTree = "<group>";
-		};
-		E60E3BF33DDF10E510884550 /* Pods */ = {
-			isa = PBXGroup;
-			children = (
-				8BEE04C81FA7DBA9A8D36BBD /* Pods-Runner.debug.xcconfig */,
-				B03FD6D8ED78792E9B6726F4 /* Pods-Runner.release.xcconfig */,
-			);
-			name = Pods;
-			sourceTree = "<group>";
-		};
 /* 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 @@
 <Workspace
    version = "1.0">
    <FileRef
-      location = "group:Runner.xcodeproj">
+      location = "self:">
    </FileRef>
 </Workspace>
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 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "0910"
+   LastUpgradeVersion = "1510"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
@@ -26,10 +26,8 @@
       buildConfiguration = "Debug"
       selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
       selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
-      language = ""
+      customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
       shouldUseLaunchSchemeArgsEnv = "YES">
-      <Testables>
-      </Testables>
       <MacroExpansion>
          <BuildableReference
             BuildableIdentifier = "primary"
@@ -39,19 +37,31 @@
             ReferencedContainer = "container:Runner.xcodeproj">
          </BuildableReference>
       </MacroExpansion>
-      <AdditionalOptions>
-      </AdditionalOptions>
+      <Testables>
+         <TestableReference
+            skipped = "NO"
+            parallelizable = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "331C8080294A63A400263BE5"
+               BuildableName = "RunnerTests.xctest"
+               BlueprintName = "RunnerTests"
+               ReferencedContainer = "container:Runner.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
    </TestAction>
    <LaunchAction
       buildConfiguration = "Debug"
       selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
       selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
-      language = ""
+      customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
       launchStyle = "0"
       useCustomWorkingDirectory = "NO"
       ignoresPersistentStateOnLaunch = "NO"
       debugDocumentVersioning = "YES"
       debugServiceExtension = "internal"
+      enableGPUValidationMode = "1"
       allowLocationSimulation = "YES">
       <BuildableProductRunnable
          runnableDebuggingMode = "0">
@@ -63,11 +73,9 @@
             ReferencedContainer = "container:Runner.xcodeproj">
          </BuildableReference>
       </BuildableProductRunnable>
-      <AdditionalOptions>
-      </AdditionalOptions>
    </LaunchAction>
    <ProfileAction
-      buildConfiguration = "Release"
+      buildConfiguration = "Profile"
       shouldUseLaunchSchemeArgsEnv = "YES"
       savedToolIdentifier = ""
       useCustomWorkingDirectory = "NO"
diff --git a/example/ios/Runner/AppDelegate.h b/example/ios/Runner/AppDelegate.h
deleted file mode 100644
index cf210d213..000000000
--- a/example/ios/Runner/AppDelegate.h
+++ /dev/null
@@ -1,6 +0,0 @@
-#import <UIKit/UIKit.h>
-#import <Flutter/Flutter.h>
-
-@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 @@
 <plist version="1.0">
 <dict>
 	<key>CFBundleDevelopmentRegion</key>
-	<string>en</string>
+	<string>$(DEVELOPMENT_LANGUAGE)</string>
+	<key>CFBundleDisplayName</key>
+	<string>Example</string>
 	<key>CFBundleExecutable</key>
 	<string>$(EXECUTABLE_NAME)</string>
 	<key>CFBundleIdentifier</key>
@@ -11,25 +13,21 @@
 	<key>CFBundleInfoDictionaryVersion</key>
 	<string>6.0</string>
 	<key>CFBundleName</key>
-	<string>chewie_example</string>
+	<string>example</string>
 	<key>CFBundlePackageType</key>
 	<string>APPL</string>
 	<key>CFBundleShortVersionString</key>
-	<string>1.0</string>
+	<string>$(FLUTTER_BUILD_NAME)</string>
 	<key>CFBundleSignature</key>
 	<string>????</string>
 	<key>CFBundleVersion</key>
-	<string>1</string>
+	<string>$(FLUTTER_BUILD_NUMBER)</string>
 	<key>LSRequiresIPhoneOS</key>
 	<true/>
 	<key>UILaunchStoryboardName</key>
 	<string>LaunchScreen</string>
 	<key>UIMainStoryboardFile</key>
 	<string>Main</string>
-	<key>UIRequiredDeviceCapabilities</key>
-	<array>
-		<string>arm64</string>
-	</array>
 	<key>UISupportedInterfaceOrientations</key>
 	<array>
 		<string>UIInterfaceOrientationPortrait</string>
@@ -45,10 +43,9 @@
 	</array>
 	<key>UIViewControllerBasedStatusBarAppearance</key>
 	<false/>
-	<key>NSAppTransportSecurity</key>
-    <dict>
-      <key>NSAllowsArbitraryLoads</key>
-      <true/>
-    </dict>
+	<key>CADisableMinimumFrameDurationOnPhone</key>
+	<true/>
+	<key>UIApplicationSupportsIndirectInputEvents</key>
+	<true/>
 </dict>
 </plist>
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 <UIKit/UIKit.h>
-#import <Flutter/Flutter.h>
-#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..ce3f46efa
--- /dev/null
+++ b/example/lib/app/app.dart
@@ -0,0 +1,375 @@
+import 'package:chewie/chewie.dart';
+import 'package:chewie_example/app/theme.dart';
+import 'package:flutter/material.dart';
+import 'package:video_player/video_player.dart';
+
+class ChewieDemo extends StatefulWidget {
+  const ChewieDemo({super.key, this.title = 'Chewie Demo'});
+
+  final String title;
+
+  @override
+  State<StatefulWidget> createState() {
+    return _ChewieDemoState();
+  }
+}
+
+class _ChewieDemoState extends State<ChewieDemo> {
+  TargetPlatform? _platform;
+  late VideoPlayerController _videoPlayerController1;
+  late VideoPlayerController _videoPlayerController2;
+  ChewieController? _chewieController;
+  int? bufferDelay;
+
+  @override
+  void initState() {
+    super.initState();
+    initializePlayer();
+  }
+
+  @override
+  void dispose() {
+    _videoPlayerController1.dispose();
+    _videoPlayerController2.dispose();
+    _chewieController?.dispose();
+    super.dispose();
+  }
+
+  List<String> srcs = [
+    "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
+    "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4",
+    "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4",
+  ];
+
+  Future<void> initializePlayer() async {
+    _videoPlayerController1 = VideoPlayerController.networkUrl(
+      Uri.parse(srcs[currPlayIndex]),
+    );
+    _videoPlayerController2 = VideoPlayerController.networkUrl(
+      Uri.parse(srcs[currPlayIndex]),
+    );
+    await Future.wait([
+      _videoPlayerController1.initialize(),
+      _videoPlayerController2.initialize(),
+    ]);
+    _createChewieController();
+    setState(() {});
+  }
+
+  void _createChewieController() {
+    // final subtitles = [
+    //     Subtitle(
+    //       index: 0,
+    //       start: Duration.zero,
+    //       end: const Duration(seconds: 10),
+    //       text: 'Hello from subtitles',
+    //     ),
+    //     Subtitle(
+    //       index: 0,
+    //       start: const Duration(seconds: 10),
+    //       end: const Duration(seconds: 20),
+    //       text: 'Whats up? :)',
+    //     ),
+    //   ];
+
+    final subtitles = [
+      Subtitle(
+        index: 0,
+        start: Duration.zero,
+        end: const Duration(seconds: 10),
+        text: const TextSpan(
+          children: [
+            TextSpan(
+              text: 'Hello',
+              style: TextStyle(color: Colors.red, fontSize: 22),
+            ),
+            TextSpan(
+              text: ' from ',
+              style: TextStyle(color: Colors.green, fontSize: 20),
+            ),
+            TextSpan(
+              text: 'subtitles',
+              style: TextStyle(color: Colors.blue, fontSize: 18),
+            ),
+          ],
+        ),
+      ),
+      Subtitle(
+        index: 0,
+        start: const Duration(seconds: 10),
+        end: const Duration(seconds: 20),
+        text: 'Whats up? :)',
+        // text: const TextSpan(
+        //   text: 'Whats up? :)',
+        //   style: TextStyle(color: Colors.amber, fontSize: 22, fontStyle: FontStyle.italic),
+        // ),
+      ),
+    ];
+
+    _chewieController = ChewieController(
+      videoPlayerController: _videoPlayerController1,
+      autoPlay: true,
+      zoomAndPan: true,
+      looping: true,
+      progressIndicatorDelay:
+          bufferDelay != null ? Duration(milliseconds: bufferDelay!) : null,
+
+      additionalOptions: (context) {
+        return <OptionItem>[
+          OptionItem(
+            onTap: (context) => toggleVideo(),
+            iconData: Icons.live_tv_sharp,
+            title: 'Toggle Video Src',
+          ),
+        ];
+      },
+      subtitle: Subtitles(subtitles),
+      showSubtitles: true,
+      subtitleBuilder: (context, dynamic subtitle) => Container(
+        padding: const EdgeInsets.all(10.0),
+        child: subtitle is InlineSpan
+            ? RichText(text: subtitle)
+            : Text(
+                subtitle.toString(),
+                style: const TextStyle(color: Colors.black),
+              ),
+      ),
+
+      hideControlsTimer: const Duration(seconds: 1),
+
+      // Try playing around with some of these other options:
+
+      // showControls: false,
+      // materialProgressColors: ChewieProgressColors(
+      //   playedColor: Colors.red,
+      //   handleColor: Colors.blue,
+      //   backgroundColor: Colors.grey,
+      //   bufferedColor: Colors.lightGreen,
+      // ),
+      // placeholder: Container(
+      //   color: Colors.grey,
+      // ),
+      // autoInitialize: true,
+    );
+  }
+
+  int currPlayIndex = 0;
+
+  Future<void> 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: <Widget>[
+            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: <Widget>[
+                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: <Widget>[
+                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: <Widget>[
+                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<DelaySlider> createState() => _DelaySliderState();
+}
+
+class _DelaySliderState extends State<DelaySlider> {
+  int? delay;
+  bool saved = false;
+
+  @override
+  void initState() {
+    super.initState();
+    delay = widget.delay;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    const int max = 1000;
+    return ListTile(
+      title: Text(
+        "Progress indicator delay ${delay != null ? "${delay.toString()} MS" : ""}",
+      ),
+      subtitle: Slider(
+        value: delay != null ? (delay! / max) : 0,
+        onChanged: (value) async {
+          delay = (value * max).toInt();
+          setState(() {
+            saved = false;
+          });
+        },
+      ),
+      trailing: IconButton(
+        icon: const Icon(Icons.save),
+        onPressed: saved
+            ? null
+            : () {
+                widget.onSave(delay);
+                setState(() {
+                  saved = true;
+                });
+              },
+      ),
+    );
+  }
+}
diff --git a/example/lib/app/theme.dart b/example/lib/app/theme.dart
new file mode 100644
index 000000000..41ac7e4d0
--- /dev/null
+++ b/example/lib/app/theme.dart
@@ -0,0 +1,20 @@
+import 'package:flutter/material.dart';
+
+// ignore: avoid_classes_with_only_static_members
+class AppTheme {
+  static final light = ThemeData(
+    brightness: Brightness.light,
+    useMaterial3: true,
+    colorScheme: const ColorScheme.light(secondary: Colors.red),
+    disabledColor: Colors.grey.shade400,
+    visualDensity: VisualDensity.adaptivePlatformDensity,
+  );
+
+  static final dark = ThemeData(
+    brightness: Brightness.dark,
+    colorScheme: const ColorScheme.dark(secondary: Colors.red),
+    disabledColor: Colors.grey.shade400,
+    useMaterial3: true,
+    visualDensity: VisualDensity.adaptivePlatformDensity,
+  );
+}
diff --git a/example/lib/auto_rotate.dart b/example/lib/auto_rotate.dart
deleted file mode 100644
index add5622e4..000000000
--- a/example/lib/auto_rotate.dart
+++ /dev/null
@@ -1,245 +0,0 @@
-import 'package:auto_orientation/auto_orientation.dart';
-import 'package:chewie/chewie.dart';
-import 'package:chewie/src/chewie_player.dart';
-import 'package:flutter/cupertino.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
-import 'package:video_player/video_player.dart';
-
-void main() {
-  runApp(
-    ChewieDemo(),
-  );
-}
-
-class ChewieDemo extends StatefulWidget {
-  ChewieDemo({this.title = 'Chewie Demo'});
-
-  final String title;
-
-  @override
-  State<StatefulWidget> createState() {
-    return _ChewieDemoState();
-  }
-}
-
-class _ChewieDemoState extends State<ChewieDemo> {
-  TargetPlatform _platform;
-  VideoPlayerController _videoPlayerController1;
-  VideoPlayerController _videoPlayerController2;
-  ChewieController _chewieController;
-
-  @override
-  void initState() {
-    super.initState();
-    this.initializeAutoRotatePlayer();
-  }
-
-  Future<void> 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<double> animation,
-            Animation<double> 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: <Widget>[
-            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: <Widget>[
-                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: <Widget>[
-                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<StatefulWidget> createState() => _VideoScaffoldState();
-}
-
-class _VideoScaffoldState extends State<VideoScaffold> {
-  @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<StatefulWidget> createState() {
-    return _ChewieDemoState();
-  }
-}
-
-class _ChewieDemoState extends State<ChewieDemo> {
-  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<void> 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: <Widget>[
-            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: <Widget>[
-                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: <Widget>[
-                Expanded(
-                  child: FlatButton(
-                    onPressed: () {
-                      setState(() {
-                        _platform = TargetPlatform.android;
-                      });
-                    },
-                    child: Padding(
-                      child: Text("Android controls"),
-                      padding: EdgeInsets.symmetric(vertical: 16.0),
-                    ),
-                  ),
-                ),
-                Expanded(
-                  child: FlatButton(
-                    onPressed: () {
-                      setState(() {
-                        _platform = TargetPlatform.iOS;
-                      });
-                    },
-                    child: Padding(
-                      padding: EdgeInsets.symmetric(vertical: 16.0),
-                      child: Text("iOS controls"),
-                    ),
-                  ),
-                )
-              ],
-            )
-          ],
-        ),
-      ),
-    );
-  }
+  runApp(const ChewieDemo());
 }
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
index 7e0f68340..be412fe2a 100644
--- a/example/pubspec.yaml
+++ b/example/pubspec.yaml
@@ -1,30 +1,29 @@
 name: chewie_example
 description: An example of how to use the chewie for Flutter
+version: 1.0.0
+publish_to: none
+
+environment:
+  sdk: '>=3.6.0 <4.0.0'
+  flutter: ">=3.27.0"
 
 dependencies:
   chewie:
     path: ../
-  video_player: ^1.0.0
-  auto_orientation: ^1.0.6
   flutter:
     sdk: flutter
-
-  # The following adds the Cupertino Icons font to your application.
-  # Use with the CupertinoIcons class for iOS style icons.
-
-  cupertino_icons: ^1.0.0
+  video_player: ^2.9.3
 
 dev_dependencies:
   flutter_test:
     sdk: flutter
-
+  flutter_lints: ^5.0.0
 
 # For information on the generic Dart part of this file, see the
 # following page: https://www.dartlang.org/tools/pub/pubspec
 
 # The following section is specific to Flutter.
 flutter:
-
   # The following line ensures that the Material Icons font is
   # included with your application, so that you can use the icons in
   # the material Icons class.
diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart
index aadb1712a..922999316 100644
--- a/example/test/widget_test.dart
+++ b/example/test/widget_test.dart
@@ -5,12 +5,12 @@
 // gestures. You can also use WidgetTester to find child widgets in the widget
 // tree, read text, and verify that the values of widget properties are correct.
 
-import 'package:chewie_example/auto_rotate.dart';
+import 'package:chewie_example/app/app.dart';
 import 'package:flutter_test/flutter_test.dart';
 
 void main() {
   testWidgets('Counter increments smoke test', (WidgetTester tester) async {
     // Build our app and trigger a frame.
-    await tester.pumpWidget(ChewieDemo());
+    await tester.pumpWidget(const ChewieDemo());
   });
 }
diff --git a/lib/chewie.dart b/lib/chewie.dart
index ed95928a7..7061505ad 100644
--- a/lib/chewie.dart
+++ b/lib/chewie.dart
@@ -1,4 +1,9 @@
-library chewie;
+library;
 
 export 'src/chewie_player.dart';
 export 'src/chewie_progress_colors.dart';
+export 'src/cupertino/cupertino_controls.dart';
+export 'src/material/material_controls.dart';
+export 'src/material/material_desktop_controls.dart';
+export 'src/material/material_progress_bar.dart';
+export 'src/models/index.dart';
diff --git a/lib/src/animated_play_pause.dart b/lib/src/animated_play_pause.dart
new file mode 100644
index 000000000..e41111b7a
--- /dev/null
+++ b/lib/src/animated_play_pause.dart
@@ -0,0 +1,57 @@
+import 'package:flutter/material.dart';
+
+/// A widget that animates implicitly between a play and a pause icon.
+class AnimatedPlayPause extends StatefulWidget {
+  const AnimatedPlayPause({
+    super.key,
+    required this.playing,
+    this.size,
+    this.color,
+  });
+
+  final double? size;
+  final bool playing;
+  final Color? color;
+
+  @override
+  State<StatefulWidget> createState() => AnimatedPlayPauseState();
+}
+
+class AnimatedPlayPauseState extends State<AnimatedPlayPause>
+    with SingleTickerProviderStateMixin {
+  late final animationController = AnimationController(
+    vsync: this,
+    value: widget.playing ? 1 : 0,
+    duration: const Duration(milliseconds: 400),
+  );
+
+  @override
+  void didUpdateWidget(AnimatedPlayPause oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    if (widget.playing != oldWidget.playing) {
+      if (widget.playing) {
+        animationController.forward();
+      } else {
+        animationController.reverse();
+      }
+    }
+  }
+
+  @override
+  void dispose() {
+    animationController.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Center(
+      child: AnimatedIcon(
+        color: widget.color,
+        size: widget.size,
+        icon: AnimatedIcons.play_pause,
+        progress: animationController,
+      ),
+    );
+  }
+}
diff --git a/lib/src/center_play_button.dart b/lib/src/center_play_button.dart
new file mode 100644
index 000000000..c8a87067f
--- /dev/null
+++ b/lib/src/center_play_button.dart
@@ -0,0 +1,56 @@
+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..6c98a3caf 100644
--- a/lib/src/chewie_player.dart
+++ b/lib/src/chewie_player.dart
@@ -1,30 +1,32 @@
 import 'dart:async';
 
 import 'package:chewie/src/chewie_progress_colors.dart';
+import 'package:chewie/src/models/option_item.dart';
+import 'package:chewie/src/models/options_translation.dart';
+import 'package:chewie/src/models/subtitle_model.dart';
+import 'package:chewie/src/notifiers/player_notifier.dart';
 import 'package:chewie/src/player_with_controls.dart';
+import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
-import 'package:flutter/widgets.dart';
+import 'package:provider/provider.dart';
 import 'package:video_player/video_player.dart';
-import 'package:wakelock/wakelock.dart';
-
-typedef Widget ChewieRoutePageBuilder(
-  BuildContext context,
-  Animation<double> animation,
-  Animation<double> secondaryAnimation,
-  _ChewieControllerProvider controllerProvider,
-);
+import 'package:wakelock_plus/wakelock_plus.dart';
+
+typedef ChewieRoutePageBuilder =
+    Widget Function(
+      BuildContext context,
+      Animation<double> animation,
+      Animation<double> 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;
@@ -38,15 +40,20 @@ class Chewie extends StatefulWidget {
 class ChewieState extends State<Chewie> {
   bool _isFullScreen = false;
 
+  bool get isControllerFullScreen => widget.controller.isFullScreen;
+  late PlayerNotifier notifier;
+
   @override
   void initState() {
     super.initState();
     widget.controller.addListener(listener);
+    notifier = PlayerNotifier.init();
   }
 
   @override
   void dispose() {
     widget.controller.removeListener(listener);
+    notifier.dispose();
     super.dispose();
   }
 
@@ -56,33 +63,42 @@ class ChewieState extends State<Chewie> {
       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<void> listener() async {
+    if (isControllerFullScreen && !_isFullScreen) {
+      _isFullScreen = isControllerFullScreen;
       await _pushFullScreenWidget(context);
     } else if (_isFullScreen) {
-      Navigator.of(context, rootNavigator: true).pop();
+      Navigator.of(
+        context,
+        rootNavigator: widget.controller.useRootNavigator,
+      ).pop();
       _isFullScreen = false;
     }
   }
 
   @override
   Widget build(BuildContext context) {
-    return _ChewieControllerProvider(
+    return ChewieControllerProvider(
       controller: widget.controller,
-      child: PlayerWithControls(),
+      child: ChangeNotifierProvider<PlayerNotifier>.value(
+        value: notifier,
+        builder: (context, w) => const PlayerWithControls(),
+      ),
     );
   }
 
   Widget _buildFullScreenVideo(
     BuildContext context,
     Animation<double> animation,
-    _ChewieControllerProvider controllerProvider,
+    ChewieControllerProvider controllerProvider,
   ) {
     return Scaffold(
-      resizeToAvoidBottomPadding: false,
+      resizeToAvoidBottomInset: false,
       body: Container(
         alignment: Alignment.center,
         color: Colors.black,
@@ -95,11 +111,11 @@ class ChewieState extends State<Chewie> {
     BuildContext context,
     Animation<double> animation,
     Animation<double> secondaryAnimation,
-    _ChewieControllerProvider controllerProvider,
+    ChewieControllerProvider controllerProvider,
   ) {
     return AnimatedBuilder(
       animation: animation,
-      builder: (BuildContext context, Widget child) {
+      builder: (BuildContext context, Widget? child) {
         return _buildFullScreenVideo(context, animation, controllerProvider);
       },
     );
@@ -110,42 +126,64 @@ class ChewieState extends State<Chewie> {
     Animation<double> animation,
     Animation<double> secondaryAnimation,
   ) {
-    var controllerProvider = _ChewieControllerProvider(
+    final controllerProvider = ChewieControllerProvider(
       controller: widget.controller,
-      child: PlayerWithControls(),
+      child: ChangeNotifierProvider<PlayerNotifier>.value(
+        value: notifier,
+        builder: (context, w) => const PlayerWithControls(),
+      ),
     );
 
     if (widget.controller.routePageBuilder == null) {
       return _defaultRoutePageBuilder(
-          context, animation, secondaryAnimation, controllerProvider);
+        context,
+        animation,
+        secondaryAnimation,
+        controllerProvider,
+      );
     }
-    return widget.controller.routePageBuilder(
-        context, animation, secondaryAnimation, controllerProvider);
+    return widget.controller.routePageBuilder!(
+      context,
+      animation,
+      secondaryAnimation,
+      controllerProvider,
+    );
   }
 
   Future<dynamic> _pushFullScreenWidget(BuildContext context) async {
-    final TransitionRoute<Null> route = PageRouteBuilder<Null>(
+    final TransitionRoute<void> route = PageRouteBuilder<void>(
       pageBuilder: _fullScreenRoutePageBuilder,
     );
 
     onEnterFullScreen();
 
     if (!widget.controller.allowedScreenSleep) {
-      Wakelock.enable();
+      WakelockPlus.enable();
+    }
+
+    await Navigator.of(
+      context,
+      rootNavigator: widget.controller.useRootNavigator,
+    ).push(route);
+
+    if (kIsWeb) {
+      _reInitializeControllers();
     }
 
-    await Navigator.of(context, rootNavigator: true).push(route);
     _isFullScreen = false;
     widget.controller.exitFullScreen();
 
-    // The wakelock plugins checks whether it needs to perform an action internally,
-    // so we do not need to check Wakelock.isEnabled.
-    Wakelock.disable();
+    if (!widget.controller.allowedScreenSleep) {
+      WakelockPlus.disable();
+    }
 
-    SystemChrome.setEnabledSystemUIOverlays(
-        widget.controller.systemOverlaysAfterFullScreen);
+    SystemChrome.setEnabledSystemUIMode(
+      SystemUiMode.manual,
+      overlays: widget.controller.systemOverlaysAfterFullScreen,
+    );
     SystemChrome.setPreferredOrientations(
-        widget.controller.deviceOrientationsAfterFullScreen);
+      widget.controller.deviceOrientationsAfterFullScreen,
+    );
   }
 
   void onEnterFullScreen() {
@@ -153,43 +191,61 @@ class ChewieState extends State<Chewie> {
     final videoHeight =
         widget.controller.videoPlayerController.value.size.height;
 
-    if (widget.controller.systemOverlaysOnEnterFullScreen != null) {
-      /// Optional user preferred settings
-      SystemChrome.setEnabledSystemUIOverlays(
-          widget.controller.systemOverlaysOnEnterFullScreen);
-    } else {
-      /// Default behavior
-      SystemChrome.setEnabledSystemUIOverlays([]);
-    }
+    SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
+
+    // if (widget.controller.systemOverlaysOnEnterFullScreen != null) {
+    //   /// Optional user preferred settings
+    //   SystemChrome.setEnabledSystemUIMode(
+    //     SystemUiMode.manual,
+    //     overlays: widget.controller.systemOverlaysOnEnterFullScreen,
+    //   );
+    // } else {
+    //   /// Default behavior
+    //   SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values);
+    // }
 
     if (widget.controller.deviceOrientationsOnEnterFullScreen != null) {
       /// Optional user preferred settings
       SystemChrome.setPreferredOrientations(
-          widget.controller.deviceOrientationsOnEnterFullScreen);
+        widget.controller.deviceOrientationsOnEnterFullScreen!,
+      );
     } else {
+      final isLandscapeVideo = videoWidth > videoHeight;
+      final isPortraitVideo = videoWidth < videoHeight;
+
       /// Default behavior
       /// Video w > h means we force landscape
-      if (videoWidth > videoHeight) {
+      if (isLandscapeVideo) {
         SystemChrome.setPreferredOrientations([
           DeviceOrientation.landscapeLeft,
           DeviceOrientation.landscapeRight,
         ]);
       }
-
       /// Video h > w means we force portrait
-      else if (videoHeight > videoWidth) {
+      else if (isPortraitVideo) {
         SystemChrome.setPreferredOrientations([
           DeviceOrientation.portraitUp,
           DeviceOrientation.portraitDown,
         ]);
       }
-
       /// Otherwise if h == w (square video)
       else {
         SystemChrome.setPreferredOrientations(DeviceOrientation.values);
       }
     }
   }
+
+  ///When viewing full screen on web, returning from full screen causes original video to lose the picture.
+  ///We re initialise controllers for web only when returning from full screen
+  void _reInitializeControllers() {
+    final prevPosition = widget.controller.videoPlayerController.value.position;
+    widget.controller.videoPlayerController.initialize().then((_) async {
+      widget.controller._initialize();
+      widget.controller.videoPlayerController.seekTo(prevPosition);
+      await widget.controller.videoPlayerController.play();
+      widget.controller.videoPlayerController.pause();
+    });
+  }
 }
 
 /// The ChewieController is used to configure and drive the Chewie Player
@@ -204,35 +260,218 @@ class ChewieState extends State<Chewie> {
 /// `VideoPlayerController`.
 class ChewieController extends ChangeNotifier {
   ChewieController({
-    this.videoPlayerController,
+    required this.videoPlayerController,
+    this.optionsTranslation,
     this.aspectRatio,
     this.autoInitialize = false,
     this.autoPlay = false,
+    this.draggableProgressBar = true,
     this.startAt,
     this.looping = false,
     this.fullScreenByDefault = false,
     this.cupertinoProgressColors,
     this.materialProgressColors,
+    this.materialSeekButtonFadeDuration = const Duration(milliseconds: 300),
+    this.materialSeekButtonSize = 26,
     this.placeholder,
     this.overlay,
     this.showControlsOnInitialize = true,
+    this.showOptions = true,
+    this.optionsBuilder,
+    this.additionalOptions,
     this.showControls = true,
+    this.transformationController,
+    this.zoomAndPan = false,
+    this.maxScale = 2.5,
+    this.subtitle,
+    this.showSubtitles = false,
+    this.subtitleBuilder,
     this.customControls,
     this.errorBuilder,
+    this.bufferingBuilder,
     this.allowedScreenSleep = true,
     this.isLive = false,
     this.allowFullScreen = true,
     this.allowMuting = true,
+    this.allowPlaybackSpeedChanging = true,
+    this.useRootNavigator = true,
+    this.playbackSpeeds = const [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],
     this.systemOverlaysOnEnterFullScreen,
     this.deviceOrientationsOnEnterFullScreen,
     this.systemOverlaysAfterFullScreen = SystemUiOverlay.values,
     this.deviceOrientationsAfterFullScreen = DeviceOrientation.values,
-    this.routePageBuilder = null,
-  }) : assert(videoPlayerController != null,
-            'You must provide a controller to play a video') {
+    this.routePageBuilder,
+    this.progressIndicatorDelay,
+    this.hideControlsTimer = defaultHideControlsTimer,
+    this.controlsSafeAreaMinimum = EdgeInsets.zero,
+    this.pauseOnBackgroundTap = false,
+  }) : assert(
+         playbackSpeeds.every((speed) => speed > 0),
+         'The playbackSpeeds values must all be greater than 0',
+       ) {
     _initialize();
   }
 
+  ChewieController copyWith({
+    VideoPlayerController? videoPlayerController,
+    OptionsTranslation? optionsTranslation,
+    double? aspectRatio,
+    bool? autoInitialize,
+    bool? autoPlay,
+    bool? draggableProgressBar,
+    Duration? startAt,
+    bool? looping,
+    bool? fullScreenByDefault,
+    ChewieProgressColors? cupertinoProgressColors,
+    ChewieProgressColors? materialProgressColors,
+    Duration? materialSeekButtonFadeDuration,
+    double? materialSeekButtonSize,
+    Widget? placeholder,
+    Widget? overlay,
+    bool? showControlsOnInitialize,
+    bool? showOptions,
+    Future<void> Function(BuildContext, List<OptionItem>)? optionsBuilder,
+    List<OptionItem> Function(BuildContext)? additionalOptions,
+    bool? showControls,
+    TransformationController? transformationController,
+    bool? zoomAndPan,
+    double? maxScale,
+    Subtitles? subtitle,
+    bool? showSubtitles,
+    Widget Function(BuildContext, dynamic)? subtitleBuilder,
+    Widget? customControls,
+    WidgetBuilder? bufferingBuilder,
+    Widget Function(BuildContext, String)? errorBuilder,
+    bool? allowedScreenSleep,
+    bool? isLive,
+    bool? allowFullScreen,
+    bool? allowMuting,
+    bool? allowPlaybackSpeedChanging,
+    bool? useRootNavigator,
+    Duration? hideControlsTimer,
+    EdgeInsets? controlsSafeAreaMinimum,
+    List<double>? playbackSpeeds,
+    List<SystemUiOverlay>? systemOverlaysOnEnterFullScreen,
+    List<DeviceOrientation>? deviceOrientationsOnEnterFullScreen,
+    List<SystemUiOverlay>? systemOverlaysAfterFullScreen,
+    List<DeviceOrientation>? deviceOrientationsAfterFullScreen,
+    Duration? progressIndicatorDelay,
+    Widget Function(
+      BuildContext,
+      Animation<double>,
+      Animation<double>,
+      ChewieControllerProvider,
+    )?
+    routePageBuilder,
+    bool? pauseOnBackgroundTap,
+  }) {
+    return ChewieController(
+      draggableProgressBar: draggableProgressBar ?? this.draggableProgressBar,
+      videoPlayerController:
+          videoPlayerController ?? this.videoPlayerController,
+      optionsTranslation: optionsTranslation ?? this.optionsTranslation,
+      aspectRatio: aspectRatio ?? this.aspectRatio,
+      autoInitialize: autoInitialize ?? this.autoInitialize,
+      autoPlay: autoPlay ?? this.autoPlay,
+      startAt: startAt ?? this.startAt,
+      looping: looping ?? this.looping,
+      fullScreenByDefault: fullScreenByDefault ?? this.fullScreenByDefault,
+      cupertinoProgressColors:
+          cupertinoProgressColors ?? this.cupertinoProgressColors,
+      materialProgressColors:
+          materialProgressColors ?? this.materialProgressColors,
+      zoomAndPan: zoomAndPan ?? this.zoomAndPan,
+      maxScale: maxScale ?? this.maxScale,
+      controlsSafeAreaMinimum:
+          controlsSafeAreaMinimum ?? this.controlsSafeAreaMinimum,
+      transformationController:
+          transformationController ?? this.transformationController,
+      materialSeekButtonFadeDuration:
+          materialSeekButtonFadeDuration ?? this.materialSeekButtonFadeDuration,
+      materialSeekButtonSize:
+          materialSeekButtonSize ?? this.materialSeekButtonSize,
+      placeholder: placeholder ?? this.placeholder,
+      overlay: overlay ?? this.overlay,
+      showControlsOnInitialize:
+          showControlsOnInitialize ?? this.showControlsOnInitialize,
+      showOptions: showOptions ?? this.showOptions,
+      optionsBuilder: optionsBuilder ?? this.optionsBuilder,
+      additionalOptions: additionalOptions ?? this.additionalOptions,
+      showControls: showControls ?? this.showControls,
+      showSubtitles: showSubtitles ?? this.showSubtitles,
+      subtitle: subtitle ?? this.subtitle,
+      subtitleBuilder: subtitleBuilder ?? this.subtitleBuilder,
+      customControls: customControls ?? this.customControls,
+      errorBuilder: errorBuilder ?? this.errorBuilder,
+      bufferingBuilder: bufferingBuilder ?? this.bufferingBuilder,
+      allowedScreenSleep: allowedScreenSleep ?? this.allowedScreenSleep,
+      isLive: isLive ?? this.isLive,
+      allowFullScreen: allowFullScreen ?? this.allowFullScreen,
+      allowMuting: allowMuting ?? this.allowMuting,
+      allowPlaybackSpeedChanging:
+          allowPlaybackSpeedChanging ?? this.allowPlaybackSpeedChanging,
+      useRootNavigator: useRootNavigator ?? this.useRootNavigator,
+      playbackSpeeds: playbackSpeeds ?? this.playbackSpeeds,
+      systemOverlaysOnEnterFullScreen:
+          systemOverlaysOnEnterFullScreen ??
+          this.systemOverlaysOnEnterFullScreen,
+      deviceOrientationsOnEnterFullScreen:
+          deviceOrientationsOnEnterFullScreen ??
+          this.deviceOrientationsOnEnterFullScreen,
+      systemOverlaysAfterFullScreen:
+          systemOverlaysAfterFullScreen ?? this.systemOverlaysAfterFullScreen,
+      deviceOrientationsAfterFullScreen:
+          deviceOrientationsAfterFullScreen ??
+          this.deviceOrientationsAfterFullScreen,
+      routePageBuilder: routePageBuilder ?? this.routePageBuilder,
+      hideControlsTimer: hideControlsTimer ?? this.hideControlsTimer,
+      progressIndicatorDelay:
+          progressIndicatorDelay ?? this.progressIndicatorDelay,
+      pauseOnBackgroundTap: pauseOnBackgroundTap ?? this.pauseOnBackgroundTap,
+    );
+  }
+
+  static const defaultHideControlsTimer = Duration(seconds: 3);
+
+  /// If false, the options button in MaterialUI and MaterialDesktopUI
+  /// won't be shown.
+  final bool showOptions;
+
+  /// Pass your translations for the options like:
+  /// - PlaybackSpeed
+  /// - Subtitles
+  /// - Cancel
+  ///
+  /// Buttons
+  ///
+  /// These are required for the default `OptionItem`'s
+  final OptionsTranslation? optionsTranslation;
+
+  /// Build your own options with default chewieOptions shiped through
+  /// the builder method. Just add your own options to the Widget
+  /// you'll build. If you want to hide the chewieOptions, just leave them
+  /// out from your Widget.
+  final Future<void> Function(
+    BuildContext context,
+    List<OptionItem> chewieOptions,
+  )?
+  optionsBuilder;
+
+  /// Add your own additional options on top of chewie options
+  final List<OptionItem> Function(BuildContext context)? additionalOptions;
+
+  /// Define here your own Widget on how your n'th subtitle will look like
+  Widget Function(BuildContext context, dynamic subtitle)? subtitleBuilder;
+
+  /// Add a List of Subtitles here in `Subtitles.subtitle`
+  Subtitles? subtitle;
+
+  /// Determines whether subtitles should be shown by default when the video starts.
+  ///
+  /// If set to `true`, subtitles will be displayed automatically when the video
+  /// begins playing. If set to `false`, subtitles will be hidden by default.
+  bool showSubtitles;
+
   /// The controller for the video you want to play
   final VideoPlayerController videoPlayerController;
 
@@ -242,46 +481,72 @@ class ChewieController extends ChangeNotifier {
   /// Play the video as soon as it's displayed
   final bool autoPlay;
 
+  /// Non-Draggable Progress Bar
+  final bool draggableProgressBar;
+
   /// Start video at a certain position
-  final Duration startAt;
+  final Duration? startAt;
 
   /// Whether or not the video should loop
   final bool looping;
 
-  /// Weather or not to show the controls when initializing the widget.
+  /// Wether or not to show the controls when initializing the widget.
   final bool showControlsOnInitialize;
 
   /// Whether or not to show the controls at all
   final bool showControls;
 
+  /// Controller to pass into the [InteractiveViewer] component.
+  /// If it is required to control the transformation only via the controller,
+  /// `zoomAndPan` should be set to false.
+  final TransformationController? transformationController;
+
+  /// Whether or not to allow zooming and panning.
+  /// This can still be false, and the `transformationController` can be used to control the
+  /// transformation.
+  final bool zoomAndPan;
+
+  /// Max scale when zooming
+  final double maxScale;
+
   /// Defines customised controls. Check [MaterialControls] or
   /// [CupertinoControls] for reference.
-  final Widget customControls;
+  final Widget? customControls;
 
-  /// When the video playback runs  into an error, you can build a custom
+  /// When the video playback runs into an error, you can build a custom
   /// error message.
-  final Widget Function(BuildContext context, String errorMessage) errorBuilder;
+  final Widget Function(BuildContext context, String errorMessage)?
+  errorBuilder;
+
+  /// When the video is buffering, you can build a custom widget.
+  final WidgetBuilder? bufferingBuilder;
 
   /// The Aspect Ratio of the Video. Important to get the correct size of the
   /// video!
   ///
   /// Will fallback to fitting within the space allowed.
-  final double aspectRatio;
+  final double? aspectRatio;
 
   /// The colors to use for controls on iOS. By default, the iOS player uses
   /// colors sampled from the original iOS 11 designs.
-  final ChewieProgressColors cupertinoProgressColors;
+  final ChewieProgressColors? cupertinoProgressColors;
 
   /// The colors to use for the Material Progress Bar. By default, the Material
   /// player uses the colors from your Theme.
-  final ChewieProgressColors materialProgressColors;
+  final ChewieProgressColors? materialProgressColors;
+
+  // The duration of the fade animation for the seek button (Material Player only)
+  final Duration materialSeekButtonFadeDuration;
+
+  // The size of the seek button for the Material Player only
+  final double materialSeekButtonSize;
 
   /// The placeholder is displayed underneath the Video before it is initialized
   /// or played.
-  final Widget placeholder;
+  final Widget? placeholder;
 
   /// A widget which is placed between the video and the controls
-  final Widget overlay;
+  final Widget? overlay;
 
   /// Defines if the player will start in fullscreen when play is pressed
   final bool fullScreenByDefault;
@@ -289,7 +554,7 @@ class ChewieController extends ChangeNotifier {
   /// Defines if the player will sleep in fullscreen or not
   final bool allowedScreenSleep;
 
-  /// Defines if the controls should be for live stream video
+  /// Defines if the controls should be shown for live stream video
   final bool isLive;
 
   /// Defines if the fullscreen control should be shown
@@ -298,11 +563,23 @@ class ChewieController extends ChangeNotifier {
   /// Defines if the mute control should be shown
   final bool allowMuting;
 
+  /// Defines if the playback speed control should be shown
+  final bool allowPlaybackSpeedChanging;
+
+  /// Defines if push/pop navigations use the rootNavigator
+  final bool useRootNavigator;
+
+  /// Defines the [Duration] before the video controls are hidden. By default, this is set to three seconds.
+  final Duration hideControlsTimer;
+
+  /// Defines the set of allowed playback speeds user can change
+  final List<double> playbackSpeeds;
+
   /// Defines the system overlays visible on entering fullscreen
-  final List<SystemUiOverlay> systemOverlaysOnEnterFullScreen;
+  final List<SystemUiOverlay>? systemOverlaysOnEnterFullScreen;
 
   /// Defines the set of allowed device orientations on entering fullscreen
-  final List<DeviceOrientation> deviceOrientationsOnEnterFullScreen;
+  final List<DeviceOrientation>? deviceOrientationsOnEnterFullScreen;
 
   /// Defines the system overlays visible after exiting fullscreen
   final List<SystemUiOverlay> systemOverlaysAfterFullScreen;
@@ -311,11 +588,21 @@ class ChewieController extends ChangeNotifier {
   final List<DeviceOrientation> deviceOrientationsAfterFullScreen;
 
   /// Defines a custom RoutePageBuilder for the fullscreen
-  final ChewieRoutePageBuilder routePageBuilder;
+  final ChewieRoutePageBuilder? routePageBuilder;
+
+  /// Defines a delay in milliseconds between entering buffering state and displaying the loading spinner. Set null (default) to disable it.
+  final Duration? progressIndicatorDelay;
+
+  /// Adds additional padding to the controls' [SafeArea] as desired.
+  /// Defaults to [EdgeInsets.zero].
+  final EdgeInsets controlsSafeAreaMinimum;
+
+  /// Defines if the player should pause when the background is tapped
+  final bool pauseOnBackgroundTap;
 
   static ChewieController of(BuildContext context) {
     final chewieControllerProvider =
-        context.dependOnInheritedWidgetOfExactType<_ChewieControllerProvider>();
+        context.dependOnInheritedWidgetOfExactType<ChewieControllerProvider>()!;
 
     return chewieControllerProvider.controller;
   }
@@ -326,11 +613,11 @@ class ChewieController extends ChangeNotifier {
 
   bool get isPlaying => videoPlayerController.value.isPlaying;
 
-  Future _initialize() async {
+  Future<dynamic> _initialize() async {
     await videoPlayerController.setLooping(looping);
 
     if ((autoInitialize || autoPlay) &&
-        !videoPlayerController.value.initialized) {
+        !videoPlayerController.value.isInitialized) {
       await videoPlayerController.initialize();
     }
 
@@ -343,7 +630,7 @@ class ChewieController extends ChangeNotifier {
     }
 
     if (startAt != null) {
-      await videoPlayerController.seekTo(startAt);
+      await videoPlayerController.seekTo(startAt!);
     }
 
     if (fullScreenByDefault) {
@@ -351,7 +638,7 @@ class ChewieController extends ChangeNotifier {
     }
   }
 
-  void _fullScreenListener() async {
+  Future<void> _fullScreenListener() async {
     if (videoPlayerController.value.isPlaying && !_isFullScreen) {
       enterFullScreen();
       videoPlayerController.removeListener(_fullScreenListener);
@@ -381,6 +668,7 @@ class ChewieController extends ChangeNotifier {
     await videoPlayerController.play();
   }
 
+  // ignore: avoid_positional_boolean_parameters
   Future<void> setLooping(bool looping) async {
     await videoPlayerController.setLooping(looping);
   }
@@ -396,20 +684,22 @@ class ChewieController extends ChangeNotifier {
   Future<void> setVolume(double volume) async {
     await videoPlayerController.setVolume(volume);
   }
+
+  void setSubtitle(List<Subtitle> newSubtitle) {
+    subtitle = Subtitles(newSubtitle);
+  }
 }
 
-class _ChewieControllerProvider extends InheritedWidget {
-  const _ChewieControllerProvider({
-    Key key,
-    @required this.controller,
-    @required Widget child,
-  })  : assert(controller != null),
-        assert(child != null),
-        super(key: key, child: child);
+class ChewieControllerProvider extends InheritedWidget {
+  const ChewieControllerProvider({
+    super.key,
+    required this.controller,
+    required super.child,
+  });
 
   final ChewieController controller;
 
   @override
-  bool updateShouldNotify(_ChewieControllerProvider old) =>
-      controller != old.controller;
+  bool updateShouldNotify(ChewieControllerProvider oldWidget) =>
+      controller != oldWidget.controller;
 }
diff --git a/lib/src/chewie_progress_colors.dart b/lib/src/chewie_progress_colors.dart
index 9f73717c4..d0187d0ea 100644
--- a/lib/src/chewie_progress_colors.dart
+++ b/lib/src/chewie_progress_colors.dart
@@ -2,14 +2,14 @@ import 'package:flutter/rendering.dart';
 
 class ChewieProgressColors {
   ChewieProgressColors({
-    Color playedColor: const Color.fromRGBO(255, 0, 0, 0.7),
-    Color bufferedColor: const Color.fromRGBO(30, 30, 200, 0.2),
-    Color handleColor: const Color.fromRGBO(200, 200, 200, 1.0),
-    Color backgroundColor: const Color.fromRGBO(200, 200, 200, 0.5),
-  })  : playedPaint = Paint()..color = playedColor,
-        bufferedPaint = Paint()..color = bufferedColor,
-        handlePaint = Paint()..color = handleColor,
-        backgroundPaint = Paint()..color = backgroundColor;
+    Color playedColor = const Color.fromRGBO(255, 0, 0, 0.7),
+    Color bufferedColor = const Color.fromRGBO(30, 30, 200, 0.2),
+    Color handleColor = const Color.fromRGBO(200, 200, 200, 1.0),
+    Color backgroundColor = const Color.fromRGBO(200, 200, 200, 0.5),
+  }) : playedPaint = Paint()..color = playedColor,
+       bufferedPaint = Paint()..color = bufferedColor,
+       handlePaint = Paint()..color = handleColor,
+       backgroundPaint = Paint()..color = backgroundColor;
 
   final Paint playedPaint;
   final Paint bufferedPaint;
diff --git a/lib/src/cupertino/cupertino_controls.dart b/lib/src/cupertino/cupertino_controls.dart
new file mode 100644
index 000000000..5029b7d65
--- /dev/null
+++ b/lib/src/cupertino/cupertino_controls.dart
@@ -0,0 +1,817 @@
+import 'dart:async';
+import 'dart:math' as math;
+import 'dart:ui' as ui;
+
+import 'package:chewie/src/animated_play_pause.dart';
+import 'package:chewie/src/center_play_button.dart';
+import 'package:chewie/src/chewie_player.dart';
+import 'package:chewie/src/chewie_progress_colors.dart';
+import 'package:chewie/src/cupertino/cupertino_progress_bar.dart';
+import 'package:chewie/src/cupertino/widgets/cupertino_options_dialog.dart';
+import 'package:chewie/src/helpers/utils.dart';
+import 'package:chewie/src/models/option_item.dart';
+import 'package:chewie/src/models/subtitle_model.dart';
+import 'package:chewie/src/notifiers/index.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import 'package:video_player/video_player.dart';
+
+class CupertinoControls extends StatefulWidget {
+  const CupertinoControls({
+    required this.backgroundColor,
+    required this.iconColor,
+    this.showPlayButton = true,
+    super.key,
+  });
+
+  final Color backgroundColor;
+  final Color iconColor;
+  final bool showPlayButton;
+
+  @override
+  State<StatefulWidget> createState() {
+    return _CupertinoControlsState();
+  }
+}
+
+class _CupertinoControlsState extends State<CupertinoControls>
+    with SingleTickerProviderStateMixin {
+  late PlayerNotifier notifier;
+  late VideoPlayerValue _latestValue;
+  double? _latestVolume;
+  Timer? _hideTimer;
+  final marginSize = 5.0;
+  Timer? _expandCollapseTimer;
+  Timer? _initTimer;
+  bool _dragging = false;
+  Duration? _subtitlesPosition;
+  bool _subtitleOn = false;
+  Timer? _bufferingDisplayTimer;
+  bool _displayBufferingIndicator = false;
+  double selectedSpeed = 1.0;
+  late VideoPlayerController controller;
+
+  // We know that _chewieController is set in didChangeDependencies
+  ChewieController get chewieController => _chewieController!;
+  ChewieController? _chewieController;
+
+  @override
+  void initState() {
+    super.initState();
+    notifier = Provider.of<PlayerNotifier>(context, listen: false);
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    if (_latestValue.hasError) {
+      return chewieController.errorBuilder != null
+          ? chewieController.errorBuilder!(
+            context,
+            chewieController.videoPlayerController.value.errorDescription!,
+          )
+          : const Center(
+            child: Icon(
+              CupertinoIcons.exclamationmark_circle,
+              color: Colors.white,
+              size: 42,
+            ),
+          );
+    }
+
+    final backgroundColor = widget.backgroundColor;
+    final iconColor = widget.iconColor;
+    final orientation = MediaQuery.of(context).orientation;
+    final barHeight = orientation == Orientation.portrait ? 30.0 : 47.0;
+    final buttonPadding = orientation == Orientation.portrait ? 16.0 : 24.0;
+
+    return MouseRegion(
+      onHover: (_) => _cancelAndRestartTimer(),
+      child: GestureDetector(
+        onTap: () => _cancelAndRestartTimer(),
+        child: AbsorbPointer(
+          absorbing: notifier.hideStuff,
+          child: Stack(
+            children: [
+              if (_displayBufferingIndicator)
+                _chewieController?.bufferingBuilder?.call(context) ??
+                    const Center(child: CircularProgressIndicator())
+              else
+                _buildHitArea(),
+              Column(
+                mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                children: <Widget>[
+                  _buildTopBar(
+                    backgroundColor,
+                    iconColor,
+                    barHeight,
+                    buttonPadding,
+                  ),
+                  const Spacer(),
+                  if (_subtitleOn)
+                    Transform.translate(
+                      offset: Offset(
+                        0.0,
+                        notifier.hideStuff ? barHeight * 0.8 : 0.0,
+                      ),
+                      child: _buildSubtitles(chewieController.subtitle!),
+                    ),
+                  _buildBottomBar(backgroundColor, iconColor, barHeight),
+                ],
+              ),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+
+  @override
+  void dispose() {
+    _dispose();
+    super.dispose();
+  }
+
+  void _dispose() {
+    controller.removeListener(_updateState);
+    _hideTimer?.cancel();
+    _expandCollapseTimer?.cancel();
+    _initTimer?.cancel();
+  }
+
+  @override
+  void didChangeDependencies() {
+    final oldController = _chewieController;
+    _chewieController = ChewieController.of(context);
+    controller = chewieController.videoPlayerController;
+
+    if (oldController != chewieController) {
+      _dispose();
+      _initialize();
+    }
+
+    super.didChangeDependencies();
+  }
+
+  GestureDetector _buildOptionsButton(Color iconColor, double barHeight) {
+    final options = <OptionItem>[];
+
+    if (chewieController.additionalOptions != null &&
+        chewieController.additionalOptions!(context).isNotEmpty) {
+      options.addAll(chewieController.additionalOptions!(context));
+    }
+
+    return GestureDetector(
+      onTap: () async {
+        _hideTimer?.cancel();
+
+        if (chewieController.optionsBuilder != null) {
+          await chewieController.optionsBuilder!(context, options);
+        } else {
+          await showCupertinoModalPopup<OptionItem>(
+            context: context,
+            semanticsDismissible: true,
+            useRootNavigator: chewieController.useRootNavigator,
+            builder:
+                (context) => CupertinoOptionsDialog(
+                  options: options,
+                  cancelButtonText:
+                      chewieController.optionsTranslation?.cancelButtonText,
+                ),
+          );
+          if (_latestValue.isPlaying) {
+            _startHideTimer();
+          }
+        }
+      },
+      child: Container(
+        height: barHeight,
+        color: Colors.transparent,
+        padding: const EdgeInsets.only(left: 4.0, right: 8.0),
+        margin: const EdgeInsets.only(right: 6.0),
+        child: Icon(Icons.more_vert, color: iconColor, size: 18),
+      ),
+    );
+  }
+
+  Widget _buildSubtitles(Subtitles subtitles) {
+    if (!_subtitleOn) {
+      return const SizedBox();
+    }
+    if (_subtitlesPosition == null) {
+      return const SizedBox();
+    }
+    final currentSubtitle = subtitles.getByPosition(_subtitlesPosition!);
+    if (currentSubtitle.isEmpty) {
+      return const SizedBox();
+    }
+
+    if (chewieController.subtitleBuilder != null) {
+      return chewieController.subtitleBuilder!(
+        context,
+        currentSubtitle.first!.text,
+      );
+    }
+
+    return Padding(
+      padding: EdgeInsets.only(left: marginSize, right: marginSize),
+      child: Container(
+        padding: const EdgeInsets.all(5),
+        decoration: BoxDecoration(
+          color: const Color(0x96000000),
+          borderRadius: BorderRadius.circular(10.0),
+        ),
+        child: Text(
+          currentSubtitle.first!.text.toString(),
+          style: const TextStyle(fontSize: 18),
+          textAlign: TextAlign.center,
+        ),
+      ),
+    );
+  }
+
+  Widget _buildBottomBar(
+    Color backgroundColor,
+    Color iconColor,
+    double barHeight,
+  ) {
+    return SafeArea(
+      bottom: chewieController.isFullScreen,
+      minimum: chewieController.controlsSafeAreaMinimum,
+      child: AnimatedOpacity(
+        opacity: notifier.hideStuff ? 0.0 : 1.0,
+        duration: const Duration(milliseconds: 300),
+        child: Container(
+          color: Colors.transparent,
+          alignment: Alignment.bottomCenter,
+          margin: EdgeInsets.all(marginSize),
+          child: ClipRRect(
+            borderRadius: BorderRadius.circular(10.0),
+            child: BackdropFilter(
+              filter: ui.ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
+              child: Container(
+                height: barHeight,
+                color: backgroundColor,
+                child:
+                    chewieController.isLive
+                        ? Row(
+                          mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                          children: <Widget>[
+                            _buildPlayPause(controller, iconColor, barHeight),
+                            _buildLive(iconColor),
+                          ],
+                        )
+                        : Row(
+                          children: <Widget>[
+                            _buildSkipBack(iconColor, barHeight),
+                            _buildPlayPause(controller, iconColor, barHeight),
+                            _buildSkipForward(iconColor, barHeight),
+                            _buildPosition(iconColor),
+                            _buildProgressBar(),
+                            _buildRemaining(iconColor),
+                            _buildSubtitleToggle(iconColor, barHeight),
+                            if (chewieController.allowPlaybackSpeedChanging)
+                              _buildSpeedButton(
+                                controller,
+                                iconColor,
+                                barHeight,
+                              ),
+                            if (chewieController.additionalOptions != null &&
+                                chewieController
+                                    .additionalOptions!(context)
+                                    .isNotEmpty)
+                              _buildOptionsButton(iconColor, barHeight),
+                          ],
+                        ),
+              ),
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+
+  Widget _buildLive(Color iconColor) {
+    return Padding(
+      padding: const EdgeInsets.only(right: 12.0),
+      child: Text('LIVE', style: TextStyle(color: iconColor, fontSize: 12.0)),
+    );
+  }
+
+  GestureDetector _buildExpandButton(
+    Color backgroundColor,
+    Color iconColor,
+    double barHeight,
+    double buttonPadding,
+  ) {
+    return GestureDetector(
+      onTap: _onExpandCollapse,
+      child: AnimatedOpacity(
+        opacity: notifier.hideStuff ? 0.0 : 1.0,
+        duration: const Duration(milliseconds: 300),
+        child: ClipRRect(
+          borderRadius: BorderRadius.circular(10.0),
+          child: BackdropFilter(
+            filter: ui.ImageFilter.blur(sigmaX: 10.0),
+            child: Container(
+              height: barHeight,
+              padding: EdgeInsets.only(
+                left: buttonPadding,
+                right: buttonPadding,
+              ),
+              color: backgroundColor,
+              child: Center(
+                child: Icon(
+                  chewieController.isFullScreen
+                      ? CupertinoIcons.arrow_down_right_arrow_up_left
+                      : CupertinoIcons.arrow_up_left_arrow_down_right,
+                  color: iconColor,
+                  size: 16,
+                ),
+              ),
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+
+  Widget _buildHitArea() {
+    final bool isFinished =
+        (_latestValue.position >= _latestValue.duration) &&
+        _latestValue.duration.inSeconds > 0;
+    final bool showPlayButton =
+        widget.showPlayButton && !_latestValue.isPlaying && !_dragging;
+
+    return GestureDetector(
+      onTap:
+          _latestValue.isPlaying
+              ? _chewieController?.pauseOnBackgroundTap ?? false
+                  ? () {
+                    _playPause();
+
+                    setState(() {
+                      notifier.hideStuff = true;
+                    });
+                  }
+                  : _cancelAndRestartTimer
+              : () {
+                _hideTimer?.cancel();
+
+                setState(() {
+                  notifier.hideStuff = false;
+                });
+              },
+      child: CenterPlayButton(
+        backgroundColor: widget.backgroundColor,
+        iconColor: widget.iconColor,
+        isFinished: isFinished,
+        isPlaying: controller.value.isPlaying,
+        show: showPlayButton,
+        onPressed: _playPause,
+      ),
+    );
+  }
+
+  GestureDetector _buildMuteButton(
+    VideoPlayerController controller,
+    Color backgroundColor,
+    Color iconColor,
+    double barHeight,
+    double buttonPadding,
+  ) {
+    return GestureDetector(
+      onTap: () {
+        _cancelAndRestartTimer();
+
+        if (_latestValue.volume == 0) {
+          controller.setVolume(_latestVolume ?? 0.5);
+        } else {
+          _latestVolume = controller.value.volume;
+          controller.setVolume(0.0);
+        }
+      },
+      child: AnimatedOpacity(
+        opacity: notifier.hideStuff ? 0.0 : 1.0,
+        duration: const Duration(milliseconds: 300),
+        child: ClipRRect(
+          borderRadius: BorderRadius.circular(10.0),
+          child: BackdropFilter(
+            filter: ui.ImageFilter.blur(sigmaX: 10.0),
+            child: ColoredBox(
+              color: backgroundColor,
+              child: Container(
+                height: barHeight,
+                padding: EdgeInsets.only(
+                  left: buttonPadding,
+                  right: buttonPadding,
+                ),
+                child: Icon(
+                  _latestValue.volume > 0 ? Icons.volume_up : Icons.volume_off,
+                  color: iconColor,
+                  size: 16,
+                ),
+              ),
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+
+  GestureDetector _buildPlayPause(
+    VideoPlayerController controller,
+    Color iconColor,
+    double barHeight,
+  ) {
+    return GestureDetector(
+      onTap: _playPause,
+      child: Container(
+        height: barHeight,
+        color: Colors.transparent,
+        padding: const EdgeInsets.only(left: 6.0, right: 6.0),
+        child: AnimatedPlayPause(
+          color: widget.iconColor,
+          playing: controller.value.isPlaying,
+        ),
+      ),
+    );
+  }
+
+  Widget _buildPosition(Color iconColor) {
+    final position = _latestValue.position;
+
+    return Padding(
+      padding: const EdgeInsets.only(right: 12.0),
+      child: Text(
+        formatDuration(position),
+        style: TextStyle(color: iconColor, fontSize: 12.0),
+      ),
+    );
+  }
+
+  Widget _buildRemaining(Color iconColor) {
+    final position = _latestValue.duration - _latestValue.position;
+
+    return Padding(
+      padding: const EdgeInsets.symmetric(horizontal: 12.0),
+      child: Text(
+        '-${formatDuration(position)}',
+        style: TextStyle(color: iconColor, fontSize: 12.0),
+      ),
+    );
+  }
+
+  Widget _buildSubtitleToggle(Color iconColor, double barHeight) {
+    //if don't have subtitle hiden button
+    if (chewieController.subtitle?.isEmpty ?? true) {
+      return const SizedBox();
+    }
+    return GestureDetector(
+      onTap: _subtitleToggle,
+      child: Container(
+        height: barHeight,
+        color: Colors.transparent,
+        margin: const EdgeInsets.only(right: 10.0),
+        padding: const EdgeInsets.only(left: 6.0, right: 6.0),
+        child: Icon(
+          Icons.subtitles,
+          color: _subtitleOn ? iconColor : Colors.grey[700],
+          size: 16.0,
+        ),
+      ),
+    );
+  }
+
+  void _subtitleToggle() {
+    setState(() {
+      _subtitleOn = !_subtitleOn;
+    });
+  }
+
+  GestureDetector _buildSkipBack(Color iconColor, double barHeight) {
+    return GestureDetector(
+      onTap: _skipBack,
+      child: Container(
+        height: barHeight,
+        color: Colors.transparent,
+        margin: const EdgeInsets.only(left: 10.0),
+        padding: const EdgeInsets.only(left: 6.0, right: 6.0),
+        child: Icon(CupertinoIcons.gobackward_15, color: iconColor, size: 18.0),
+      ),
+    );
+  }
+
+  GestureDetector _buildSkipForward(Color iconColor, double barHeight) {
+    return GestureDetector(
+      onTap: _skipForward,
+      child: Container(
+        height: barHeight,
+        color: Colors.transparent,
+        padding: const EdgeInsets.only(left: 6.0, right: 8.0),
+        margin: const EdgeInsets.only(right: 8.0),
+        child: Icon(CupertinoIcons.goforward_15, color: iconColor, size: 18.0),
+      ),
+    );
+  }
+
+  GestureDetector _buildSpeedButton(
+    VideoPlayerController controller,
+    Color iconColor,
+    double barHeight,
+  ) {
+    return GestureDetector(
+      onTap: () async {
+        _hideTimer?.cancel();
+
+        final chosenSpeed = await showCupertinoModalPopup<double>(
+          context: context,
+          semanticsDismissible: true,
+          useRootNavigator: chewieController.useRootNavigator,
+          builder:
+              (context) => _PlaybackSpeedDialog(
+                speeds: chewieController.playbackSpeeds,
+                selected: _latestValue.playbackSpeed,
+              ),
+        );
+
+        if (chosenSpeed != null) {
+          controller.setPlaybackSpeed(chosenSpeed);
+
+          selectedSpeed = chosenSpeed;
+        }
+
+        if (_latestValue.isPlaying) {
+          _startHideTimer();
+        }
+      },
+      child: Container(
+        height: barHeight,
+        color: Colors.transparent,
+        padding: const EdgeInsets.only(left: 6.0, right: 8.0),
+        margin: const EdgeInsets.only(right: 8.0),
+        child: Transform(
+          alignment: Alignment.center,
+          transform:
+              Matrix4.skewY(0.0)
+                ..rotateX(math.pi)
+                ..rotateZ(math.pi * 0.8),
+          child: Icon(Icons.speed, color: iconColor, size: 18.0),
+        ),
+      ),
+    );
+  }
+
+  Widget _buildTopBar(
+    Color backgroundColor,
+    Color iconColor,
+    double barHeight,
+    double buttonPadding,
+  ) {
+    return Container(
+      height: barHeight,
+      margin: EdgeInsets.only(
+        top: marginSize,
+        right: marginSize,
+        left: marginSize,
+      ),
+      child: Row(
+        children: <Widget>[
+          if (chewieController.allowFullScreen)
+            _buildExpandButton(
+              backgroundColor,
+              iconColor,
+              barHeight,
+              buttonPadding,
+            ),
+          const Spacer(),
+          if (chewieController.allowMuting)
+            _buildMuteButton(
+              controller,
+              backgroundColor,
+              iconColor,
+              barHeight,
+              buttonPadding,
+            ),
+        ],
+      ),
+    );
+  }
+
+  void _cancelAndRestartTimer() {
+    _hideTimer?.cancel();
+
+    setState(() {
+      notifier.hideStuff = false;
+
+      _startHideTimer();
+    });
+  }
+
+  Future<void> _initialize() async {
+    _subtitleOn =
+        chewieController.showSubtitles &&
+        (chewieController.subtitle?.isNotEmpty ?? false);
+    controller.addListener(_updateState);
+
+    _updateState();
+
+    if (controller.value.isPlaying || chewieController.autoPlay) {
+      _startHideTimer();
+    }
+
+    if (chewieController.showControlsOnInitialize) {
+      _initTimer = Timer(const Duration(milliseconds: 200), () {
+        setState(() {
+          notifier.hideStuff = false;
+        });
+      });
+    }
+  }
+
+  void _onExpandCollapse() {
+    setState(() {
+      notifier.hideStuff = true;
+
+      chewieController.toggleFullScreen();
+      _expandCollapseTimer = Timer(const Duration(milliseconds: 300), () {
+        setState(() {
+          _cancelAndRestartTimer();
+        });
+      });
+    });
+  }
+
+  Widget _buildProgressBar() {
+    return Expanded(
+      child: Padding(
+        padding: const EdgeInsets.only(right: 12.0),
+        child: CupertinoVideoProgressBar(
+          controller,
+          onDragStart: () {
+            setState(() {
+              _dragging = true;
+            });
+
+            _hideTimer?.cancel();
+          },
+          onDragUpdate: () {
+            _hideTimer?.cancel();
+          },
+          onDragEnd: () {
+            setState(() {
+              _dragging = false;
+            });
+
+            _startHideTimer();
+          },
+          colors:
+              chewieController.cupertinoProgressColors ??
+              ChewieProgressColors(
+                playedColor: const Color.fromARGB(120, 255, 255, 255),
+                handleColor: const Color.fromARGB(255, 255, 255, 255),
+                bufferedColor: const Color.fromARGB(60, 255, 255, 255),
+                backgroundColor: const Color.fromARGB(20, 255, 255, 255),
+              ),
+          draggableProgressBar: chewieController.draggableProgressBar,
+        ),
+      ),
+    );
+  }
+
+  void _playPause() {
+    final isFinished =
+        _latestValue.position >= _latestValue.duration &&
+        _latestValue.duration.inSeconds > 0;
+
+    setState(() {
+      if (controller.value.isPlaying) {
+        notifier.hideStuff = false;
+        _hideTimer?.cancel();
+        controller.pause();
+      } else {
+        _cancelAndRestartTimer();
+
+        if (!controller.value.isInitialized) {
+          controller.initialize().then((_) {
+            controller.play();
+          });
+        } else {
+          if (isFinished) {
+            controller.seekTo(Duration.zero);
+          }
+          controller.play();
+        }
+      }
+    });
+  }
+
+  Future<void> _skipBack() async {
+    _cancelAndRestartTimer();
+    final beginning = Duration.zero.inMilliseconds;
+    final skip =
+        (_latestValue.position - const Duration(seconds: 15)).inMilliseconds;
+    await controller.seekTo(Duration(milliseconds: math.max(skip, beginning)));
+    // Restoring the video speed to selected speed
+    // A delay of 1 second is added to ensure a smooth transition of speed after reversing the video as reversing is an asynchronous function
+    Future.delayed(const Duration(milliseconds: 1000), () {
+      controller.setPlaybackSpeed(selectedSpeed);
+    });
+  }
+
+  Future<void> _skipForward() async {
+    _cancelAndRestartTimer();
+    final end = _latestValue.duration.inMilliseconds;
+    final skip =
+        (_latestValue.position + const Duration(seconds: 15)).inMilliseconds;
+    await controller.seekTo(Duration(milliseconds: math.min(skip, end)));
+    // Restoring the video speed to selected speed
+    // A delay of 1 second is added to ensure a smooth transition of speed after forwarding the video as forwaring is an asynchronous function
+    Future.delayed(const Duration(milliseconds: 1000), () {
+      controller.setPlaybackSpeed(selectedSpeed);
+    });
+  }
+
+  void _startHideTimer() {
+    final hideControlsTimer =
+        chewieController.hideControlsTimer.isNegative
+            ? ChewieController.defaultHideControlsTimer
+            : chewieController.hideControlsTimer;
+    _hideTimer = Timer(hideControlsTimer, () {
+      setState(() {
+        notifier.hideStuff = true;
+      });
+    });
+  }
+
+  void _bufferingTimerTimeout() {
+    _displayBufferingIndicator = true;
+    if (mounted) {
+      setState(() {});
+    }
+  }
+
+  void _updateState() {
+    if (!mounted) return;
+
+    final bool buffering = getIsBuffering(controller);
+
+    // display the progress bar indicator only after the buffering delay if it has been set
+    if (chewieController.progressIndicatorDelay != null) {
+      if (buffering) {
+        _bufferingDisplayTimer ??= Timer(
+          chewieController.progressIndicatorDelay!,
+          _bufferingTimerTimeout,
+        );
+      } else {
+        _bufferingDisplayTimer?.cancel();
+        _bufferingDisplayTimer = null;
+        _displayBufferingIndicator = false;
+      }
+    } else {
+      _displayBufferingIndicator = buffering;
+    }
+
+    setState(() {
+      _latestValue = controller.value;
+      _subtitlesPosition = controller.value.position;
+    });
+  }
+}
+
+class _PlaybackSpeedDialog extends StatelessWidget {
+  const _PlaybackSpeedDialog({
+    required List<double> speeds,
+    required double selected,
+  }) : _speeds = speeds,
+       _selected = selected;
+
+  final List<double> _speeds;
+  final double _selected;
+
+  @override
+  Widget build(BuildContext context) {
+    final selectedColor = CupertinoTheme.of(context).primaryColor;
+
+    return CupertinoActionSheet(
+      actions:
+          _speeds
+              .map(
+                (e) => CupertinoActionSheetAction(
+                  onPressed: () {
+                    Navigator.of(context).pop(e);
+                  },
+                  child: Row(
+                    mainAxisAlignment: MainAxisAlignment.center,
+                    children: [
+                      if (e == _selected)
+                        Icon(Icons.check, size: 20.0, color: selectedColor),
+                      Text(e.toString()),
+                    ],
+                  ),
+                ),
+              )
+              .toList(),
+    );
+  }
+}
diff --git a/lib/src/cupertino/cupertino_progress_bar.dart b/lib/src/cupertino/cupertino_progress_bar.dart
new file mode 100644
index 000000000..f24fc0f80
--- /dev/null
+++ b/lib/src/cupertino/cupertino_progress_bar.dart
@@ -0,0 +1,39 @@
+import 'package:chewie/src/chewie_progress_colors.dart';
+import 'package:chewie/src/progress_bar.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+import 'package:video_player/video_player.dart';
+
+class CupertinoVideoProgressBar extends StatelessWidget {
+  CupertinoVideoProgressBar(
+    this.controller, {
+    ChewieProgressColors? colors,
+    this.onDragEnd,
+    this.onDragStart,
+    this.onDragUpdate,
+    super.key,
+    this.draggableProgressBar = true,
+  }) : colors = colors ?? ChewieProgressColors();
+
+  final VideoPlayerController controller;
+  final ChewieProgressColors colors;
+  final Function()? onDragStart;
+  final Function()? onDragEnd;
+  final Function()? onDragUpdate;
+  final bool draggableProgressBar;
+
+  @override
+  Widget build(BuildContext context) {
+    return VideoProgressBar(
+      controller,
+      barHeight: 5,
+      handleHeight: 6,
+      drawShadow: true,
+      colors: colors,
+      onDragEnd: onDragEnd,
+      onDragStart: onDragStart,
+      onDragUpdate: onDragUpdate,
+      draggableProgressBar: draggableProgressBar,
+    );
+  }
+}
diff --git a/lib/src/cupertino/widgets/cupertino_options_dialog.dart b/lib/src/cupertino/widgets/cupertino_options_dialog.dart
new file mode 100644
index 000000000..72cc74f4c
--- /dev/null
+++ b/lib/src/cupertino/widgets/cupertino_options_dialog.dart
@@ -0,0 +1,41 @@
+import 'package:chewie/src/models/option_item.dart';
+import 'package:flutter/cupertino.dart';
+
+class CupertinoOptionsDialog extends StatefulWidget {
+  const CupertinoOptionsDialog({
+    super.key,
+    required this.options,
+    this.cancelButtonText,
+  });
+
+  final List<OptionItem> options;
+  final String? cancelButtonText;
+
+  @override
+  // ignore: library_private_types_in_public_api
+  _CupertinoOptionsDialogState createState() => _CupertinoOptionsDialogState();
+}
+
+class _CupertinoOptionsDialogState extends State<CupertinoOptionsDialog> {
+  @override
+  Widget build(BuildContext context) {
+    return SafeArea(
+      child: CupertinoActionSheet(
+        actions:
+            widget.options
+                .map(
+                  (option) => CupertinoActionSheetAction(
+                    onPressed: () => option.onTap(context),
+                    child: Text(option.title),
+                  ),
+                )
+                .toList(),
+        cancelButton: CupertinoActionSheetAction(
+          onPressed: () => Navigator.pop(context),
+          isDestructiveAction: true,
+          child: Text(widget.cancelButtonText ?? 'Cancel'),
+        ),
+      ),
+    );
+  }
+}
diff --git a/lib/src/cupertino_controls.dart b/lib/src/cupertino_controls.dart
deleted file mode 100644
index b916c52de..000000000
--- a/lib/src/cupertino_controls.dart
+++ /dev/null
@@ -1,547 +0,0 @@
-import 'dart:async';
-import 'dart:math' as math;
-import 'dart:ui' as ui;
-
-import 'package:chewie/src/chewie_player.dart';
-import 'package:chewie/src/chewie_progress_colors.dart';
-import 'package:chewie/src/cupertino_progress_bar.dart';
-import 'package:chewie/src/utils.dart';
-import 'package:flutter/cupertino.dart';
-import 'package:flutter/foundation.dart';
-import 'package:flutter/material.dart';
-import 'package:video_player/video_player.dart';
-
-class CupertinoControls extends StatefulWidget {
-  const CupertinoControls({
-    @required this.backgroundColor,
-    @required this.iconColor,
-  });
-
-  final Color backgroundColor;
-  final Color iconColor;
-
-  @override
-  State<StatefulWidget> createState() {
-    return _CupertinoControlsState();
-  }
-}
-
-class _CupertinoControlsState extends State<CupertinoControls> {
-  VideoPlayerValue _latestValue;
-  double _latestVolume;
-  bool _hideStuff = true;
-  Timer _hideTimer;
-  final marginSize = 5.0;
-  Timer _expandCollapseTimer;
-  Timer _initTimer;
-
-  VideoPlayerController controller;
-  ChewieController chewieController;
-
-  @override
-  Widget build(BuildContext context) {
-    chewieController = ChewieController.of(context);
-
-    if (_latestValue.hasError) {
-      return chewieController.errorBuilder != null
-          ? chewieController.errorBuilder(
-              context,
-              chewieController.videoPlayerController.value.errorDescription,
-            )
-          : Center(
-              child: Icon(
-                CupertinoIcons.exclamationmark_circle,
-                color: Colors.white,
-                size: 42,
-              ),
-            );
-    }
-
-    final backgroundColor = widget.backgroundColor;
-    final iconColor = widget.iconColor;
-    chewieController = ChewieController.of(context);
-    controller = chewieController.videoPlayerController;
-    final orientation = MediaQuery.of(context).orientation;
-    final barHeight = orientation == Orientation.portrait ? 30.0 : 47.0;
-    final buttonPadding = orientation == Orientation.portrait ? 16.0 : 24.0;
-
-    return MouseRegion(
-      onHover: (_) {
-        _cancelAndRestartTimer();
-      },
-      child: GestureDetector(
-        onTap: () {
-          _cancelAndRestartTimer();
-        },
-        child: AbsorbPointer(
-          absorbing: _hideStuff,
-          child: Column(
-            children: <Widget>[
-              _buildTopBar(
-                  backgroundColor, iconColor, barHeight, buttonPadding),
-              _buildHitArea(),
-              _buildBottomBar(backgroundColor, iconColor, barHeight),
-            ],
-          ),
-        ),
-      ),
-    );
-  }
-
-  @override
-  void dispose() {
-    _dispose();
-    super.dispose();
-  }
-
-  void _dispose() {
-    controller.removeListener(_updateState);
-    _hideTimer?.cancel();
-    _expandCollapseTimer?.cancel();
-    _initTimer?.cancel();
-  }
-
-  @override
-  void didChangeDependencies() {
-    final _oldController = chewieController;
-    chewieController = ChewieController.of(context);
-    controller = chewieController.videoPlayerController;
-
-    if (_oldController != chewieController) {
-      _dispose();
-      _initialize();
-    }
-
-    super.didChangeDependencies();
-  }
-
-  AnimatedOpacity _buildBottomBar(
-    Color backgroundColor,
-    Color iconColor,
-    double barHeight,
-  ) {
-    return AnimatedOpacity(
-      opacity: _hideStuff ? 0.0 : 1.0,
-      duration: Duration(milliseconds: 300),
-      child: Container(
-        color: Colors.transparent,
-        alignment: Alignment.bottomCenter,
-        margin: EdgeInsets.all(marginSize),
-        child: ClipRRect(
-          borderRadius: BorderRadius.circular(10.0),
-          child: BackdropFilter(
-            filter: ui.ImageFilter.blur(
-              sigmaX: 10.0,
-              sigmaY: 10.0,
-            ),
-            child: Container(
-              height: barHeight,
-              color: backgroundColor,
-              child: chewieController.isLive
-                  ? Row(
-                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
-                      children: <Widget>[
-                        _buildPlayPause(controller, iconColor, barHeight),
-                        _buildLive(iconColor),
-                      ],
-                    )
-                  : Row(
-                      children: <Widget>[
-                        _buildSkipBack(iconColor, barHeight),
-                        _buildPlayPause(controller, iconColor, barHeight),
-                        _buildSkipForward(iconColor, barHeight),
-                        _buildPosition(iconColor),
-                        _buildProgressBar(),
-                        _buildRemaining(iconColor)
-                      ],
-                    ),
-            ),
-          ),
-        ),
-      ),
-    );
-  }
-
-  Widget _buildLive(Color iconColor) {
-    return Padding(
-      padding: EdgeInsets.only(right: 12.0),
-      child: Text(
-        'LIVE',
-        style: TextStyle(color: iconColor, fontSize: 12.0),
-      ),
-    );
-  }
-
-  GestureDetector _buildExpandButton(
-    Color backgroundColor,
-    Color iconColor,
-    double barHeight,
-    double buttonPadding,
-  ) {
-    return GestureDetector(
-      onTap: _onExpandCollapse,
-      child: AnimatedOpacity(
-        opacity: _hideStuff ? 0.0 : 1.0,
-        duration: Duration(milliseconds: 300),
-        child: ClipRRect(
-          borderRadius: BorderRadius.circular(10.0),
-          child: BackdropFilter(
-            filter: ui.ImageFilter.blur(sigmaX: 10.0),
-            child: Container(
-              height: barHeight,
-              padding: EdgeInsets.only(
-                left: buttonPadding,
-                right: buttonPadding,
-              ),
-              color: backgroundColor,
-              child: Center(
-                child: Icon(
-                  chewieController.isFullScreen
-                      ? CupertinoIcons.arrow_down_right_arrow_up_left
-                      : CupertinoIcons.arrow_up_left_arrow_down_right,
-                  color: iconColor,
-                  size: 16,
-                ),
-              ),
-            ),
-          ),
-        ),
-      ),
-    );
-  }
-
-  Expanded _buildHitArea() {
-    return Expanded(
-      child: GestureDetector(
-        onTap: _latestValue != null && _latestValue.isPlaying
-            ? _cancelAndRestartTimer
-            : () {
-                _hideTimer?.cancel();
-
-                setState(() {
-                  _hideStuff = false;
-                });
-              },
-        child: Container(
-          color: Colors.transparent,
-        ),
-      ),
-    );
-  }
-
-  GestureDetector _buildMuteButton(
-    VideoPlayerController controller,
-    Color backgroundColor,
-    Color iconColor,
-    double barHeight,
-    double buttonPadding,
-  ) {
-    return GestureDetector(
-      onTap: () {
-        _cancelAndRestartTimer();
-
-        if (_latestValue.volume == 0) {
-          controller.setVolume(_latestVolume ?? 0.5);
-        } else {
-          _latestVolume = controller.value.volume;
-          controller.setVolume(0.0);
-        }
-      },
-      child: AnimatedOpacity(
-        opacity: _hideStuff ? 0.0 : 1.0,
-        duration: Duration(milliseconds: 300),
-        child: ClipRRect(
-          borderRadius: BorderRadius.circular(10.0),
-          child: BackdropFilter(
-            filter: ui.ImageFilter.blur(sigmaX: 10.0),
-            child: Container(
-              color: backgroundColor,
-              child: Container(
-                height: barHeight,
-                padding: EdgeInsets.only(
-                  left: buttonPadding,
-                  right: buttonPadding,
-                ),
-                child: Icon(
-                  (_latestValue != null && _latestValue.volume > 0)
-                      ? Icons.volume_up
-                      : Icons.volume_off,
-                  color: iconColor,
-                  size: 16,
-                ),
-              ),
-            ),
-          ),
-        ),
-      ),
-    );
-  }
-
-  GestureDetector _buildPlayPause(
-    VideoPlayerController controller,
-    Color iconColor,
-    double barHeight,
-  ) {
-    return GestureDetector(
-      onTap: _playPause,
-      child: Container(
-        height: barHeight,
-        color: Colors.transparent,
-        padding: EdgeInsets.only(
-          left: 6.0,
-          right: 6.0,
-        ),
-        child: Icon(
-          controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
-          color: iconColor,
-        ),
-      ),
-    );
-  }
-
-  Widget _buildPosition(Color iconColor) {
-    final position =
-        _latestValue != null ? _latestValue.position : Duration(seconds: 0);
-
-    return Padding(
-      padding: EdgeInsets.only(right: 12.0),
-      child: Text(
-        formatDuration(position),
-        style: TextStyle(
-          color: iconColor,
-          fontSize: 12.0,
-        ),
-      ),
-    );
-  }
-
-  Widget _buildRemaining(Color iconColor) {
-    final position = _latestValue != null && _latestValue.duration != null
-        ? _latestValue.duration - _latestValue.position
-        : Duration(seconds: 0);
-
-    return Padding(
-      padding: EdgeInsets.only(right: 12.0),
-      child: Text(
-        '-${formatDuration(position)}',
-        style: TextStyle(color: iconColor, fontSize: 12.0),
-      ),
-    );
-  }
-
-  GestureDetector _buildSkipBack(Color iconColor, double barHeight) {
-    return GestureDetector(
-      onTap: _skipBack,
-      child: Container(
-        height: barHeight,
-        color: Colors.transparent,
-        margin: EdgeInsets.only(left: 10.0),
-        padding: EdgeInsets.only(
-          left: 6.0,
-          right: 6.0,
-        ),
-        child: Icon(
-          CupertinoIcons.gobackward_15,
-          color: iconColor,
-          size: 18.0,
-        ),
-      ),
-    );
-  }
-
-  GestureDetector _buildSkipForward(Color iconColor, double barHeight) {
-    return GestureDetector(
-      onTap: _skipForward,
-      child: Container(
-        height: barHeight,
-        color: Colors.transparent,
-        padding: EdgeInsets.only(
-          left: 6.0,
-          right: 8.0,
-        ),
-        margin: EdgeInsets.only(
-          right: 8.0,
-        ),
-        child: Icon(
-          CupertinoIcons.goforward_15,
-          color: iconColor,
-          size: 18.0,
-        ),
-      ),
-    );
-  }
-
-  Widget _buildTopBar(
-    Color backgroundColor,
-    Color iconColor,
-    double barHeight,
-    double buttonPadding,
-  ) {
-    return Container(
-      height: barHeight,
-      margin: EdgeInsets.only(
-        top: marginSize,
-        right: marginSize,
-        left: marginSize,
-      ),
-      child: Row(
-        children: <Widget>[
-          chewieController.allowFullScreen
-              ? _buildExpandButton(
-                  backgroundColor, iconColor, barHeight, buttonPadding)
-              : Container(),
-          Expanded(child: Container()),
-          chewieController.allowMuting
-              ? _buildMuteButton(controller, backgroundColor, iconColor,
-                  barHeight, buttonPadding)
-              : Container(),
-        ],
-      ),
-    );
-  }
-
-  void _cancelAndRestartTimer() {
-    _hideTimer?.cancel();
-
-    setState(() {
-      _hideStuff = false;
-
-      _startHideTimer();
-    });
-  }
-
-  Future<Null> _initialize() async {
-    controller.addListener(_updateState);
-
-    _updateState();
-
-    if ((controller.value != null && controller.value.isPlaying) ||
-        chewieController.autoPlay) {
-      _startHideTimer();
-    }
-
-    if (chewieController.showControlsOnInitialize) {
-      _initTimer = Timer(Duration(milliseconds: 200), () {
-        setState(() {
-          _hideStuff = false;
-        });
-      });
-    }
-  }
-
-  void _onExpandCollapse() {
-    setState(() {
-      _hideStuff = true;
-
-      chewieController.toggleFullScreen();
-      _expandCollapseTimer = Timer(Duration(milliseconds: 300), () {
-        setState(() {
-          _cancelAndRestartTimer();
-        });
-      });
-    });
-  }
-
-  Widget _buildProgressBar() {
-    return Expanded(
-      child: Padding(
-        padding: EdgeInsets.only(right: 12.0),
-        child: CupertinoVideoProgressBar(
-          controller,
-          onDragStart: () {
-            _hideTimer?.cancel();
-          },
-          onDragEnd: () {
-            _startHideTimer();
-          },
-          colors: chewieController.cupertinoProgressColors ??
-              ChewieProgressColors(
-                playedColor: Color.fromARGB(
-                  120,
-                  255,
-                  255,
-                  255,
-                ),
-                handleColor: Color.fromARGB(
-                  255,
-                  255,
-                  255,
-                  255,
-                ),
-                bufferedColor: Color.fromARGB(
-                  60,
-                  255,
-                  255,
-                  255,
-                ),
-                backgroundColor: Color.fromARGB(
-                  20,
-                  255,
-                  255,
-                  255,
-                ),
-              ),
-        ),
-      ),
-    );
-  }
-
-  void _playPause() {
-    bool isFinished;
-    if (_latestValue.duration != null) {
-      isFinished = _latestValue.position >= _latestValue.duration;
-    } else {
-      isFinished = false;
-    }
-
-    setState(() {
-      if (controller.value.isPlaying) {
-        _hideStuff = false;
-        _hideTimer?.cancel();
-        controller.pause();
-      } else {
-        _cancelAndRestartTimer();
-
-        if (!controller.value.initialized) {
-          controller.initialize().then((_) {
-            controller.play();
-          });
-        } else {
-          if (isFinished) {
-            controller.seekTo(Duration(seconds: 0));
-          }
-          controller.play();
-        }
-      }
-    });
-  }
-
-  void _skipBack() {
-    _cancelAndRestartTimer();
-    final beginning = Duration(seconds: 0).inMilliseconds;
-    final skip = (_latestValue.position - Duration(seconds: 15)).inMilliseconds;
-    controller.seekTo(Duration(milliseconds: math.max(skip, beginning)));
-  }
-
-  void _skipForward() {
-    _cancelAndRestartTimer();
-    final end = _latestValue.duration.inMilliseconds;
-    final skip = (_latestValue.position + Duration(seconds: 15)).inMilliseconds;
-    controller.seekTo(Duration(milliseconds: math.min(skip, end)));
-  }
-
-  void _startHideTimer() {
-    _hideTimer = Timer(const Duration(seconds: 3), () {
-      setState(() {
-        _hideStuff = true;
-      });
-    });
-  }
-
-  void _updateState() {
-    if (!this.mounted) return;
-    setState(() {
-      _latestValue = controller.value;
-    });
-  }
-}
diff --git a/lib/src/cupertino_progress_bar.dart b/lib/src/cupertino_progress_bar.dart
deleted file mode 100644
index 60256df54..000000000
--- a/lib/src/cupertino_progress_bar.dart
+++ /dev/null
@@ -1,169 +0,0 @@
-import 'package:chewie/src/chewie_progress_colors.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter/widgets.dart';
-import 'package:video_player/video_player.dart';
-
-class CupertinoVideoProgressBar extends StatefulWidget {
-  CupertinoVideoProgressBar(
-    this.controller, {
-    ChewieProgressColors colors,
-    this.onDragEnd,
-    this.onDragStart,
-    this.onDragUpdate,
-  }) : colors = colors ?? ChewieProgressColors();
-
-  final VideoPlayerController controller;
-  final ChewieProgressColors colors;
-  final Function() onDragStart;
-  final Function() onDragEnd;
-  final Function() onDragUpdate;
-
-  @override
-  _VideoProgressBarState createState() {
-    return _VideoProgressBarState();
-  }
-}
-
-class _VideoProgressBarState extends State<CupertinoVideoProgressBar> {
-  bool _controllerWasPlaying = false;
-
-  VideoPlayerController get controller => widget.controller;
-
-  @override
-  Widget build(BuildContext context) {
-    void seekToRelativePosition(Offset globalPosition) {
-      final box = context.findRenderObject() as RenderBox;
-      final Offset tapPos = box.globalToLocal(globalPosition);
-      final double relative = tapPos.dx / box.size.width;
-      final Duration position = controller.value.duration * relative;
-      controller.seekTo(position);
-    }
-
-    return GestureDetector(
-      child: Center(
-        child: Container(
-          height: MediaQuery.of(context).size.height,
-          width: MediaQuery.of(context).size.width,
-          color: Colors.transparent,
-          child: CustomPaint(
-            painter: _ProgressBarPainter(
-              controller.value,
-              widget.colors,
-            ),
-          ),
-        ),
-      ),
-      onHorizontalDragStart: (DragStartDetails details) {
-        if (!controller.value.initialized) {
-          return;
-        }
-        _controllerWasPlaying = controller.value.isPlaying;
-        if (_controllerWasPlaying) {
-          controller.pause();
-        }
-
-        if (widget.onDragStart != null) {
-          widget.onDragStart();
-        }
-      },
-      onHorizontalDragUpdate: (DragUpdateDetails details) {
-        if (!controller.value.initialized) {
-          return;
-        }
-        seekToRelativePosition(details.globalPosition);
-
-        if (widget.onDragUpdate != null) {
-          widget.onDragUpdate();
-        }
-      },
-      onHorizontalDragEnd: (DragEndDetails details) {
-        if (_controllerWasPlaying) {
-          controller.play();
-        }
-
-        if (widget.onDragEnd != null) {
-          widget.onDragEnd();
-        }
-      },
-      onTapDown: (TapDownDetails details) {
-        if (!controller.value.initialized) {
-          return;
-        }
-        seekToRelativePosition(details.globalPosition);
-      },
-    );
-  }
-}
-
-class _ProgressBarPainter extends CustomPainter {
-  _ProgressBarPainter(this.value, this.colors);
-
-  VideoPlayerValue value;
-  ChewieProgressColors colors;
-
-  @override
-  bool shouldRepaint(CustomPainter painter) {
-    return true;
-  }
-
-  @override
-  void paint(Canvas canvas, Size size) {
-    final barHeight = 5.0;
-    final handleHeight = 6.0;
-    final baseOffset = size.height / 2 - barHeight / 2.0;
-
-    canvas.drawRRect(
-      RRect.fromRectAndRadius(
-        Rect.fromPoints(
-          Offset(0.0, baseOffset),
-          Offset(size.width, baseOffset + barHeight),
-        ),
-        Radius.circular(4.0),
-      ),
-      colors.backgroundPaint,
-    );
-    if (!value.initialized) {
-      return;
-    }
-    final double playedPartPercent =
-        value.position.inMilliseconds / value.duration.inMilliseconds;
-    final double playedPart =
-        playedPartPercent > 1 ? size.width : playedPartPercent * size.width;
-    for (DurationRange range in value.buffered) {
-      final double start = range.startFraction(value.duration) * size.width;
-      final double end = range.endFraction(value.duration) * size.width;
-      canvas.drawRRect(
-        RRect.fromRectAndRadius(
-          Rect.fromPoints(
-            Offset(start, baseOffset),
-            Offset(end, baseOffset + barHeight),
-          ),
-          Radius.circular(4.0),
-        ),
-        colors.bufferedPaint,
-      );
-    }
-    canvas.drawRRect(
-      RRect.fromRectAndRadius(
-        Rect.fromPoints(
-          Offset(0.0, baseOffset),
-          Offset(playedPart, baseOffset + barHeight),
-        ),
-        Radius.circular(4.0),
-      ),
-      colors.playedPaint,
-    );
-
-    final shadowPath = Path()
-      ..addOval(Rect.fromCircle(
-          center: Offset(playedPart, baseOffset + barHeight / 2),
-          radius: handleHeight));
-
-    canvas.drawShadow(shadowPath, Colors.black, 0.2, false);
-    canvas.drawCircle(
-      Offset(playedPart, baseOffset + barHeight / 2),
-      handleHeight,
-      colors.handlePaint,
-    );
-  }
-}
diff --git a/lib/src/helpers/adaptive_controls.dart b/lib/src/helpers/adaptive_controls.dart
new file mode 100644
index 000000000..518ec376e
--- /dev/null
+++ b/lib/src/helpers/adaptive_controls.dart
@@ -0,0 +1,26 @@
+import 'package:chewie/chewie.dart';
+import 'package:flutter/material.dart';
+
+class AdaptiveControls extends StatelessWidget {
+  const AdaptiveControls({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    switch (Theme.of(context).platform) {
+      case TargetPlatform.android:
+      case TargetPlatform.fuchsia:
+        return const MaterialControls();
+
+      case TargetPlatform.macOS:
+      case TargetPlatform.windows:
+      case TargetPlatform.linux:
+        return const MaterialDesktopControls();
+
+      case TargetPlatform.iOS:
+        return const CupertinoControls(
+          backgroundColor: Color.fromRGBO(41, 41, 41, 0.7),
+          iconColor: Color.fromARGB(255, 200, 200, 200),
+        );
+    }
+  }
+}
diff --git a/lib/src/helpers/utils.dart b/lib/src/helpers/utils.dart
new file mode 100644
index 000000000..855e48004
--- /dev/null
+++ b/lib/src/helpers/utils.dart
@@ -0,0 +1,74 @@
+import 'package:flutter/foundation.dart';
+import 'package:video_player/video_player.dart';
+
+String formatDuration(Duration position) {
+  final ms = position.inMilliseconds;
+
+  int seconds = ms ~/ 1000;
+  final int hours = seconds ~/ 3600;
+  seconds = seconds % 3600;
+  final minutes = seconds ~/ 60;
+  seconds = seconds % 60;
+
+  final hoursString =
+      hours >= 10
+          ? '$hours'
+          : hours == 0
+          ? '00'
+          : '0$hours';
+
+  final minutesString =
+      minutes >= 10
+          ? '$minutes'
+          : minutes == 0
+          ? '00'
+          : '0$minutes';
+
+  final secondsString =
+      seconds >= 10
+          ? '$seconds'
+          : seconds == 0
+          ? '00'
+          : '0$seconds';
+
+  final formattedTime =
+      '${hoursString == '00' ? '' : '$hoursString:'}$minutesString:$secondsString';
+
+  return formattedTime;
+}
+
+/// Gets the current buffering state of the video player.
+///
+/// For Android, it will use a workaround due to a [bug](https://github.com/flutter/flutter/issues/165149)
+/// affecting the `video_player` plugin, preventing it from getting the
+/// actual buffering state. This currently results in the `VideoPlayerController` always buffering,
+/// thus breaking UI elements.
+///
+/// For this, the actual buffer position is used to determine if the video is
+/// buffering or not. See Issue [#912](https://github.com/fluttercommunity/chewie/pull/912) for more details.
+bool getIsBuffering(VideoPlayerController controller) {
+  final VideoPlayerValue value = controller.value;
+
+  if (defaultTargetPlatform == TargetPlatform.android) {
+    if (value.isBuffering) {
+      // -> Check if we actually buffer, as android has a bug preventing to
+      //    get the correct buffering state from this single bool.
+      final int position = value.position.inMilliseconds;
+
+      // Special case, if the video is finished, we don't want to show the
+      // buffering indicator anymore
+      if (position >= value.duration.inMilliseconds) {
+        return false;
+      } else {
+        final int buffer = value.buffered.lastOrNull?.end.inMilliseconds ?? -1;
+
+        return position >= buffer;
+      }
+    } else {
+      // -> No buffering
+      return false;
+    }
+  }
+
+  return value.isBuffering;
+}
diff --git a/lib/src/material/material_controls.dart b/lib/src/material/material_controls.dart
new file mode 100644
index 000000000..c8c7cbfe1
--- /dev/null
+++ b/lib/src/material/material_controls.dart
@@ -0,0 +1,678 @@
+import 'dart:async';
+
+import 'package:chewie/src/center_play_button.dart';
+import 'package:chewie/src/center_seek_button.dart';
+import 'package:chewie/src/chewie_player.dart';
+import 'package:chewie/src/chewie_progress_colors.dart';
+import 'package:chewie/src/helpers/utils.dart';
+import 'package:chewie/src/material/material_progress_bar.dart';
+import 'package:chewie/src/material/widgets/options_dialog.dart';
+import 'package:chewie/src/material/widgets/playback_speed_dialog.dart';
+import 'package:chewie/src/models/option_item.dart';
+import 'package:chewie/src/models/subtitle_model.dart';
+import 'package:chewie/src/notifiers/index.dart';
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import 'package:video_player/video_player.dart';
+
+class MaterialControls extends StatefulWidget {
+  const MaterialControls({this.showPlayButton = true, super.key});
+
+  final bool showPlayButton;
+
+  @override
+  State<StatefulWidget> createState() {
+    return _MaterialControlsState();
+  }
+}
+
+class _MaterialControlsState extends State<MaterialControls>
+    with SingleTickerProviderStateMixin {
+  late PlayerNotifier notifier;
+  late VideoPlayerValue _latestValue;
+  double? _latestVolume;
+  Timer? _hideTimer;
+  Timer? _initTimer;
+  late var _subtitlesPosition = Duration.zero;
+  bool _subtitleOn = false;
+  Timer? _showAfterExpandCollapseTimer;
+  bool _dragging = false;
+  bool _displayTapped = false;
+  Timer? _bufferingDisplayTimer;
+  bool _displayBufferingIndicator = false;
+
+  final barHeight = 48.0 * 1.5;
+  final marginSize = 5.0;
+
+  late VideoPlayerController controller;
+  ChewieController? _chewieController;
+
+  // We know that _chewieController is set in didChangeDependencies
+  ChewieController get chewieController => _chewieController!;
+
+  @override
+  void initState() {
+    super.initState();
+    notifier = Provider.of<PlayerNotifier>(context, listen: false);
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    if (_latestValue.hasError) {
+      return chewieController.errorBuilder?.call(
+            context,
+            chewieController.videoPlayerController.value.errorDescription!,
+          ) ??
+          const Center(child: Icon(Icons.error, color: Colors.white, size: 42));
+    }
+
+    return MouseRegion(
+      onHover: (_) {
+        _cancelAndRestartTimer();
+      },
+      child: GestureDetector(
+        onTap: () => _cancelAndRestartTimer(),
+        child: AbsorbPointer(
+          absorbing: notifier.hideStuff,
+          child: Stack(
+            children: [
+              if (_displayBufferingIndicator)
+                _chewieController?.bufferingBuilder?.call(context) ??
+                    const Center(child: CircularProgressIndicator())
+              else
+                _buildHitArea(),
+              _buildActionBar(),
+              Column(
+                mainAxisAlignment: MainAxisAlignment.end,
+                children: <Widget>[
+                  if (_subtitleOn)
+                    Transform.translate(
+                      offset: Offset(
+                        0.0,
+                        notifier.hideStuff ? barHeight * 0.8 : 0.0,
+                      ),
+                      child: _buildSubtitles(
+                        context,
+                        chewieController.subtitle!,
+                      ),
+                    ),
+                  _buildBottomBar(context),
+                ],
+              ),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+
+  @override
+  void dispose() {
+    _dispose();
+    super.dispose();
+  }
+
+  void _dispose() {
+    controller.removeListener(_updateState);
+    _hideTimer?.cancel();
+    _initTimer?.cancel();
+    _showAfterExpandCollapseTimer?.cancel();
+  }
+
+  @override
+  void didChangeDependencies() {
+    final oldController = _chewieController;
+    _chewieController = ChewieController.of(context);
+    controller = chewieController.videoPlayerController;
+
+    if (oldController != chewieController) {
+      _dispose();
+      _initialize();
+    }
+
+    super.didChangeDependencies();
+  }
+
+  Widget _buildActionBar() {
+    return Positioned(
+      top: 0,
+      right: 0,
+      child: SafeArea(
+        child: AnimatedOpacity(
+          opacity: notifier.hideStuff ? 0.0 : 1.0,
+          duration: const Duration(milliseconds: 250),
+          child: Row(
+            children: [
+              _buildSubtitleToggle(),
+              if (chewieController.showOptions) _buildOptionsButton(),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+
+  List<OptionItem> _buildOptions(BuildContext context) {
+    final options = <OptionItem>[
+      OptionItem(
+        onTap: (context) async {
+          Navigator.pop(context);
+          _onSpeedButtonTap();
+        },
+        iconData: Icons.speed,
+        title:
+            chewieController.optionsTranslation?.playbackSpeedButtonText ??
+            'Playback speed',
+      ),
+    ];
+
+    if (chewieController.additionalOptions != null &&
+        chewieController.additionalOptions!(context).isNotEmpty) {
+      options.addAll(chewieController.additionalOptions!(context));
+    }
+    return options;
+  }
+
+  Widget _buildOptionsButton() {
+    return AnimatedOpacity(
+      opacity: notifier.hideStuff ? 0.0 : 1.0,
+      duration: const Duration(milliseconds: 250),
+      child: IconButton(
+        onPressed: () async {
+          _hideTimer?.cancel();
+
+          if (chewieController.optionsBuilder != null) {
+            await chewieController.optionsBuilder!(
+              context,
+              _buildOptions(context),
+            );
+          } else {
+            await showModalBottomSheet<OptionItem>(
+              context: context,
+              isScrollControlled: true,
+              useRootNavigator: chewieController.useRootNavigator,
+              builder:
+                  (context) => OptionsDialog(
+                    options: _buildOptions(context),
+                    cancelButtonText:
+                        chewieController.optionsTranslation?.cancelButtonText,
+                  ),
+            );
+          }
+
+          if (_latestValue.isPlaying) {
+            _startHideTimer();
+          }
+        },
+        icon: const Icon(Icons.more_vert, color: Colors.white),
+      ),
+    );
+  }
+
+  Widget _buildSubtitles(BuildContext context, Subtitles subtitles) {
+    if (!_subtitleOn) {
+      return const SizedBox();
+    }
+    final currentSubtitle = subtitles.getByPosition(_subtitlesPosition);
+    if (currentSubtitle.isEmpty) {
+      return const SizedBox();
+    }
+
+    if (chewieController.subtitleBuilder != null) {
+      return chewieController.subtitleBuilder!(
+        context,
+        currentSubtitle.first!.text,
+      );
+    }
+
+    return Padding(
+      padding: EdgeInsets.all(marginSize),
+      child: Container(
+        padding: const EdgeInsets.all(5),
+        decoration: BoxDecoration(
+          color: const Color(0x96000000),
+          borderRadius: BorderRadius.circular(10.0),
+        ),
+        child: Text(
+          currentSubtitle.first!.text.toString(),
+          style: const TextStyle(fontSize: 18),
+          textAlign: TextAlign.center,
+        ),
+      ),
+    );
+  }
+
+  AnimatedOpacity _buildBottomBar(BuildContext context) {
+    final iconColor = Theme.of(context).textTheme.labelLarge!.color;
+
+    return AnimatedOpacity(
+      opacity: notifier.hideStuff ? 0.0 : 1.0,
+      duration: const Duration(milliseconds: 300),
+      child: Container(
+        height: barHeight + (chewieController.isFullScreen ? 10.0 : 0),
+        padding: EdgeInsets.only(
+          left: 20,
+          right: 20,
+          bottom: !chewieController.isFullScreen ? 10.0 : 0,
+        ),
+        child: SafeArea(
+          top: false,
+          bottom: chewieController.isFullScreen,
+          minimum: chewieController.controlsSafeAreaMinimum,
+          child: Column(
+            mainAxisSize: MainAxisSize.min,
+            mainAxisAlignment: MainAxisAlignment.center,
+            children: [
+              Flexible(
+                child: Row(
+                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                  children: <Widget>[
+                    if (chewieController.isLive)
+                      const Expanded(child: Text('LIVE'))
+                    else
+                      _buildPosition(iconColor),
+                    if (chewieController.allowMuting)
+                      _buildMuteButton(controller),
+                    const Spacer(),
+                    if (chewieController.allowFullScreen) _buildExpandButton(),
+                  ],
+                ),
+              ),
+              SizedBox(height: chewieController.isFullScreen ? 15.0 : 0),
+              if (!chewieController.isLive)
+                Expanded(
+                  child: Container(
+                    padding: const EdgeInsets.symmetric(horizontal: 20),
+                    child: Row(children: [_buildProgressBar()]),
+                  ),
+                ),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+
+  GestureDetector _buildMuteButton(VideoPlayerController controller) {
+    return GestureDetector(
+      onTap: () {
+        _cancelAndRestartTimer();
+
+        if (_latestValue.volume == 0) {
+          controller.setVolume(_latestVolume ?? 0.5);
+        } else {
+          _latestVolume = controller.value.volume;
+          controller.setVolume(0.0);
+        }
+      },
+      child: AnimatedOpacity(
+        opacity: notifier.hideStuff ? 0.0 : 1.0,
+        duration: const Duration(milliseconds: 300),
+        child: ClipRect(
+          child: Container(
+            height: barHeight,
+            padding: const EdgeInsets.only(left: 6.0),
+            child: Icon(
+              _latestValue.volume > 0 ? Icons.volume_up : Icons.volume_off,
+              color: Colors.white,
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+
+  GestureDetector _buildExpandButton() {
+    return GestureDetector(
+      onTap: _onExpandCollapse,
+      child: AnimatedOpacity(
+        opacity: notifier.hideStuff ? 0.0 : 1.0,
+        duration: const Duration(milliseconds: 300),
+        child: Container(
+          height: barHeight + (chewieController.isFullScreen ? 15.0 : 0),
+          margin: const EdgeInsets.only(right: 12.0),
+          padding: const EdgeInsets.only(left: 8.0, right: 8.0),
+          child: Center(
+            child: Icon(
+              chewieController.isFullScreen
+                  ? Icons.fullscreen_exit
+                  : Icons.fullscreen,
+              color: Colors.white,
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+
+  Widget _buildHitArea() {
+    final bool isFinished =
+        (_latestValue.position >= _latestValue.duration) &&
+        _latestValue.duration.inSeconds > 0;
+    final bool showPlayButton =
+        widget.showPlayButton && !_dragging && !notifier.hideStuff;
+
+    return GestureDetector(
+      onTap: () {
+        if (_latestValue.isPlaying) {
+          if (_chewieController?.pauseOnBackgroundTap ?? false) {
+            _playPause();
+            _cancelAndRestartTimer();
+          } else {
+            if (_displayTapped) {
+              setState(() {
+                notifier.hideStuff = true;
+              });
+            } else {
+              _cancelAndRestartTimer();
+            }
+          }
+        } else {
+          _playPause();
+
+          setState(() {
+            notifier.hideStuff = true;
+          });
+        }
+      },
+      child: Container(
+        alignment: Alignment.center,
+        color:
+            Colors
+                .transparent, // The Gesture Detector doesn't expand to the full size of the container without this; Not sure why!
+        child: Row(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: [
+            if (!isFinished && !chewieController.isLive)
+              CenterSeekButton(
+                iconData: Icons.replay_10,
+                backgroundColor: Colors.black54,
+                iconColor: Colors.white,
+                show: showPlayButton,
+                fadeDuration: chewieController.materialSeekButtonFadeDuration,
+                iconSize: chewieController.materialSeekButtonSize,
+                onPressed: _seekBackward,
+              ),
+            Container(
+              margin: EdgeInsets.symmetric(horizontal: marginSize),
+              child: CenterPlayButton(
+                backgroundColor: Colors.black54,
+                iconColor: Colors.white,
+                isFinished: isFinished,
+                isPlaying: controller.value.isPlaying,
+                show: showPlayButton,
+                onPressed: _playPause,
+              ),
+            ),
+            if (!isFinished && !chewieController.isLive)
+              CenterSeekButton(
+                iconData: Icons.forward_10,
+                backgroundColor: Colors.black54,
+                iconColor: Colors.white,
+                show: showPlayButton,
+                fadeDuration: chewieController.materialSeekButtonFadeDuration,
+                iconSize: chewieController.materialSeekButtonSize,
+                onPressed: _seekForward,
+              ),
+          ],
+        ),
+      ),
+    );
+  }
+
+  Future<void> _onSpeedButtonTap() async {
+    _hideTimer?.cancel();
+
+    final chosenSpeed = await showModalBottomSheet<double>(
+      context: context,
+      isScrollControlled: true,
+      useRootNavigator: chewieController.useRootNavigator,
+      builder:
+          (context) => PlaybackSpeedDialog(
+            speeds: chewieController.playbackSpeeds,
+            selected: _latestValue.playbackSpeed,
+          ),
+    );
+
+    if (chosenSpeed != null) {
+      controller.setPlaybackSpeed(chosenSpeed);
+    }
+
+    if (_latestValue.isPlaying) {
+      _startHideTimer();
+    }
+  }
+
+  Widget _buildPosition(Color? iconColor) {
+    final position = _latestValue.position;
+    final duration = _latestValue.duration;
+
+    return RichText(
+      text: TextSpan(
+        text: '${formatDuration(position)} ',
+        children: <InlineSpan>[
+          TextSpan(
+            text: '/ ${formatDuration(duration)}',
+            style: TextStyle(
+              fontSize: 14.0,
+              color: Colors.white.withValues(alpha: .75),
+              fontWeight: FontWeight.normal,
+            ),
+          ),
+        ],
+        style: const TextStyle(
+          fontSize: 14.0,
+          color: Colors.white,
+          fontWeight: FontWeight.bold,
+        ),
+      ),
+    );
+  }
+
+  Widget _buildSubtitleToggle() {
+    //if don't have subtitle hiden button
+    if (chewieController.subtitle?.isEmpty ?? true) {
+      return const SizedBox();
+    }
+    return GestureDetector(
+      onTap: _onSubtitleTap,
+      child: Container(
+        height: barHeight,
+        color: Colors.transparent,
+        padding: const EdgeInsets.only(left: 12.0, right: 12.0),
+        child: Icon(
+          _subtitleOn
+              ? Icons.closed_caption
+              : Icons.closed_caption_off_outlined,
+          color: _subtitleOn ? Colors.white : Colors.grey[700],
+        ),
+      ),
+    );
+  }
+
+  void _onSubtitleTap() {
+    setState(() {
+      _subtitleOn = !_subtitleOn;
+    });
+  }
+
+  void _cancelAndRestartTimer() {
+    _hideTimer?.cancel();
+    _startHideTimer();
+
+    setState(() {
+      notifier.hideStuff = false;
+      _displayTapped = true;
+    });
+  }
+
+  Future<void> _initialize() async {
+    _subtitleOn =
+        chewieController.showSubtitles &&
+        (chewieController.subtitle?.isNotEmpty ?? false);
+    controller.addListener(_updateState);
+
+    _updateState();
+
+    if (controller.value.isPlaying || chewieController.autoPlay) {
+      _startHideTimer();
+    }
+
+    if (chewieController.showControlsOnInitialize) {
+      _initTimer = Timer(const Duration(milliseconds: 200), () {
+        setState(() {
+          notifier.hideStuff = false;
+        });
+      });
+    }
+  }
+
+  void _onExpandCollapse() {
+    setState(() {
+      notifier.hideStuff = true;
+
+      chewieController.toggleFullScreen();
+      _showAfterExpandCollapseTimer = Timer(
+        const Duration(milliseconds: 300),
+        () {
+          setState(() {
+            _cancelAndRestartTimer();
+          });
+        },
+      );
+    });
+  }
+
+  void _playPause() {
+    final bool isFinished =
+        (_latestValue.position >= _latestValue.duration) &&
+        _latestValue.duration.inSeconds > 0;
+
+    setState(() {
+      if (controller.value.isPlaying) {
+        notifier.hideStuff = false;
+        _hideTimer?.cancel();
+        controller.pause();
+      } else {
+        _cancelAndRestartTimer();
+
+        if (!controller.value.isInitialized) {
+          controller.initialize().then((_) {
+            controller.play();
+          });
+        } else {
+          if (isFinished) {
+            controller.seekTo(Duration.zero);
+          }
+          controller.play();
+        }
+      }
+    });
+  }
+
+  void _seekRelative(Duration relativeSeek) {
+    _cancelAndRestartTimer();
+    final position = _latestValue.position + relativeSeek;
+    final duration = _latestValue.duration;
+
+    if (position < Duration.zero) {
+      controller.seekTo(Duration.zero);
+    } else if (position > duration) {
+      controller.seekTo(duration);
+    } else {
+      controller.seekTo(position);
+    }
+  }
+
+  void _seekBackward() {
+    _seekRelative(const Duration(seconds: -10));
+  }
+
+  void _seekForward() {
+    _seekRelative(const Duration(seconds: 10));
+  }
+
+  void _startHideTimer() {
+    final hideControlsTimer =
+        chewieController.hideControlsTimer.isNegative
+            ? ChewieController.defaultHideControlsTimer
+            : chewieController.hideControlsTimer;
+    _hideTimer = Timer(hideControlsTimer, () {
+      setState(() {
+        notifier.hideStuff = true;
+      });
+    });
+  }
+
+  void _bufferingTimerTimeout() {
+    _displayBufferingIndicator = true;
+    if (mounted) {
+      setState(() {});
+    }
+  }
+
+  void _updateState() {
+    if (!mounted) return;
+
+    final bool buffering = getIsBuffering(controller);
+
+    // display the progress bar indicator only after the buffering delay if it has been set
+    if (chewieController.progressIndicatorDelay != null) {
+      if (buffering) {
+        _bufferingDisplayTimer ??= Timer(
+          chewieController.progressIndicatorDelay!,
+          _bufferingTimerTimeout,
+        );
+      } else {
+        _bufferingDisplayTimer?.cancel();
+        _bufferingDisplayTimer = null;
+        _displayBufferingIndicator = false;
+      }
+    } else {
+      _displayBufferingIndicator = buffering;
+    }
+
+    setState(() {
+      _latestValue = controller.value;
+      _subtitlesPosition = controller.value.position;
+    });
+  }
+
+  Widget _buildProgressBar() {
+    return Expanded(
+      child: MaterialVideoProgressBar(
+        controller,
+        onDragStart: () {
+          setState(() {
+            _dragging = true;
+          });
+
+          _hideTimer?.cancel();
+        },
+        onDragUpdate: () {
+          _hideTimer?.cancel();
+        },
+        onDragEnd: () {
+          setState(() {
+            _dragging = false;
+          });
+
+          _startHideTimer();
+        },
+        colors:
+            chewieController.materialProgressColors ??
+            ChewieProgressColors(
+              playedColor: Theme.of(context).colorScheme.secondary,
+              handleColor: Theme.of(context).colorScheme.secondary,
+              bufferedColor: Theme.of(
+                context,
+              ).colorScheme.surface.withValues(alpha: 0.5),
+              backgroundColor: Theme.of(
+                context,
+              ).disabledColor.withValues(alpha: .5),
+            ),
+        draggableProgressBar: chewieController.draggableProgressBar,
+      ),
+    );
+  }
+}
diff --git a/lib/src/material/material_desktop_controls.dart b/lib/src/material/material_desktop_controls.dart
new file mode 100644
index 000000000..5ca96c55f
--- /dev/null
+++ b/lib/src/material/material_desktop_controls.dart
@@ -0,0 +1,639 @@
+import 'dart:async';
+
+import 'package:chewie/src/animated_play_pause.dart';
+import 'package:chewie/src/center_play_button.dart';
+import 'package:chewie/src/chewie_player.dart';
+import 'package:chewie/src/chewie_progress_colors.dart';
+import 'package:chewie/src/helpers/utils.dart';
+import 'package:chewie/src/material/material_progress_bar.dart';
+import 'package:chewie/src/material/widgets/options_dialog.dart';
+import 'package:chewie/src/material/widgets/playback_speed_dialog.dart';
+import 'package:chewie/src/models/option_item.dart';
+import 'package:chewie/src/models/subtitle_model.dart';
+import 'package:chewie/src/notifiers/index.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:provider/provider.dart';
+import 'package:video_player/video_player.dart';
+
+class MaterialDesktopControls extends StatefulWidget {
+  const MaterialDesktopControls({this.showPlayButton = true, super.key});
+
+  final bool showPlayButton;
+
+  @override
+  State<StatefulWidget> createState() {
+    return _MaterialDesktopControlsState();
+  }
+}
+
+class _MaterialDesktopControlsState extends State<MaterialDesktopControls>
+    with SingleTickerProviderStateMixin {
+  late PlayerNotifier notifier;
+  late VideoPlayerValue _latestValue;
+  double? _latestVolume;
+  Timer? _hideTimer;
+  Timer? _initTimer;
+  late var _subtitlesPosition = Duration.zero;
+  bool _subtitleOn = false;
+  Timer? _showAfterExpandCollapseTimer;
+  bool _dragging = false;
+  bool _displayTapped = false;
+  Timer? _bufferingDisplayTimer;
+  bool _displayBufferingIndicator = false;
+
+  final barHeight = 48.0 * 1.5;
+  final marginSize = 5.0;
+
+  late VideoPlayerController controller;
+  ChewieController? _chewieController;
+  late final FocusNode _focusNode;
+
+  // We know that _chewieController is set in didChangeDependencies
+  ChewieController get chewieController => _chewieController!;
+
+  @override
+  void initState() {
+    super.initState();
+    _focusNode = FocusNode();
+    _focusNode.requestFocus();
+    notifier = Provider.of<PlayerNotifier>(context, listen: false);
+  }
+
+  void _handleKeyPress(KeyEvent event) {
+    if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.space) {
+      _playPause();
+    } else if (event is KeyDownEvent &&
+        event.logicalKey == LogicalKeyboardKey.arrowRight) {
+      _seekForward();
+    } else if (event is KeyDownEvent &&
+        event.logicalKey == LogicalKeyboardKey.arrowLeft) {
+      _seekBackward();
+    } else if (event is KeyDownEvent &&
+        event.logicalKey == LogicalKeyboardKey.escape) {
+      if (chewieController.isFullScreen) {
+        _onExpandCollapse();
+      }
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    if (_latestValue.hasError) {
+      return chewieController.errorBuilder?.call(
+            context,
+            chewieController.videoPlayerController.value.errorDescription!,
+          ) ??
+          const Center(child: Icon(Icons.error, color: Colors.white, size: 42));
+    }
+
+    return KeyboardListener(
+      focusNode: _focusNode,
+      onKeyEvent: _handleKeyPress,
+      child: MouseRegion(
+        onHover: (_) {
+          _focusNode.requestFocus();
+          _cancelAndRestartTimer();
+        },
+        child: GestureDetector(
+          onTap: () {
+            _playPause();
+            _cancelAndRestartTimer();
+          },
+          child: AbsorbPointer(
+            absorbing: notifier.hideStuff,
+            child: Stack(
+              children: [
+                if (_displayBufferingIndicator)
+                  _chewieController?.bufferingBuilder?.call(context) ??
+                      const Center(child: CircularProgressIndicator())
+                else
+                  _buildHitArea(),
+                Column(
+                  mainAxisAlignment: MainAxisAlignment.end,
+                  children: <Widget>[
+                    if (_subtitleOn)
+                      Transform.translate(
+                        offset: Offset(
+                          0.0,
+                          notifier.hideStuff ? barHeight * 0.8 : 0.0,
+                        ),
+                        child: _buildSubtitles(
+                          context,
+                          chewieController.subtitle!,
+                        ),
+                      ),
+                    _buildBottomBar(context),
+                  ],
+                ),
+              ],
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+
+  @override
+  void dispose() {
+    _dispose();
+    _focusNode.dispose();
+    super.dispose();
+  }
+
+  void _dispose() {
+    controller.removeListener(_updateState);
+    _hideTimer?.cancel();
+    _initTimer?.cancel();
+    _showAfterExpandCollapseTimer?.cancel();
+  }
+
+  @override
+  void didChangeDependencies() {
+    final oldController = _chewieController;
+    _chewieController = ChewieController.of(context);
+    controller = chewieController.videoPlayerController;
+
+    if (oldController != chewieController) {
+      _dispose();
+      _initialize();
+    }
+
+    super.didChangeDependencies();
+  }
+
+  Widget _buildSubtitleToggle({IconData? icon, bool isPadded = false}) {
+    return IconButton(
+      padding: isPadded ? const EdgeInsets.all(8.0) : EdgeInsets.zero,
+      icon: Icon(icon, color: _subtitleOn ? Colors.white : Colors.grey[700]),
+      onPressed: _onSubtitleTap,
+    );
+  }
+
+  Widget _buildOptionsButton({IconData? icon, bool isPadded = false}) {
+    final options = <OptionItem>[
+      OptionItem(
+        onTap: (context) async {
+          Navigator.pop(context);
+          _onSpeedButtonTap();
+        },
+        iconData: Icons.speed,
+        title:
+            chewieController.optionsTranslation?.playbackSpeedButtonText ??
+            'Playback speed',
+      ),
+    ];
+
+    if (chewieController.additionalOptions != null &&
+        chewieController.additionalOptions!(context).isNotEmpty) {
+      options.addAll(chewieController.additionalOptions!(context));
+    }
+
+    return AnimatedOpacity(
+      opacity: notifier.hideStuff ? 0.0 : 1.0,
+      duration: const Duration(milliseconds: 250),
+      child: IconButton(
+        padding: isPadded ? const EdgeInsets.all(8.0) : EdgeInsets.zero,
+        onPressed: () async {
+          _hideTimer?.cancel();
+
+          if (chewieController.optionsBuilder != null) {
+            await chewieController.optionsBuilder!(context, options);
+          } else {
+            await showModalBottomSheet<OptionItem>(
+              context: context,
+              isScrollControlled: true,
+              useRootNavigator: chewieController.useRootNavigator,
+              builder:
+                  (context) => OptionsDialog(
+                    options: options,
+                    cancelButtonText:
+                        chewieController.optionsTranslation?.cancelButtonText,
+                  ),
+            );
+          }
+
+          if (_latestValue.isPlaying) {
+            _startHideTimer();
+          }
+        },
+        icon: Icon(icon ?? Icons.more_vert, color: Colors.white),
+      ),
+    );
+  }
+
+  Widget _buildSubtitles(BuildContext context, Subtitles subtitles) {
+    if (!_subtitleOn) {
+      return const SizedBox();
+    }
+    final currentSubtitle = subtitles.getByPosition(_subtitlesPosition);
+    if (currentSubtitle.isEmpty) {
+      return const SizedBox();
+    }
+
+    if (chewieController.subtitleBuilder != null) {
+      return chewieController.subtitleBuilder!(
+        context,
+        currentSubtitle.first!.text,
+      );
+    }
+
+    return Padding(
+      padding: EdgeInsets.all(marginSize),
+      child: Container(
+        padding: const EdgeInsets.all(5),
+        decoration: BoxDecoration(
+          color: const Color(0x96000000),
+          borderRadius: BorderRadius.circular(10.0),
+        ),
+        child: Text(
+          currentSubtitle.first!.text.toString(),
+          style: const TextStyle(fontSize: 18),
+          textAlign: TextAlign.center,
+        ),
+      ),
+    );
+  }
+
+  AnimatedOpacity _buildBottomBar(BuildContext context) {
+    final iconColor = Theme.of(context).textTheme.labelLarge!.color;
+
+    return AnimatedOpacity(
+      opacity: notifier.hideStuff ? 0.0 : 1.0,
+      duration: const Duration(milliseconds: 300),
+      child: Container(
+        height: barHeight + (chewieController.isFullScreen ? 20.0 : 0),
+        padding: EdgeInsets.only(
+          bottom: chewieController.isFullScreen ? 10.0 : 15,
+        ),
+        child: SafeArea(
+          bottom: chewieController.isFullScreen,
+          child: Column(
+            mainAxisSize: MainAxisSize.min,
+            mainAxisAlignment: MainAxisAlignment.center,
+            verticalDirection: VerticalDirection.up,
+            children: [
+              Flexible(
+                child: Row(
+                  children: <Widget>[
+                    _buildPlayPause(controller),
+                    if (chewieController.allowMuting)
+                      _buildMuteButton(controller),
+                    if (chewieController.isLive)
+                      const Expanded(child: Text('LIVE'))
+                    else
+                      _buildPosition(iconColor),
+                    const Spacer(),
+                    if (chewieController.showControls &&
+                        chewieController.subtitle != null &&
+                        chewieController.subtitle!.isNotEmpty)
+                      _buildSubtitleToggle(icon: Icons.subtitles),
+                    if (chewieController.showOptions)
+                      _buildOptionsButton(icon: Icons.settings),
+                    if (chewieController.allowFullScreen) _buildExpandButton(),
+                  ],
+                ),
+              ),
+              if (!chewieController.isLive)
+                Expanded(
+                  child: Container(
+                    padding: EdgeInsets.only(
+                      right: 20,
+                      left: 20,
+                      bottom: chewieController.isFullScreen ? 5.0 : 0,
+                    ),
+                    child: Row(children: [_buildProgressBar()]),
+                  ),
+                ),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+
+  GestureDetector _buildExpandButton() {
+    return GestureDetector(
+      onTap: _onExpandCollapse,
+      child: AnimatedOpacity(
+        opacity: notifier.hideStuff ? 0.0 : 1.0,
+        duration: const Duration(milliseconds: 300),
+        child: Container(
+          height: barHeight + (chewieController.isFullScreen ? 15.0 : 0),
+          margin: const EdgeInsets.only(right: 12.0),
+          padding: const EdgeInsets.only(left: 8.0, right: 8.0),
+          child: Center(
+            child: Icon(
+              chewieController.isFullScreen
+                  ? Icons.fullscreen_exit
+                  : Icons.fullscreen,
+              color: Colors.white,
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+
+  Widget _buildHitArea() {
+    final bool isFinished =
+        _latestValue.position >= _latestValue.duration &&
+        _latestValue.duration.inSeconds > 0;
+    final bool showPlayButton =
+        widget.showPlayButton && !_dragging && !notifier.hideStuff;
+
+    return GestureDetector(
+      onTap: () {
+        if (_latestValue.isPlaying) {
+          if (_chewieController?.pauseOnBackgroundTap ?? false) {
+            _playPause();
+            _cancelAndRestartTimer();
+          } else {
+            if (_displayTapped) {
+              setState(() {
+                notifier.hideStuff = true;
+              });
+            } else {
+              _cancelAndRestartTimer();
+            }
+          }
+        } else {
+          _playPause();
+
+          setState(() {
+            notifier.hideStuff = true;
+          });
+        }
+      },
+      child: CenterPlayButton(
+        backgroundColor: Colors.black54,
+        iconColor: Colors.white,
+        isFinished: isFinished,
+        isPlaying: controller.value.isPlaying,
+        show: showPlayButton,
+        onPressed: _playPause,
+      ),
+    );
+  }
+
+  Future<void> _onSpeedButtonTap() async {
+    _hideTimer?.cancel();
+
+    final chosenSpeed = await showModalBottomSheet<double>(
+      context: context,
+      isScrollControlled: true,
+      useRootNavigator: chewieController.useRootNavigator,
+      builder:
+          (context) => PlaybackSpeedDialog(
+            speeds: chewieController.playbackSpeeds,
+            selected: _latestValue.playbackSpeed,
+          ),
+    );
+
+    if (chosenSpeed != null) {
+      controller.setPlaybackSpeed(chosenSpeed);
+    }
+
+    if (_latestValue.isPlaying) {
+      _startHideTimer();
+    }
+  }
+
+  GestureDetector _buildMuteButton(VideoPlayerController controller) {
+    return GestureDetector(
+      onTap: () {
+        _cancelAndRestartTimer();
+
+        if (_latestValue.volume == 0) {
+          controller.setVolume(_latestVolume ?? 0.5);
+        } else {
+          _latestVolume = controller.value.volume;
+          controller.setVolume(0.0);
+        }
+      },
+      child: AnimatedOpacity(
+        opacity: notifier.hideStuff ? 0.0 : 1.0,
+        duration: const Duration(milliseconds: 300),
+        child: ClipRect(
+          child: Container(
+            height: barHeight,
+            padding: const EdgeInsets.only(right: 15.0),
+            child: Icon(
+              _latestValue.volume > 0 ? Icons.volume_up : Icons.volume_off,
+              color: Colors.white,
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+
+  GestureDetector _buildPlayPause(VideoPlayerController controller) {
+    return GestureDetector(
+      onTap: _playPause,
+      child: Container(
+        height: barHeight,
+        color: Colors.transparent,
+        margin: const EdgeInsets.only(left: 8.0, right: 4.0),
+        padding: const EdgeInsets.only(left: 12.0, right: 12.0),
+        child: AnimatedPlayPause(
+          playing: controller.value.isPlaying,
+          color: Colors.white,
+        ),
+      ),
+    );
+  }
+
+  Widget _buildPosition(Color? iconColor) {
+    final position = _latestValue.position;
+    final duration = _latestValue.duration;
+
+    return Text(
+      '${formatDuration(position)} / ${formatDuration(duration)}',
+      style: const TextStyle(fontSize: 14.0, color: Colors.white),
+    );
+  }
+
+  void _onSubtitleTap() {
+    setState(() {
+      _subtitleOn = !_subtitleOn;
+    });
+  }
+
+  void _cancelAndRestartTimer() {
+    _hideTimer?.cancel();
+    _startHideTimer();
+
+    setState(() {
+      notifier.hideStuff = false;
+      _displayTapped = true;
+    });
+  }
+
+  Future<void> _initialize() async {
+    _subtitleOn =
+        chewieController.showSubtitles &&
+        (chewieController.subtitle?.isNotEmpty ?? false);
+    controller.addListener(_updateState);
+
+    _updateState();
+
+    if (controller.value.isPlaying || chewieController.autoPlay) {
+      _startHideTimer();
+    }
+
+    if (chewieController.showControlsOnInitialize) {
+      _initTimer = Timer(const Duration(milliseconds: 200), () {
+        setState(() {
+          notifier.hideStuff = false;
+        });
+      });
+    }
+  }
+
+  void _onExpandCollapse() {
+    setState(() {
+      notifier.hideStuff = true;
+    });
+
+    chewieController.toggleFullScreen();
+
+    _showAfterExpandCollapseTimer = Timer(
+      const Duration(milliseconds: 300),
+      () {
+        setState(() {
+          _cancelAndRestartTimer();
+        });
+      },
+    );
+  }
+
+  void _playPause() {
+    if (controller.value.isPlaying) {
+      setState(() {
+        notifier.hideStuff = false;
+      });
+
+      _hideTimer?.cancel();
+      controller.pause();
+    } else {
+      _cancelAndRestartTimer();
+
+      if (!controller.value.isInitialized) {
+        controller.initialize().then((_) {
+          //[VideoPlayerController.play] If the video is at the end, this method starts playing from the beginning
+          controller.play();
+        });
+      } else {
+        //[VideoPlayerController.play] If the video is at the end, this method starts playing from the beginning
+        controller.play();
+      }
+    }
+  }
+
+  void _startHideTimer() {
+    final hideControlsTimer =
+        chewieController.hideControlsTimer.isNegative
+            ? ChewieController.defaultHideControlsTimer
+            : chewieController.hideControlsTimer;
+    _hideTimer = Timer(hideControlsTimer, () {
+      setState(() {
+        notifier.hideStuff = true;
+      });
+    });
+  }
+
+  void _bufferingTimerTimeout() {
+    _displayBufferingIndicator = true;
+    if (mounted) {
+      setState(() {});
+    }
+  }
+
+  void _updateState() {
+    if (!mounted) return;
+
+    final bool buffering = getIsBuffering(controller);
+
+    // display the progress bar indicator only after the buffering delay if it has been set
+    if (chewieController.progressIndicatorDelay != null) {
+      if (buffering) {
+        _bufferingDisplayTimer ??= Timer(
+          chewieController.progressIndicatorDelay!,
+          _bufferingTimerTimeout,
+        );
+      } else {
+        _bufferingDisplayTimer?.cancel();
+        _bufferingDisplayTimer = null;
+        _displayBufferingIndicator = false;
+      }
+    } else {
+      _displayBufferingIndicator = buffering;
+    }
+
+    setState(() {
+      _latestValue = controller.value;
+      _subtitlesPosition = controller.value.position;
+    });
+  }
+
+  void _seekBackward() {
+    _seekRelative(const Duration(seconds: -10));
+  }
+
+  void _seekForward() {
+    _seekRelative(const Duration(seconds: 10));
+  }
+
+  void _seekRelative(Duration relativeSeek) {
+    _cancelAndRestartTimer();
+    final position = _latestValue.position + relativeSeek;
+    final duration = _latestValue.duration;
+
+    if (position < Duration.zero) {
+      controller.seekTo(Duration.zero);
+    } else if (position > duration) {
+      controller.seekTo(duration);
+    } else {
+      controller.seekTo(position);
+    }
+  }
+
+  Widget _buildProgressBar() {
+    return Expanded(
+      child: MaterialVideoProgressBar(
+        controller,
+        onDragStart: () {
+          setState(() {
+            _dragging = true;
+          });
+
+          _hideTimer?.cancel();
+        },
+        onDragUpdate: () {
+          _hideTimer?.cancel();
+        },
+        onDragEnd: () {
+          setState(() {
+            _dragging = false;
+          });
+
+          _startHideTimer();
+        },
+        colors:
+            chewieController.materialProgressColors ??
+            ChewieProgressColors(
+              playedColor: Theme.of(context).colorScheme.secondary,
+              handleColor: Theme.of(context).colorScheme.secondary,
+              bufferedColor: Theme.of(
+                context,
+              ).colorScheme.surface.withValues(alpha: 0.5),
+              backgroundColor: Theme.of(
+                context,
+              ).disabledColor.withValues(alpha: 0.5),
+            ),
+        draggableProgressBar: chewieController.draggableProgressBar,
+      ),
+    );
+  }
+}
diff --git a/lib/src/material/material_progress_bar.dart b/lib/src/material/material_progress_bar.dart
new file mode 100644
index 000000000..ba58a37d6
--- /dev/null
+++ b/lib/src/material/material_progress_bar.dart
@@ -0,0 +1,44 @@
+import 'package:chewie/src/chewie_progress_colors.dart';
+import 'package:chewie/src/progress_bar.dart';
+import 'package:flutter/material.dart';
+import 'package:video_player/video_player.dart';
+
+class MaterialVideoProgressBar extends StatelessWidget {
+  MaterialVideoProgressBar(
+    this.controller, {
+    this.height = kToolbarHeight,
+    this.barHeight = 10,
+    this.handleHeight = 6,
+    ChewieProgressColors? colors,
+    this.onDragEnd,
+    this.onDragStart,
+    this.onDragUpdate,
+    super.key,
+    this.draggableProgressBar = true,
+  }) : colors = colors ?? ChewieProgressColors();
+
+  final double height;
+  final double barHeight;
+  final double handleHeight;
+  final VideoPlayerController controller;
+  final ChewieProgressColors colors;
+  final Function()? onDragStart;
+  final Function()? onDragEnd;
+  final Function()? onDragUpdate;
+  final bool draggableProgressBar;
+
+  @override
+  Widget build(BuildContext context) {
+    return VideoProgressBar(
+      controller,
+      barHeight: barHeight,
+      handleHeight: handleHeight,
+      drawShadow: true,
+      colors: colors,
+      onDragEnd: onDragEnd,
+      onDragStart: onDragStart,
+      onDragUpdate: onDragUpdate,
+      draggableProgressBar: draggableProgressBar,
+    );
+  }
+}
diff --git a/lib/src/material/widgets/options_dialog.dart b/lib/src/material/widgets/options_dialog.dart
new file mode 100644
index 000000000..80817868f
--- /dev/null
+++ b/lib/src/material/widgets/options_dialog.dart
@@ -0,0 +1,54 @@
+import 'package:chewie/src/models/option_item.dart';
+import 'package:flutter/material.dart';
+
+class OptionsDialog extends StatefulWidget {
+  const OptionsDialog({
+    super.key,
+    required this.options,
+    this.cancelButtonText,
+  });
+
+  final List<OptionItem> options;
+  final String? cancelButtonText;
+
+  @override
+  // ignore: library_private_types_in_public_api
+  _OptionsDialogState createState() => _OptionsDialogState();
+}
+
+class _OptionsDialogState extends State<OptionsDialog> {
+  @override
+  Widget build(BuildContext context) {
+    return SafeArea(
+      child: Column(
+        mainAxisSize: MainAxisSize.min,
+        children: [
+          ListView.builder(
+            shrinkWrap: true,
+            itemCount: widget.options.length,
+            itemBuilder: (context, i) {
+              return ListTile(
+                onTap: () => widget.options[i].onTap(context),
+                leading: Icon(widget.options[i].iconData),
+                title: Text(widget.options[i].title),
+                subtitle:
+                    widget.options[i].subtitle != null
+                        ? Text(widget.options[i].subtitle!)
+                        : null,
+              );
+            },
+          ),
+          const Padding(
+            padding: EdgeInsets.symmetric(horizontal: 16),
+            child: Divider(thickness: 1.0),
+          ),
+          ListTile(
+            onTap: () => Navigator.pop(context),
+            leading: const Icon(Icons.close),
+            title: Text(widget.cancelButtonText ?? 'Cancel'),
+          ),
+        ],
+      ),
+    );
+  }
+}
diff --git a/lib/src/material/widgets/playback_speed_dialog.dart b/lib/src/material/widgets/playback_speed_dialog.dart
new file mode 100644
index 000000000..331c8ee72
--- /dev/null
+++ b/lib/src/material/widgets/playback_speed_dialog.dart
@@ -0,0 +1,44 @@
+import 'package:flutter/material.dart';
+
+class PlaybackSpeedDialog extends StatelessWidget {
+  const PlaybackSpeedDialog({
+    super.key,
+    required List<double> speeds,
+    required double selected,
+  }) : _speeds = speeds,
+       _selected = selected;
+
+  final List<double> _speeds;
+  final double _selected;
+
+  @override
+  Widget build(BuildContext context) {
+    final Color selectedColor = Theme.of(context).primaryColor;
+
+    return ListView.builder(
+      shrinkWrap: true,
+      physics: const ScrollPhysics(),
+      itemBuilder: (context, index) {
+        final speed = _speeds[index];
+        return ListTile(
+          dense: true,
+          title: Row(
+            children: [
+              if (speed == _selected)
+                Icon(Icons.check, size: 20.0, color: selectedColor)
+              else
+                Container(width: 20.0),
+              const SizedBox(width: 16.0),
+              Text(speed.toString()),
+            ],
+          ),
+          selected: speed == _selected,
+          onTap: () {
+            Navigator.of(context).pop(speed);
+          },
+        );
+      },
+      itemCount: _speeds.length,
+    );
+  }
+}
diff --git a/lib/src/material_controls.dart b/lib/src/material_controls.dart
deleted file mode 100644
index 8ad8be8a1..000000000
--- a/lib/src/material_controls.dart
+++ /dev/null
@@ -1,399 +0,0 @@
-import 'dart:async';
-
-import 'package:chewie/src/chewie_player.dart';
-import 'package:chewie/src/chewie_progress_colors.dart';
-import 'package:chewie/src/material_progress_bar.dart';
-import 'package:chewie/src/utils.dart';
-import 'package:flutter/material.dart';
-import 'package:video_player/video_player.dart';
-
-class MaterialControls extends StatefulWidget {
-  const MaterialControls({Key key}) : super(key: key);
-
-  @override
-  State<StatefulWidget> createState() {
-    return _MaterialControlsState();
-  }
-}
-
-class _MaterialControlsState extends State<MaterialControls> {
-  VideoPlayerValue _latestValue;
-  double _latestVolume;
-  bool _hideStuff = true;
-  Timer _hideTimer;
-  Timer _initTimer;
-  Timer _showAfterExpandCollapseTimer;
-  bool _dragging = false;
-  bool _displayTapped = false;
-
-  final barHeight = 48.0;
-  final marginSize = 5.0;
-
-  VideoPlayerController controller;
-  ChewieController chewieController;
-
-  @override
-  Widget build(BuildContext context) {
-    if (_latestValue.hasError) {
-      return chewieController.errorBuilder != null
-          ? chewieController.errorBuilder(
-              context,
-              chewieController.videoPlayerController.value.errorDescription,
-            )
-          : Center(
-              child: Icon(
-                Icons.error,
-                color: Colors.white,
-                size: 42,
-              ),
-            );
-    }
-
-    return MouseRegion(
-      onHover: (_) {
-        _cancelAndRestartTimer();
-      },
-      child: GestureDetector(
-        onTap: () => _cancelAndRestartTimer(),
-        child: AbsorbPointer(
-          absorbing: _hideStuff,
-          child: Column(
-            children: <Widget>[
-              _latestValue != null &&
-                          !_latestValue.isPlaying &&
-                          _latestValue.duration == null ||
-                      _latestValue.isBuffering
-                  ? const Expanded(
-                      child: const Center(
-                        child: const CircularProgressIndicator(),
-                      ),
-                    )
-                  : _buildHitArea(),
-              _buildBottomBar(context),
-            ],
-          ),
-        ),
-      ),
-    );
-  }
-
-  @override
-  void dispose() {
-    _dispose();
-    super.dispose();
-  }
-
-  void _dispose() {
-    controller.removeListener(_updateState);
-    _hideTimer?.cancel();
-    _initTimer?.cancel();
-    _showAfterExpandCollapseTimer?.cancel();
-  }
-
-  @override
-  void didChangeDependencies() {
-    final _oldController = chewieController;
-    chewieController = ChewieController.of(context);
-    controller = chewieController.videoPlayerController;
-
-    if (_oldController != chewieController) {
-      _dispose();
-      _initialize();
-    }
-
-    super.didChangeDependencies();
-  }
-
-  AnimatedOpacity _buildBottomBar(
-    BuildContext context,
-  ) {
-    final iconColor = Theme.of(context).textTheme.button.color;
-
-    return AnimatedOpacity(
-      opacity: _hideStuff ? 0.0 : 1.0,
-      duration: Duration(milliseconds: 300),
-      child: Container(
-        height: barHeight,
-        color: Theme.of(context).dialogBackgroundColor,
-        child: Row(
-          children: <Widget>[
-            _buildPlayPause(controller),
-            chewieController.isLive
-                ? Expanded(child: const Text('LIVE'))
-                : _buildPosition(iconColor),
-            chewieController.isLive ? const SizedBox() : _buildProgressBar(),
-            chewieController.allowMuting
-                ? _buildMuteButton(controller)
-                : Container(),
-            chewieController.allowFullScreen
-                ? _buildExpandButton()
-                : Container(),
-          ],
-        ),
-      ),
-    );
-  }
-
-  GestureDetector _buildExpandButton() {
-    return GestureDetector(
-      onTap: _onExpandCollapse,
-      child: AnimatedOpacity(
-        opacity: _hideStuff ? 0.0 : 1.0,
-        duration: Duration(milliseconds: 300),
-        child: Container(
-          height: barHeight,
-          margin: EdgeInsets.only(right: 12.0),
-          padding: EdgeInsets.only(
-            left: 8.0,
-            right: 8.0,
-          ),
-          child: Center(
-            child: Icon(
-              chewieController.isFullScreen
-                  ? Icons.fullscreen_exit
-                  : Icons.fullscreen,
-            ),
-          ),
-        ),
-      ),
-    );
-  }
-
-  Expanded _buildHitArea() {
-    return Expanded(
-      child: GestureDetector(
-        onTap: () {
-          if (_latestValue != null && _latestValue.isPlaying) {
-            if (_displayTapped) {
-              setState(() {
-                _hideStuff = true;
-              });
-            } else
-              _cancelAndRestartTimer();
-          } else {
-            _playPause();
-
-            setState(() {
-              _hideStuff = true;
-            });
-          }
-        },
-        child: Container(
-          color: Colors.transparent,
-          child: Center(
-            child: AnimatedOpacity(
-              opacity:
-                  _latestValue != null && !_latestValue.isPlaying && !_dragging
-                      ? 1.0
-                      : 0.0,
-              duration: Duration(milliseconds: 300),
-              child: GestureDetector(
-                child: Container(
-                  decoration: BoxDecoration(
-                    color: Theme.of(context).dialogBackgroundColor,
-                    borderRadius: BorderRadius.circular(48.0),
-                  ),
-                  child: Padding(
-                    padding: EdgeInsets.all(12.0),
-                    child: Icon(Icons.play_arrow, size: 32.0),
-                  ),
-                ),
-              ),
-            ),
-          ),
-        ),
-      ),
-    );
-  }
-
-  GestureDetector _buildMuteButton(
-    VideoPlayerController controller,
-  ) {
-    return GestureDetector(
-      onTap: () {
-        _cancelAndRestartTimer();
-
-        if (_latestValue.volume == 0) {
-          controller.setVolume(_latestVolume ?? 0.5);
-        } else {
-          _latestVolume = controller.value.volume;
-          controller.setVolume(0.0);
-        }
-      },
-      child: AnimatedOpacity(
-        opacity: _hideStuff ? 0.0 : 1.0,
-        duration: Duration(milliseconds: 300),
-        child: ClipRect(
-          child: Container(
-            child: Container(
-              height: barHeight,
-              padding: EdgeInsets.only(
-                left: 8.0,
-                right: 8.0,
-              ),
-              child: Icon(
-                (_latestValue != null && _latestValue.volume > 0)
-                    ? Icons.volume_up
-                    : Icons.volume_off,
-              ),
-            ),
-          ),
-        ),
-      ),
-    );
-  }
-
-  GestureDetector _buildPlayPause(VideoPlayerController controller) {
-    return GestureDetector(
-      onTap: _playPause,
-      child: Container(
-        height: barHeight,
-        color: Colors.transparent,
-        margin: EdgeInsets.only(left: 8.0, right: 4.0),
-        padding: EdgeInsets.only(
-          left: 12.0,
-          right: 12.0,
-        ),
-        child: Icon(
-          controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
-        ),
-      ),
-    );
-  }
-
-  Widget _buildPosition(Color iconColor) {
-    final position = _latestValue != null && _latestValue.position != null
-        ? _latestValue.position
-        : Duration.zero;
-    final duration = _latestValue != null && _latestValue.duration != null
-        ? _latestValue.duration
-        : Duration.zero;
-
-    return Padding(
-      padding: EdgeInsets.only(right: 24.0),
-      child: Text(
-        '${formatDuration(position)} / ${formatDuration(duration)}',
-        style: TextStyle(
-          fontSize: 14.0,
-        ),
-      ),
-    );
-  }
-
-  void _cancelAndRestartTimer() {
-    _hideTimer?.cancel();
-    _startHideTimer();
-
-    setState(() {
-      _hideStuff = false;
-      _displayTapped = true;
-    });
-  }
-
-  Future<Null> _initialize() async {
-    controller.addListener(_updateState);
-
-    _updateState();
-
-    if ((controller.value != null && controller.value.isPlaying) ||
-        chewieController.autoPlay) {
-      _startHideTimer();
-    }
-
-    if (chewieController.showControlsOnInitialize) {
-      _initTimer = Timer(Duration(milliseconds: 200), () {
-        setState(() {
-          _hideStuff = false;
-        });
-      });
-    }
-  }
-
-  void _onExpandCollapse() {
-    setState(() {
-      _hideStuff = true;
-
-      chewieController.toggleFullScreen();
-      _showAfterExpandCollapseTimer = Timer(Duration(milliseconds: 300), () {
-        setState(() {
-          _cancelAndRestartTimer();
-        });
-      });
-    });
-  }
-
-  void _playPause() {
-    bool isFinished;
-    if (_latestValue.duration != null) {
-      isFinished = _latestValue.position >= _latestValue.duration;
-    } else {
-      isFinished = false;
-    }
-
-    setState(() {
-      if (controller.value.isPlaying) {
-        _hideStuff = false;
-        _hideTimer?.cancel();
-        controller.pause();
-      } else {
-        _cancelAndRestartTimer();
-
-        if (!controller.value.initialized) {
-          controller.initialize().then((_) {
-            controller.play();
-          });
-        } else {
-          if (isFinished) {
-            controller.seekTo(Duration(seconds: 0));
-          }
-          controller.play();
-        }
-      }
-    });
-  }
-
-  void _startHideTimer() {
-    _hideTimer = Timer(const Duration(seconds: 3), () {
-      setState(() {
-        _hideStuff = true;
-      });
-    });
-  }
-
-  void _updateState() {
-    setState(() {
-      _latestValue = controller.value;
-    });
-  }
-
-  Widget _buildProgressBar() {
-    return Expanded(
-      child: Padding(
-        padding: EdgeInsets.only(right: 20.0),
-        child: MaterialVideoProgressBar(
-          controller,
-          onDragStart: () {
-            setState(() {
-              _dragging = true;
-            });
-
-            _hideTimer?.cancel();
-          },
-          onDragEnd: () {
-            setState(() {
-              _dragging = false;
-            });
-
-            _startHideTimer();
-          },
-          colors: chewieController.materialProgressColors ??
-              ChewieProgressColors(
-                  playedColor: Theme.of(context).accentColor,
-                  handleColor: Theme.of(context).accentColor,
-                  bufferedColor: Theme.of(context).backgroundColor,
-                  backgroundColor: Theme.of(context).disabledColor),
-        ),
-      ),
-    );
-  }
-}
diff --git a/lib/src/material_progress_bar.dart b/lib/src/material_progress_bar.dart
deleted file mode 100644
index 5beac567f..000000000
--- a/lib/src/material_progress_bar.dart
+++ /dev/null
@@ -1,180 +0,0 @@
-import 'package:chewie/src/chewie_progress_colors.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter/widgets.dart';
-import 'package:video_player/video_player.dart';
-
-class MaterialVideoProgressBar extends StatefulWidget {
-  MaterialVideoProgressBar(
-    this.controller, {
-    ChewieProgressColors colors,
-    this.onDragEnd,
-    this.onDragStart,
-    this.onDragUpdate,
-  }) : colors = colors ?? ChewieProgressColors();
-
-  final VideoPlayerController controller;
-  final ChewieProgressColors colors;
-  final Function() onDragStart;
-  final Function() onDragEnd;
-  final Function() onDragUpdate;
-
-  @override
-  _VideoProgressBarState createState() {
-    return _VideoProgressBarState();
-  }
-}
-
-class _VideoProgressBarState extends State<MaterialVideoProgressBar> {
-  _VideoProgressBarState() {
-    listener = () {
-      if (!this.mounted) return;
-      setState(() {});
-    };
-  }
-
-  VoidCallback listener;
-  bool _controllerWasPlaying = false;
-
-  VideoPlayerController get controller => widget.controller;
-
-  @override
-  void initState() {
-    super.initState();
-    controller.addListener(listener);
-  }
-
-  @override
-  void deactivate() {
-    controller.removeListener(listener);
-    super.deactivate();
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    void seekToRelativePosition(Offset globalPosition) {
-      final box = context.findRenderObject() as RenderBox;
-      final Offset tapPos = box.globalToLocal(globalPosition);
-      final double relative = tapPos.dx / box.size.width;
-      final Duration position = controller.value.duration * relative;
-      controller.seekTo(position);
-    }
-
-    return GestureDetector(
-      child: Center(
-        child: Container(
-          height: MediaQuery.of(context).size.height / 2,
-          width: MediaQuery.of(context).size.width,
-          color: Colors.transparent,
-          child: CustomPaint(
-            painter: _ProgressBarPainter(
-              controller.value,
-              widget.colors,
-            ),
-          ),
-        ),
-      ),
-      onHorizontalDragStart: (DragStartDetails details) {
-        if (!controller.value.initialized) {
-          return;
-        }
-        _controllerWasPlaying = controller.value.isPlaying;
-        if (_controllerWasPlaying) {
-          controller.pause();
-        }
-
-        if (widget.onDragStart != null) {
-          widget.onDragStart();
-        }
-      },
-      onHorizontalDragUpdate: (DragUpdateDetails details) {
-        if (!controller.value.initialized) {
-          return;
-        }
-        seekToRelativePosition(details.globalPosition);
-
-        if (widget.onDragUpdate != null) {
-          widget.onDragUpdate();
-        }
-      },
-      onHorizontalDragEnd: (DragEndDetails details) {
-        if (_controllerWasPlaying) {
-          controller.play();
-        }
-
-        if (widget.onDragEnd != null) {
-          widget.onDragEnd();
-        }
-      },
-      onTapDown: (TapDownDetails details) {
-        if (!controller.value.initialized) {
-          return;
-        }
-        seekToRelativePosition(details.globalPosition);
-      },
-    );
-  }
-}
-
-class _ProgressBarPainter extends CustomPainter {
-  _ProgressBarPainter(this.value, this.colors);
-
-  VideoPlayerValue value;
-  ChewieProgressColors colors;
-
-  @override
-  bool shouldRepaint(CustomPainter painter) {
-    return true;
-  }
-
-  @override
-  void paint(Canvas canvas, Size size) {
-    final height = 2.0;
-
-    canvas.drawRRect(
-      RRect.fromRectAndRadius(
-        Rect.fromPoints(
-          Offset(0.0, size.height / 2),
-          Offset(size.width, size.height / 2 + height),
-        ),
-        Radius.circular(4.0),
-      ),
-      colors.backgroundPaint,
-    );
-    if (!value.initialized) {
-      return;
-    }
-    final double playedPartPercent =
-        value.position.inMilliseconds / value.duration.inMilliseconds;
-    final double playedPart =
-        playedPartPercent > 1 ? size.width : playedPartPercent * size.width;
-    for (DurationRange range in value.buffered) {
-      final double start = range.startFraction(value.duration) * size.width;
-      final double end = range.endFraction(value.duration) * size.width;
-      canvas.drawRRect(
-        RRect.fromRectAndRadius(
-          Rect.fromPoints(
-            Offset(start, size.height / 2),
-            Offset(end, size.height / 2 + height),
-          ),
-          Radius.circular(4.0),
-        ),
-        colors.bufferedPaint,
-      );
-    }
-    canvas.drawRRect(
-      RRect.fromRectAndRadius(
-        Rect.fromPoints(
-          Offset(0.0, size.height / 2),
-          Offset(playedPart, size.height / 2 + height),
-        ),
-        Radius.circular(4.0),
-      ),
-      colors.playedPaint,
-    );
-    canvas.drawCircle(
-      Offset(playedPart, size.height / 2 + height / 2),
-      height * 3,
-      colors.handlePaint,
-    );
-  }
-}
diff --git a/lib/src/models/index.dart b/lib/src/models/index.dart
new file mode 100644
index 000000000..a308c33db
--- /dev/null
+++ b/lib/src/models/index.dart
@@ -0,0 +1,3 @@
+export 'option_item.dart';
+export 'options_translation.dart';
+export 'subtitle_model.dart';
diff --git a/lib/src/models/option_item.dart b/lib/src/models/option_item.dart
new file mode 100644
index 000000000..089d34294
--- /dev/null
+++ b/lib/src/models/option_item.dart
@@ -0,0 +1,48 @@
+import 'package:flutter/material.dart';
+
+class OptionItem {
+  OptionItem({
+    required this.onTap,
+    required this.iconData,
+    required this.title,
+    this.subtitle,
+  });
+
+  final void Function(BuildContext context) onTap;
+  final IconData iconData;
+  final String title;
+  final String? subtitle;
+
+  OptionItem copyWith({
+    Function(BuildContext context)? onTap,
+    IconData? iconData,
+    String? title,
+    String? subtitle,
+  }) {
+    return OptionItem(
+      onTap: onTap ?? this.onTap,
+      iconData: iconData ?? this.iconData,
+      title: title ?? this.title,
+      subtitle: subtitle ?? this.subtitle,
+    );
+  }
+
+  @override
+  String toString() =>
+      'OptionItem(onTap: $onTap, iconData: $iconData, title: $title, subtitle: $subtitle)';
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other)) return true;
+
+    return other is OptionItem &&
+        other.onTap == onTap &&
+        other.iconData == iconData &&
+        other.title == title &&
+        other.subtitle == subtitle;
+  }
+
+  @override
+  int get hashCode =>
+      onTap.hashCode ^ iconData.hashCode ^ title.hashCode ^ subtitle.hashCode;
+}
diff --git a/lib/src/models/options_translation.dart b/lib/src/models/options_translation.dart
new file mode 100644
index 000000000..ccbe97654
--- /dev/null
+++ b/lib/src/models/options_translation.dart
@@ -0,0 +1,44 @@
+class OptionsTranslation {
+  OptionsTranslation({
+    this.playbackSpeedButtonText,
+    this.subtitlesButtonText,
+    this.cancelButtonText,
+  });
+
+  String? playbackSpeedButtonText;
+  String? subtitlesButtonText;
+  String? cancelButtonText;
+
+  OptionsTranslation copyWith({
+    String? playbackSpeedButtonText,
+    String? subtitlesButtonText,
+    String? cancelButtonText,
+  }) {
+    return OptionsTranslation(
+      playbackSpeedButtonText:
+          playbackSpeedButtonText ?? this.playbackSpeedButtonText,
+      subtitlesButtonText: subtitlesButtonText ?? this.subtitlesButtonText,
+      cancelButtonText: cancelButtonText ?? this.cancelButtonText,
+    );
+  }
+
+  @override
+  String toString() =>
+      'OptionsTranslation(playbackSpeedButtonText: $playbackSpeedButtonText, subtitlesButtonText: $subtitlesButtonText, cancelButtonText: $cancelButtonText)';
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other)) return true;
+
+    return other is OptionsTranslation &&
+        other.playbackSpeedButtonText == playbackSpeedButtonText &&
+        other.subtitlesButtonText == subtitlesButtonText &&
+        other.cancelButtonText == cancelButtonText;
+  }
+
+  @override
+  int get hashCode =>
+      playbackSpeedButtonText.hashCode ^
+      subtitlesButtonText.hashCode ^
+      cancelButtonText.hashCode;
+}
diff --git a/lib/src/models/subtitle_model.dart b/lib/src/models/subtitle_model.dart
new file mode 100644
index 000000000..e77182e6c
--- /dev/null
+++ b/lib/src/models/subtitle_model.dart
@@ -0,0 +1,70 @@
+class Subtitles {
+  Subtitles(this.subtitle);
+
+  final List<Subtitle?> subtitle;
+
+  bool get isEmpty => subtitle.isEmpty;
+
+  bool get isNotEmpty => !isEmpty;
+
+  List<Subtitle?> getByPosition(Duration position) {
+    final found =
+        subtitle.where((item) {
+          if (item != null) {
+            return position >= item.start && position <= item.end;
+          }
+          return false;
+        }).toList();
+
+    return found;
+  }
+}
+
+class Subtitle {
+  Subtitle({
+    required this.index,
+    required this.start,
+    required this.end,
+    required this.text,
+  });
+
+  Subtitle copyWith({
+    int? index,
+    Duration? start,
+    Duration? end,
+    dynamic text,
+  }) {
+    return Subtitle(
+      index: index ?? this.index,
+      start: start ?? this.start,
+      end: end ?? this.end,
+      text: text ?? this.text,
+    );
+  }
+
+  final int index;
+  final Duration start;
+  final Duration end;
+  final dynamic text;
+
+  @override
+  String toString() {
+    return 'Subtitle(index: $index, start: $start, end: $end, text: $text)';
+  }
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other)) return true;
+
+    return other is Subtitle &&
+        other.index == index &&
+        other.start == start &&
+        other.end == end &&
+        other.text == text;
+  }
+
+  @override
+  int get hashCode {
+    return index.hashCode ^ start.hashCode ^ end.hashCode ^ text.hashCode;
+  }
+}
diff --git a/lib/src/notifiers/index.dart b/lib/src/notifiers/index.dart
new file mode 100644
index 000000000..2fd693a5f
--- /dev/null
+++ b/lib/src/notifiers/index.dart
@@ -0,0 +1 @@
+export 'player_notifier.dart';
diff --git a/lib/src/notifiers/player_notifier.dart b/lib/src/notifiers/player_notifier.dart
new file mode 100644
index 000000000..c70fa8665
--- /dev/null
+++ b/lib/src/notifiers/player_notifier.dart
@@ -0,0 +1,24 @@
+import 'package:flutter/material.dart';
+
+///
+/// The new State-Manager for Chewie!
+/// Has to be an instance of Singleton to survive
+/// over all State-Changes inside chewie
+///
+class PlayerNotifier extends ChangeNotifier {
+  PlayerNotifier._(bool hideStuff) : _hideStuff = hideStuff;
+
+  bool _hideStuff;
+
+  bool get hideStuff => _hideStuff;
+
+  set hideStuff(bool value) {
+    _hideStuff = value;
+    notifyListeners();
+  }
+
+  // ignore: prefer_constructors_over_static_methods
+  static PlayerNotifier init() {
+    return PlayerNotifier._(true);
+  }
+}
diff --git a/lib/src/player_with_controls.dart b/lib/src/player_with_controls.dart
index b8e216c59..a8d974020 100644
--- a/lib/src/player_with_controls.dart
+++ b/lib/src/player_with_controls.dart
@@ -1,20 +1,18 @@
-import 'dart:ui';
-
 import 'package:chewie/src/chewie_player.dart';
-import 'package:chewie/src/cupertino_controls.dart';
-import 'package:chewie/src/material_controls.dart';
-import 'package:flutter/foundation.dart';
+import 'package:chewie/src/helpers/adaptive_controls.dart';
+import 'package:chewie/src/notifiers/index.dart';
 import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
 import 'package:video_player/video_player.dart';
 
 class PlayerWithControls extends StatelessWidget {
-  PlayerWithControls({Key key}) : super(key: key);
+  const PlayerWithControls({super.key});
 
   @override
   Widget build(BuildContext context) {
     final ChewieController chewieController = ChewieController.of(context);
 
-    double _calculateAspectRatio(BuildContext context) {
+    double calculateAspectRatio(BuildContext context) {
       final size = MediaQuery.of(context).size;
       final width = size.width;
       final height = size.height;
@@ -22,56 +20,97 @@ class PlayerWithControls extends StatelessWidget {
       return width > height ? width / height : height / width;
     }
 
-    Widget _buildControls(
+    Widget buildControls(
       BuildContext context,
       ChewieController chewieController,
     ) {
       return chewieController.showControls
-          ? chewieController.customControls != null
-              ? chewieController.customControls
-              : Theme.of(context).platform == TargetPlatform.android
-                  ? MaterialControls()
-                  : CupertinoControls(
-                      backgroundColor: Color.fromRGBO(41, 41, 41, 0.7),
-                      iconColor: Color.fromARGB(255, 200, 200, 200),
-                    )
-          : Container();
+          ? chewieController.customControls ?? const AdaptiveControls()
+          : const SizedBox();
     }
 
-    Container _buildPlayerWithControls(
-        ChewieController chewieController, BuildContext context) {
-      return Container(
-        child: Stack(
-          children: <Widget>[
-            chewieController.placeholder ?? Container(),
-            Center(
-              child: AspectRatio(
-                aspectRatio: chewieController.aspectRatio ??
-                    chewieController.videoPlayerController.value.aspectRatio,
-                child: VideoPlayer(chewieController.videoPlayerController),
-              ),
+    Widget buildPlayerWithControls(
+      ChewieController chewieController,
+      BuildContext context,
+    ) {
+      final playerNotifier = context.read<PlayerNotifier>();
+      final child = Stack(
+        children: [
+          if (chewieController.placeholder != null)
+            chewieController.placeholder!,
+          Center(
+            child: AspectRatio(
+              aspectRatio:
+                  chewieController.aspectRatio ??
+                  chewieController.videoPlayerController.value.aspectRatio,
+              child: VideoPlayer(chewieController.videoPlayerController),
+            ),
+          ),
+          if (chewieController.overlay != null) chewieController.overlay!,
+          if (Theme.of(context).platform != TargetPlatform.iOS)
+            Consumer<PlayerNotifier>(
+              builder:
+                  (
+                    BuildContext context,
+                    PlayerNotifier notifier,
+                    Widget? widget,
+                  ) => Visibility(
+                    visible: !notifier.hideStuff,
+                    child: AnimatedOpacity(
+                      opacity: notifier.hideStuff ? 0.0 : 0.8,
+                      duration: const Duration(milliseconds: 250),
+                      child: const DecoratedBox(
+                        decoration: BoxDecoration(color: Colors.black54),
+                        child: SizedBox.expand(),
+                      ),
+                    ),
+                  ),
             ),
-            chewieController.overlay ?? Container(),
-            if (!chewieController.isFullScreen)
-              _buildControls(context, chewieController)
-            else
-              SafeArea(
-                child: _buildControls(context, chewieController),
-              ),
-          ],
-        ),
+          if (!chewieController.isFullScreen)
+            buildControls(context, chewieController)
+          else
+            SafeArea(
+              bottom: false,
+              child: buildControls(context, chewieController),
+            ),
+        ],
       );
+
+      if (chewieController.zoomAndPan ||
+          chewieController.transformationController != null) {
+        return InteractiveViewer(
+          transformationController: chewieController.transformationController,
+          maxScale: chewieController.maxScale,
+          panEnabled: chewieController.zoomAndPan,
+          scaleEnabled: chewieController.zoomAndPan,
+          onInteractionUpdate:
+              chewieController.zoomAndPan
+                  ? (_) => playerNotifier.hideStuff = true
+                  : null,
+          onInteractionEnd:
+              chewieController.zoomAndPan
+                  ? (_) => playerNotifier.hideStuff = false
+                  : null,
+          child: child,
+        );
+      }
+
+      return child;
     }
 
-    return Center(
-      child: Container(
-        height: MediaQuery.of(context).size.height,
-        width: MediaQuery.of(context).size.width,
-        child: AspectRatio(
-          aspectRatio: _calculateAspectRatio(context),
-          child: _buildPlayerWithControls(chewieController, context),
-        ),
-      ),
+    return LayoutBuilder(
+      builder: (BuildContext context, BoxConstraints constraints) {
+        return Center(
+          child: SizedBox(
+            height: constraints.maxHeight,
+            width: constraints.maxWidth,
+            child: AspectRatio(
+              aspectRatio: calculateAspectRatio(context),
+              child: buildPlayerWithControls(chewieController, context),
+            ),
+          ),
+        );
+      },
     );
   }
 }
diff --git a/lib/src/progress_bar.dart b/lib/src/progress_bar.dart
new file mode 100644
index 000000000..052060497
--- /dev/null
+++ b/lib/src/progress_bar.dart
@@ -0,0 +1,274 @@
+import 'package:chewie/chewie.dart';
+import 'package:flutter/material.dart';
+import 'package:video_player/video_player.dart';
+
+class VideoProgressBar extends StatefulWidget {
+  VideoProgressBar(
+    this.controller, {
+    ChewieProgressColors? colors,
+    this.onDragEnd,
+    this.onDragStart,
+    this.onDragUpdate,
+    this.draggableProgressBar = true,
+    super.key,
+    required this.barHeight,
+    required this.handleHeight,
+    required this.drawShadow,
+  }) : colors = colors ?? ChewieProgressColors();
+
+  final VideoPlayerController controller;
+  final ChewieProgressColors colors;
+  final Function()? onDragStart;
+  final Function()? onDragEnd;
+  final Function()? onDragUpdate;
+
+  final double barHeight;
+  final double handleHeight;
+  final bool drawShadow;
+  final bool draggableProgressBar;
+
+  @override
+  // ignore: library_private_types_in_public_api
+  _VideoProgressBarState createState() {
+    return _VideoProgressBarState();
+  }
+}
+
+class _VideoProgressBarState extends State<VideoProgressBar> {
+  void listener() {
+    if (!mounted) return;
+    setState(() {});
+  }
+
+  bool _controllerWasPlaying = false;
+
+  Offset? _latestDraggableOffset;
+
+  VideoPlayerController get controller => widget.controller;
+
+  @override
+  void initState() {
+    super.initState();
+    controller.addListener(listener);
+  }
+
+  @override
+  void deactivate() {
+    controller.removeListener(listener);
+    super.deactivate();
+  }
+
+  void _seekToRelativePosition(Offset globalPosition) {
+    controller.seekTo(
+      context.calcRelativePosition(controller.value.duration, globalPosition),
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final child = Center(
+      child: StaticProgressBar(
+        value: controller.value,
+        colors: widget.colors,
+        barHeight: widget.barHeight,
+        handleHeight: widget.handleHeight,
+        drawShadow: widget.drawShadow,
+        latestDraggableOffset: _latestDraggableOffset,
+      ),
+    );
+
+    return widget.draggableProgressBar
+        ? GestureDetector(
+          onHorizontalDragStart: (DragStartDetails details) {
+            if (!controller.value.isInitialized) {
+              return;
+            }
+            _controllerWasPlaying = controller.value.isPlaying;
+            if (_controllerWasPlaying) {
+              controller.pause();
+            }
+
+            widget.onDragStart?.call();
+          },
+          onHorizontalDragUpdate: (DragUpdateDetails details) {
+            if (!controller.value.isInitialized) {
+              return;
+            }
+            _latestDraggableOffset = details.globalPosition;
+            listener();
+
+            widget.onDragUpdate?.call();
+          },
+          onHorizontalDragEnd: (DragEndDetails details) {
+            if (_controllerWasPlaying) {
+              controller.play();
+            }
+
+            if (_latestDraggableOffset != null) {
+              _seekToRelativePosition(_latestDraggableOffset!);
+              _latestDraggableOffset = null;
+            }
+
+            widget.onDragEnd?.call();
+          },
+          onTapDown: (TapDownDetails details) {
+            if (!controller.value.isInitialized) {
+              return;
+            }
+            _seekToRelativePosition(details.globalPosition);
+          },
+          child: child,
+        )
+        : child;
+  }
+}
+
+class StaticProgressBar extends StatelessWidget {
+  const StaticProgressBar({
+    super.key,
+    required this.value,
+    required this.colors,
+    required this.barHeight,
+    required this.handleHeight,
+    required this.drawShadow,
+    this.latestDraggableOffset,
+  });
+
+  final Offset? latestDraggableOffset;
+  final VideoPlayerValue value;
+  final ChewieProgressColors colors;
+
+  final double barHeight;
+  final double handleHeight;
+  final bool drawShadow;
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      height: MediaQuery.of(context).size.height,
+      width: MediaQuery.of(context).size.width,
+      color: Colors.transparent,
+      child: CustomPaint(
+        painter: _ProgressBarPainter(
+          value: value,
+          draggableValue:
+              latestDraggableOffset != null
+                  ? context.calcRelativePosition(
+                    value.duration,
+                    latestDraggableOffset!,
+                  )
+                  : null,
+          colors: colors,
+          barHeight: barHeight,
+          handleHeight: handleHeight,
+          drawShadow: drawShadow,
+        ),
+      ),
+    );
+  }
+}
+
+class _ProgressBarPainter extends CustomPainter {
+  _ProgressBarPainter({
+    required this.value,
+    required this.colors,
+    required this.barHeight,
+    required this.handleHeight,
+    required this.drawShadow,
+    required this.draggableValue,
+  });
+
+  VideoPlayerValue value;
+  ChewieProgressColors colors;
+
+  final double barHeight;
+  final double handleHeight;
+  final bool drawShadow;
+
+  /// The value of the draggable progress bar.
+  /// If null, the progress bar is not being dragged.
+  final Duration? draggableValue;
+
+  @override
+  bool shouldRepaint(CustomPainter painter) {
+    return true;
+  }
+
+  @override
+  void paint(Canvas canvas, Size size) {
+    final baseOffset = size.height / 2 - barHeight / 2;
+
+    canvas.drawRRect(
+      RRect.fromRectAndRadius(
+        Rect.fromPoints(
+          Offset(0.0, baseOffset),
+          Offset(size.width, baseOffset + barHeight),
+        ),
+        const Radius.circular(4.0),
+      ),
+      colors.backgroundPaint,
+    );
+    if (!value.isInitialized) {
+      return;
+    }
+    final double playedPartPercent =
+        (draggableValue != null
+            ? draggableValue!.inMilliseconds
+            : value.position.inMilliseconds) /
+        value.duration.inMilliseconds;
+    final double playedPart =
+        playedPartPercent > 1 ? size.width : playedPartPercent * size.width;
+    for (final DurationRange range in value.buffered) {
+      final double start = range.startFraction(value.duration) * size.width;
+      final double end = range.endFraction(value.duration) * size.width;
+      canvas.drawRRect(
+        RRect.fromRectAndRadius(
+          Rect.fromPoints(
+            Offset(start, baseOffset),
+            Offset(end, baseOffset + barHeight),
+          ),
+          const Radius.circular(4.0),
+        ),
+        colors.bufferedPaint,
+      );
+    }
+    canvas.drawRRect(
+      RRect.fromRectAndRadius(
+        Rect.fromPoints(
+          Offset(0.0, baseOffset),
+          Offset(playedPart, baseOffset + barHeight),
+        ),
+        const Radius.circular(4.0),
+      ),
+      colors.playedPaint,
+    );
+
+    if (drawShadow) {
+      final Path shadowPath =
+          Path()..addOval(
+            Rect.fromCircle(
+              center: Offset(playedPart, baseOffset + barHeight / 2),
+              radius: handleHeight,
+            ),
+          );
+
+      canvas.drawShadow(shadowPath, Colors.black, 0.2, false);
+    }
+
+    canvas.drawCircle(
+      Offset(playedPart, baseOffset + barHeight / 2),
+      handleHeight,
+      colors.handlePaint,
+    );
+  }
+}
+
+extension RelativePositionExtensions on BuildContext {
+  Duration calcRelativePosition(Duration videoDuration, Offset globalPosition) {
+    final box = findRenderObject()! as RenderBox;
+    final Offset tapPos = box.globalToLocal(globalPosition);
+    final double relative = (tapPos.dx / box.size.width).clamp(0, 1);
+    final Duration position = videoDuration * relative;
+    return position;
+  }
+}
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
deleted file mode 100644
index f1b216fa8..000000000
--- a/lib/src/utils.dart
+++ /dev/null
@@ -1,22 +0,0 @@
-String formatDuration(Duration position) {
-  final ms = position.inMilliseconds;
-
-  int seconds = ms ~/ 1000;
-  final int hours = seconds ~/ 3600;
-  seconds = seconds % 3600;
-  var minutes = seconds ~/ 60;
-  seconds = seconds % 60;
-
-  final hoursString = hours >= 10 ? '$hours' : hours == 0 ? '00' : '0$hours';
-
-  final minutesString =
-      minutes >= 10 ? '$minutes' : minutes == 0 ? '00' : '0$minutes';
-
-  final secondsString =
-      seconds >= 10 ? '$seconds' : seconds == 0 ? '00' : '0$seconds';
-
-  final formattedTime =
-      '${hoursString == '00' ? '' : hoursString + ':'}$minutesString:$secondsString';
-
-  return formattedTime;
-}
diff --git a/pubspec.yaml b/pubspec.yaml
index 929e43657..8a303711f 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,23 +1,24 @@
 name: chewie
 description: A video player for Flutter with Cupertino and Material play controls
-version: 0.10.4
-homepage: https://github.com/brianegan/chewie
+version: 1.12.1
+homepage: https://github.com/fluttercommunity/chewie
 
 environment:
-  sdk: ">=2.7.0 <3.0.0"
-  flutter: ">=1.20.0 <2.0.0"
+  sdk: '>=3.7.0 <4.0.0'
+  flutter: ">=3.29.0"
 
 dependencies:
-  video_player: ">=0.10.12+5 <2.0.0"
-  cupertino_icons: ">=1.0.0 <2.0.0"
-  wakelock: ">=0.1.2 <1.0.0"
-
+  cupertino_icons: ^1.0.8
   flutter:
     sdk: flutter
+  provider: ^6.1.5
+  video_player: ^2.10.0
+  wakelock_plus: ^1.3.2
 
 dev_dependencies:
   flutter_test:
     sdk: flutter
+  flutter_lints: ^5.0.0
 
 flutter:
   uses-material-design: true
diff --git a/test/uninitialized_controls_state_test.dart b/test/uninitialized_controls_state_test.dart
new file mode 100644
index 000000000..0db7a83b7
--- /dev/null
+++ b/test/uninitialized_controls_state_test.dart
@@ -0,0 +1,94 @@
+import 'package:chewie/chewie.dart';
+import 'package:chewie/src/center_play_button.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:video_player/video_player.dart';
+
+List<String> srcs = [
+  "https://assets.mixkit.co/videos/preview/mixkit-spinning-around-the-earth-29351-large.mp4",
+  "https://assets.mixkit.co/videos/preview/mixkit-daytime-city-traffic-aerial-view-56-large.mp4",
+  "https://assets.mixkit.co/videos/preview/mixkit-a-girl-blowing-a-bubble-gum-at-an-amusement-park-1226-large.mp4",
+];
+
+void main() {
+  testWidgets("MaterialControls state test", (WidgetTester tester) async {
+    // Build our app and trigger a frame.
+    var videoPlayerController = VideoPlayerController.networkUrl(
+      Uri.parse(srcs[0]),
+    );
+    var materialControlsKey = GlobalKey();
+    var chewieController = ChewieController(
+      videoPlayerController: videoPlayerController,
+      autoPlay: false,
+      looping: false,
+      customControls: MaterialControls(key: materialControlsKey),
+    );
+    await tester.pumpWidget(
+      MaterialApp(home: Scaffold(body: Chewie(controller: chewieController))),
+    );
+
+    await tester.pump();
+
+    var playButton = find.byType(CenterPlayButton);
+    expect(playButton, findsOneWidget);
+    var btn = playButton.first;
+    var playButtonWidget = tester.widget<CenterPlayButton>(btn);
+    expect(playButtonWidget.isFinished, false);
+  });
+
+  testWidgets("CupertinoControls state test", (WidgetTester tester) async {
+    // Build our app and trigger a frame.
+    var videoPlayerController = VideoPlayerController.networkUrl(
+      Uri.parse(srcs[0]),
+    );
+    var materialControlsKey = GlobalKey();
+    var chewieController = ChewieController(
+      videoPlayerController: videoPlayerController,
+      autoPlay: false,
+      looping: false,
+      customControls: CupertinoControls(
+        key: materialControlsKey,
+        backgroundColor: Colors.black,
+        iconColor: Colors.white,
+      ),
+    );
+    await tester.pumpWidget(
+      MaterialApp(home: Scaffold(body: Chewie(controller: chewieController))),
+    );
+
+    await tester.pump();
+
+    var playButton = find.byType(CenterPlayButton);
+    expect(playButton, findsOneWidget);
+    var btn = playButton.first;
+    var playButtonWidget = tester.widget<CenterPlayButton>(btn);
+    expect(playButtonWidget.isFinished, false);
+  });
+
+  testWidgets("MaterialDesktopControls state test", (
+    WidgetTester tester,
+  ) async {
+    // Build our app and trigger a frame.
+    var videoPlayerController = VideoPlayerController.networkUrl(
+      Uri.parse(srcs[0]),
+    );
+    var materialControlsKey = GlobalKey();
+    var chewieController = ChewieController(
+      videoPlayerController: videoPlayerController,
+      autoPlay: false,
+      looping: false,
+      customControls: MaterialDesktopControls(key: materialControlsKey),
+    );
+    await tester.pumpWidget(
+      MaterialApp(home: Scaffold(body: Chewie(controller: chewieController))),
+    );
+
+    await tester.pump();
+
+    var playButton = find.byType(CenterPlayButton);
+    expect(playButton, findsOneWidget);
+    var btn = playButton.first;
+    var playButtonWidget = tester.widget<CenterPlayButton>(btn);
+    expect(playButtonWidget.isFinished, false);
+  });
+}