diff --git a/.github/workflows/PR-open-test-build.yaml b/.github/workflows/PR-open-test-build.yaml index 921d9ce..9ae8a15 100644 --- a/.github/workflows/PR-open-test-build.yaml +++ b/.github/workflows/PR-open-test-build.yaml @@ -17,9 +17,9 @@ jobs: channel: 'stable' - name: Get Pub Dependencies run: flutter pub get - - name: Run build runner for codegen files + - name: Run Build Runner For Codegen Files run: flutter packages pub run build_runner build --delete-conflicting-outputs - name: Run Dart Analyzer run: flutter analyze . - - name: Attempt release build generation + - name: Attempt Debug APK Build run: flutter build apk --debug \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index 10036db..aa9c8e2 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -42,7 +42,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.inceptrafay.ez_ticketz_app" - minSdkVersion 16 + minSdkVersion 17 targetSdkVersion 30 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/lib/views/screens/trailer_screen.dart b/lib/views/screens/trailer_screen.dart index 5c2098c..51c4984 100644 --- a/lib/views/screens/trailer_screen.dart +++ b/lib/views/screens/trailer_screen.dart @@ -1,4 +1,3 @@ -import 'package:auto_route/auto_route.dart'; import 'package:better_player/better_player.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -6,10 +5,15 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; // Helpers import '../../helper/utils/constants.dart'; -import '../../helper/extensions/context_extensions.dart'; //Providers import '../../providers/movies_provider.dart'; +import '../widgets/trailer/overlay_back_button.dart'; + +//Widgets +import '../widgets/trailer/overlay_black_header.dart'; +import '../widgets/trailer/overlay_movie_title.dart'; +import '../widgets/trailer/overlay_play_pause_button.dart'; class TrailerScreen extends StatefulWidget { const TrailerScreen(); @@ -21,18 +25,26 @@ class TrailerScreen extends StatefulWidget { class _TrailerScreenState extends State { late final BetterPlayerController _betterPlayerController; - final _controlsConfiguration = const BetterPlayerControlsConfiguration( - overflowModalTextColor: Constants.textGreyColor, - overflowMenuIconsColor: Constants.textGreyColor, - overflowModalColor: Constants.scaffoldGreyColor, - progressBarPlayedColor: Constants.primaryColor, - progressBarHandleColor: Constants.primaryColor, - backgroundColor: Colors.black38, - controlBarColor: Colors.black54, - enablePip: false, - enableSubtitles: false, + static const _controlsConfiguration = BetterPlayerControlsConfiguration( + overflowModalTextColor: Constants.textGreyColor, + overflowMenuIconsColor: Constants.textGreyColor, + overflowModalColor: Constants.scaffoldGreyColor, + progressBarPlayedColor: Constants.primaryColor, + progressBarHandleColor: Constants.primaryColor, + progressBarBufferedColor: Color(0x72ed0000), + backgroundColor: Colors.black38, + controlBarColor: Colors.black54, + loadingColor: Constants.redColor, + enablePip: false, + enableSubtitles: false, + controlBarHeight: 60, ); + static const _exitFullScreenOrientations = [ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + ]; + @override void initState() { super.initState(); @@ -41,16 +53,14 @@ class _TrailerScreenState extends State { fullScreenAspectRatio: 16 / 9, looping: false, autoPlay: true, - fit: BoxFit.cover, - allowedScreenSleep: false, - deviceOrientationsAfterFullScreen: [ - DeviceOrientation.portraitUp, - DeviceOrientation.portraitDown, - ], - systemOverlaysAfterFullScreen: [SystemUiOverlay.top], fullScreenByDefault: true, + allowedScreenSleep: false, autoDetectFullscreenDeviceOrientation: true, + fit: BoxFit.cover, controlsConfiguration: _controlsConfiguration, + deviceOrientationsAfterFullScreen: _exitFullScreenOrientations, + systemOverlaysAfterFullScreen: [SystemUiOverlay.top], + routePageBuilder: _fullScreenRouteBuilder, errorBuilder: _buildErrorWidget, ); final trailerUrl = context.read(selectedMovieProvider).state.trailerUrl; @@ -64,44 +74,100 @@ class _TrailerScreenState extends State { ); } + /// List of overlay widgets for the player that add useful functionalities, + /// like Back Navigation, Play/Pause ability or Movie Title. + List buildOverlayWidgets() { + return [ + //Black overlay header + Align( + alignment: Alignment.topCenter, + child: OverlayBlackHeader( + betterPlayerController: _betterPlayerController, + ), + ), + + //Movie title + Align( + alignment: Alignment.topCenter, + child: Padding( + padding: const EdgeInsets.only(top: 20), + child: OverlayMovieTitle( + betterPlayerController: _betterPlayerController, + ), + ), + ), + + //Back button + Align( + alignment: Alignment.topLeft, + child: OverlayBackButton( + betterPlayerController: _betterPlayerController, + ), + ), + + //Play/Pause Button + Align( + alignment: _betterPlayerController.isFullScreen + ? Alignment.center + : Alignment.topCenter, + child: Padding( + padding: _betterPlayerController.isFullScreen + ? const EdgeInsets.all(0) + : const EdgeInsets.only(top: 105), + child: OverlayPlayPauseButton( + betterPlayerController: _betterPlayerController, + ), + ), + ), + ]; + } + + /// Defines the builder for the new route that gets pushed on + /// the fullscreen mode. + Widget _fullScreenRouteBuilder( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + BetterPlayerControllerProvider provider, + ) { + return Scaffold( + resizeToAvoidBottomInset: false, + body: AnimatedBuilder( + animation: animation, + builder: (ctx, _) => Stack( + children: [ + //Video Player + Container( + alignment: Alignment.center, + child: provider, + ), + + //Overlay widgets + ...buildOverlayWidgets() + ], + ), + ), + ); + } + @override Widget build(BuildContext context) { return Scaffold( + resizeToAvoidBottomInset: false, + backgroundColor: Colors.black, body: SafeArea( - child: Column( + child: Stack( children: [ - const SizedBox(height: 20), - - //Back and title - Row( - children: [ - const SizedBox(width: 15), - GestureDetector( - child: const Icon(Icons.arrow_back_sharp, size: 26), - onTap: () { - context.router.pop(); - }, - ), - - const SizedBox(width: 20), - - //Movie Title - Expanded( - child: Consumer( - builder: (_, watch, __) { - final title = watch(selectedMovieProvider).state.title; - return Text( - title, - maxLines: 1, - style: context.headline3.copyWith(fontSize: 22), - ); - }, - ), - ), - ], + //Video Player + Positioned( + top: 5, + right: 0, + left: 0, + child: BetterPlayer(controller: _betterPlayerController), ), - Expanded(child: BetterPlayer(controller: _betterPlayerController)), + //Overlay widgets + ...buildOverlayWidgets(), ], ), ), diff --git a/lib/views/widgets/trailer/overlay_back_button.dart b/lib/views/widgets/trailer/overlay_back_button.dart new file mode 100644 index 0000000..2bed7b4 --- /dev/null +++ b/lib/views/widgets/trailer/overlay_back_button.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:better_player/better_player.dart'; +import 'package:auto_route/auto_route.dart'; + +class OverlayBackButton extends StatefulWidget { + final BetterPlayerController betterPlayerController; + + const OverlayBackButton({ + Key? key, + required this.betterPlayerController, + }) : super(key: key); + + @override + _OverlayBackButtonState createState() => _OverlayBackButtonState(); +} + +class _OverlayBackButtonState extends State { + bool _isVisible = false; + + BetterPlayerController get _betterPlayerController => widget.betterPlayerController; + + @override + void initState() { + super.initState(); + _betterPlayerController.addEventsListener(_handlePlayerEventChanges); + } + + /// Listens to all events sent by [_betterPlayerController] + /// and handles them with the appropriate response. + void _handlePlayerEventChanges(BetterPlayerEvent event) { + final eventType = event.betterPlayerEventType; + //handle events if initialized + final controlsVisible = eventType == BetterPlayerEventType.controlsVisible; + final controlsHidden = eventType == BetterPlayerEventType.controlsHidden; + if (controlsVisible || controlsHidden) { //if overlay controls toggled + setState(() { + _isVisible = controlsVisible; + }); + } + } + + @override + Widget build(BuildContext context) { + if (!_isVisible) { + return const SizedBox.shrink(); + } + return InkWell( + onTap: () => context.router.pop(), + child: const Padding( + padding: EdgeInsets.all(15), + child: Icon( + Icons.arrow_back_sharp, + color: Colors.white, + size: 28, + ), + ), + ); + } + + @override + void dispose() { + _betterPlayerController.removeEventsListener(_handlePlayerEventChanges); + super.dispose(); + } +} \ No newline at end of file diff --git a/lib/views/widgets/trailer/overlay_black_header.dart b/lib/views/widgets/trailer/overlay_black_header.dart new file mode 100644 index 0000000..56d159e --- /dev/null +++ b/lib/views/widgets/trailer/overlay_black_header.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:better_player/better_player.dart'; + +class OverlayBlackHeader extends StatefulWidget { + final BetterPlayerController betterPlayerController; + + const OverlayBlackHeader({ + Key? key, + required this.betterPlayerController, + }) : super(key: key); + + @override + _OverlayBlackHeaderState createState() => _OverlayBlackHeaderState(); +} + +class _OverlayBlackHeaderState extends State { + bool _isVisible = false; + + BetterPlayerController get _betterPlayerController => widget.betterPlayerController; + + @override + void initState() { + super.initState(); + _betterPlayerController.addEventsListener(_handlePlayerEventChanges); + } + + /// Listens to all events sent by [_betterPlayerController] + /// and handles them with the appropriate response. + void _handlePlayerEventChanges(BetterPlayerEvent event) { + final eventType = event.betterPlayerEventType; + //handle events if initialized + final controlsVisible = eventType == BetterPlayerEventType.controlsVisible; + final controlsHidden = eventType == BetterPlayerEventType.controlsHidden; + if (controlsVisible || controlsHidden) { //if overlay controls toggled + setState(() { + _isVisible = controlsVisible; + }); + } + } + + @override + Widget build(BuildContext context) { + if (!_isVisible) { + return const SizedBox.shrink(); + } + return const IgnorePointer( + child: SizedBox( + width: double.infinity, + height: 55, + child: ColoredBox( + color: Colors.black54, + ), + ), + ); + } + + @override + void dispose() { + _betterPlayerController.removeEventsListener(_handlePlayerEventChanges); + super.dispose(); + } +} \ No newline at end of file diff --git a/lib/views/widgets/trailer/overlay_movie_title.dart b/lib/views/widgets/trailer/overlay_movie_title.dart new file mode 100644 index 0000000..167474e --- /dev/null +++ b/lib/views/widgets/trailer/overlay_movie_title.dart @@ -0,0 +1,69 @@ +import 'package:better_player/better_player.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +//Helpers +import '../../../helper/extensions/context_extensions.dart'; + +//Providers +import '../../../providers/movies_provider.dart'; + +class OverlayMovieTitle extends StatefulHookWidget { + final BetterPlayerController betterPlayerController; + + const OverlayMovieTitle({ + Key? key, + required this.betterPlayerController, + }) : super(key: key); + + @override + _OverlayMovieTitleState createState() => _OverlayMovieTitleState(); +} + +class _OverlayMovieTitleState extends State { + bool _isVisible = false; + + BetterPlayerController get _betterPlayerController => widget.betterPlayerController; + + @override + void initState() { + super.initState(); + _betterPlayerController.addEventsListener(_handlePlayerEventChanges); + } + + /// Listens to all events sent by [_betterPlayerController] + /// and handles them with the appropriate response. + void _handlePlayerEventChanges(BetterPlayerEvent event) { + final eventType = event.betterPlayerEventType; + //handle events if initialized + final controlsVisible = eventType == BetterPlayerEventType.controlsVisible; + final controlsHidden = eventType == BetterPlayerEventType.controlsHidden; + if (controlsVisible || controlsHidden) { //if overlay controls toggled + setState(() { + _isVisible = controlsVisible; + }); + } + } + + @override + Widget build(BuildContext context) { + if (!_isVisible) { + return const SizedBox.shrink(); + } + final title = useProvider(selectedMovieProvider.select( + (value) => value.state.title, + )); + return Text( + title, + maxLines: 1, + style: context.headline3.copyWith(fontSize: 22), + ); + } + + @override + void dispose() { + _betterPlayerController.removeEventsListener(_handlePlayerEventChanges); + super.dispose(); + } +} diff --git a/lib/views/widgets/trailer/overlay_play_pause_button.dart b/lib/views/widgets/trailer/overlay_play_pause_button.dart new file mode 100644 index 0000000..e8bafba --- /dev/null +++ b/lib/views/widgets/trailer/overlay_play_pause_button.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; +import 'package:better_player/better_player.dart'; + +class OverlayPlayPauseButton extends StatefulWidget { + final BetterPlayerController betterPlayerController; + + const OverlayPlayPauseButton({ + Key? key, + required this.betterPlayerController, + }) : super(key: key); + + @override + _OverlayPlayPauseButtonState createState() => _OverlayPlayPauseButtonState(); +} + +class _OverlayPlayPauseButtonState extends State + with SingleTickerProviderStateMixin { + late bool _initialized; + bool _isVisible = false; + late final AnimationController _animController; + + BetterPlayerController get _betterPlayerController => widget.betterPlayerController; + + @override + void initState() { + super.initState(); + /// Hide visibility if buffering or uninitialized + _initialized = _betterPlayerController.isPlaying() ?? false; + _betterPlayerController.addEventsListener(_handlePlayerEventChanges); + _animController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 250), + ); + } + + /// Checks and sets the initialization flag to true when the + /// buffering is complete. + /// + /// All [BetterPlayerEventType] events are ignored until [_initialized] + /// is set to true. + void checkInitialization() { + final bufferingComplete = _betterPlayerController.isPlaying()!; + if (bufferingComplete) { + setState(() { + _initialized = bufferingComplete; + }); + } + } + + /// Listens to all events sent by [_betterPlayerController] + /// and handles them with the appropriate response. + void _handlePlayerEventChanges(BetterPlayerEvent event) { + final eventType = event.betterPlayerEventType; + if (!_initialized) { + //if not initialized + checkInitialization(); + } else if (eventType == BetterPlayerEventType.finished) { + setState(() { + _isVisible = false; + _initialized = false; + }); + } else { + //handle events if initialized + final controlsVisible = + eventType == BetterPlayerEventType.controlsVisible; + final controlsHidden = eventType == BetterPlayerEventType.controlsHidden; + if (controlsVisible || controlsHidden) { + //if overlay controls toggled + setState(() { + _isVisible = controlsVisible; + }); + } else if (_isVisible) { + //if other events, check control visibility + _handlePlayPauseEvent(eventType); + } + } + } + + /// Animates the overlay play icon to pause and vice versa. + /// + /// Called when the play/pause event is dispatched, that is, after the + /// [_handlePlayPauseTap] is called. + void _handlePlayPauseEvent(BetterPlayerEventType eventType) { + final isPlay = eventType == BetterPlayerEventType.play; + final isPause = eventType == BetterPlayerEventType.pause; + if (isPlay) { + _animController.reverse(); + } else if (isPause) { + _animController.forward(); + } + } + + /// Pauses or resumes the video using [_betterPlayerController]. + /// + /// Called when the play/pause overlay button is pressed. + /// + /// The controller's methods send a pause/play event to the + /// listener [_handlePlayerEventChanges]. + void _handlePlayPauseTap() { + if (_betterPlayerController.isPlaying()!) { + _betterPlayerController.pause(); + } else { + _betterPlayerController.play(); + } + } + + @override + Widget build(BuildContext context) { + if (!_isVisible) { + return const SizedBox.shrink(); + } + return InkWell( + onTap: _handlePlayPauseTap, + child: Container( + padding: const EdgeInsets.all(15), + decoration: const BoxDecoration( + color: Colors.black54, + shape: BoxShape.circle, + ), + child: AnimatedIcon( + icon: AnimatedIcons.pause_play, + color: Colors.white, + size: 32, + progress: _animController, + ), + ), + ); + } + + @override + void dispose() { + _betterPlayerController.removeEventsListener(_handlePlayerEventChanges); + _animController.dispose(); + super.dispose(); + } +} \ No newline at end of file