diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 0000000..8cec717 --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,27 @@ +name: Feed SDK test on every PR + +on: + pull_request: + branches-ignore: + - master + +jobs: + run-flutter-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: subosito/flutter-action@v2 + with: + flutter-version: ${{ env.FLUTTER_VERSION }} + - run: flutter pub get + + - name: Lint + run: flutter analyze > lint-results.txt + + - name: Upload the lint results as an artifact + if: always() + uses: actions/upload-artifact@v2 + with: + name: lint-results + path: lint-results.txt \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..3ab308e --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,67 @@ +name: Create Tag and Release on Version Change + +on: + push: + branches: + - master + +permissions: write-all + +jobs: + create_tag: + name: Create Git Tag + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Check for version changes + run: | + # Fetch all tags from the remote repository + git fetch --tags + + # Get the previous version from the last release tag + export previous_version=$(git describe --tags --abbrev=0) + + # Get the current version from pubspec.yaml + export current_version=$(cat pubspec.yaml | grep 'version:' | awk '{print $2}') + + if [[ "$previous_version" != "v$current_version" ]]; then + echo "Version has changed from $previous_version to v$current_version." + else + echo "Version has not changed." + exit 1 + fi + + - name: Push Git Tag + run: | + # Git login + git config --global user.name "$(git log -n 1 --pretty=format:%an)" + git config --global user.email "$(git log -n 1 --pretty=format:%ae)" + + # Push a Git tag with the new version + export current_version=$(cat pubspec.yaml | grep 'version:' | awk '{print $2}') + git tag -a "v$current_version" -m "Version $current_version" + git push origin "v$current_version" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + create-github-release: + name: Create GitHub Release + runs-on: ubuntu-latest + needs: create_tag + permissions: + contents: write + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Create Release + run: gh release create "$(git describe --tags --abbrev=0)" --generate-notes + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/analysis_options.yaml b/analysis_options.yaml index a5744c1..ea22b62 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -2,3 +2,30 @@ include: package:flutter_lints/flutter.yaml # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # Style rules + - camel_case_types + - library_names + - avoid_catches_without_on_clauses + - avoid_catching_errors + - avoid_empty_else + - unnecessary_brace_in_string_interps + - avoid_redundant_argument_values + - leading_newlines_in_multiline_strings + # formatting + - lines_longer_than_80_chars + - curly_braces_in_flow_control_structures + # doc comments + - slash_for_doc_comments \ No newline at end of file diff --git a/example/pubspec.yaml b/example/pubspec.yaml index d86269f..de993e7 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.3.5 +version: 1.3.6 environment: sdk: '>=3.0.0 <4.0.0' diff --git a/lib/src/widgets/media/carousel.dart b/lib/src/widgets/media/carousel.dart index 03e848a..e123f87 100644 --- a/lib/src/widgets/media/carousel.dart +++ b/lib/src/widgets/media/carousel.dart @@ -104,7 +104,6 @@ class _LMCarouselState extends State { @override Widget build(BuildContext context) { mapAttachmentsToWidget(); - final size = MediaQuery.of(context).size.width; return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(widget.borderRadius ?? 0), diff --git a/lib/src/widgets/media/video.dart b/lib/src/widgets/media/video.dart index 6a70970..ff5651c 100644 --- a/lib/src/widgets/media/video.dart +++ b/lib/src/widgets/media/video.dart @@ -3,16 +3,20 @@ import 'dart:io'; // import 'package:flick_video_player/flick_video_player.dart'; import 'package:flutter/material.dart'; +import 'package:likeminds_feed_ui_fl/likeminds_feed_ui_fl.dart'; +import 'package:likeminds_feed_ui_fl/src/utils/theme.dart'; import 'package:likeminds_feed_ui_fl/src/widgets/common/buttons/icon_button.dart'; import 'package:likeminds_feed_ui_fl/src/widgets/common/shimmer/post_shimmer.dart'; import 'package:media_kit/media_kit.dart'; import 'package:media_kit_video/media_kit_video.dart'; -import 'package:video_player/video_player.dart'; import 'package:visibility_detector/visibility_detector.dart'; import 'package:media_kit_video/media_kit_video_controls/media_kit_video_controls.dart' as media_kit_video_controls; +import 'package:visibility_aware_state/visibility_aware_state.dart'; class LMVideo extends StatefulWidget { + // late final LMVideo? _instance; + const LMVideo({ super.key, this.videoUrl, @@ -22,11 +26,11 @@ class LMVideo extends StatefulWidget { this.aspectRatio, this.borderRadius, this.borderColor, + this.borderWidth, this.loaderWidget, this.errorWidget, this.shimmerWidget, this.boxFit, - this.videoPlayerController, this.playButton, this.pauseButton, this.muteButton, @@ -35,27 +39,38 @@ class LMVideo extends StatefulWidget { this.looping, this.allowFullScreen, this.allowMuting, + this.isMute, + this.progressTextStyle, + this.seekBarBufferColor, + this.seekBarColor, }) : assert(videoUrl != null || videoFile != null); + //Video asset variables final String? videoUrl; final File? videoFile; + // Video structure variables final double? height; final double? width; final double? aspectRatio; // defaults to 16/9 final double? borderRadius; // defaults to 0 final Color? borderColor; + final double? borderWidth; + final BoxFit? boxFit; // defaults to BoxFit.cover + // Video styling variables + final Color? seekBarColor; + final Color? seekBarBufferColor; + final TextStyle? progressTextStyle; final Widget? loaderWidget; final Widget? errorWidget; final Widget? shimmerWidget; - - final BoxFit? boxFit; // defaults to BoxFit.cover - - final VideoPlayerController? videoPlayerController; final LMIconButton? playButton; final LMIconButton? pauseButton; final LMIconButton? muteButton; + + // Video functionality control variables + final bool? isMute; final bool? showControls; final bool? autoPlay; final bool? looping; @@ -66,180 +81,243 @@ class LMVideo extends StatefulWidget { State createState() => _LMVideoState(); } -class _LMVideoState extends State { - late VideoPlayerController videoPlayerController; - // FlickManager? flickManager; +class _LMVideoState extends VisibilityAwareState { ValueNotifier rebuildOverlay = ValueNotifier(false); bool _onTouch = true; bool initialiseOverlay = false; + ValueNotifier isMuted = ValueNotifier(false); + Future? initialiseController; + ValueNotifier rebuildVideo = ValueNotifier(false); - late final player = Player(configuration: const PlayerConfiguration()); - late final controller = VideoController(player); + Player player = Player(); + VideoController? controller; Timer? _timer; @override - void dispose() { + void dispose() async { + print("Disposing video"); _timer?.cancel(); + player.dispose(); super.dispose(); } + @override + void didUpdateWidget(LMVideo oldWidget) { + super.didUpdateWidget(oldWidget); + initialiseController = initialiseControllers(); + } + + @override + void onVisibilityChanged(WidgetVisibility visibility) { + // TODO: Use visibility + if (visibility == WidgetVisibility.INVISIBLE) { + controller?.player.pause(); + } else if (visibility == WidgetVisibility.GONE) { + controller?.player.pause(); + } + super.onVisibilityChanged(visibility); + } + @override void initState() { - MediaKit.ensureInitialized(); - player.open(Media(widget.videoUrl!)); super.initState(); + initialiseController = initialiseControllers(); } Future initialiseControllers() async { + player = Player( + configuration: PlayerConfiguration( + bufferSize: 24 * 1024 * 1024, + ready: () { + if (widget.isMute != null && widget.isMute!) player.setVolume(0); + }, + ), + ); + controller = VideoController( + player, + configuration: const VideoControllerConfiguration( + enableHardwareAcceleration: true, + scale: 0.2, + ), + ); if (widget.videoUrl != null) { - videoPlayerController = widget.videoPlayerController ?? - VideoPlayerController.networkUrl( - Uri.parse(widget.videoUrl!), - videoPlayerOptions: VideoPlayerOptions( - allowBackgroundPlayback: false, - ), - ); + await player.open( + Media(widget.videoUrl!), + play: widget.autoPlay ?? false, + ); } else { - videoPlayerController = widget.videoPlayerController ?? - VideoPlayerController.file( - widget.videoFile!, - videoPlayerOptions: VideoPlayerOptions( - allowBackgroundPlayback: false, - ), - ); + await player.open( + Media(widget.videoFile!.uri.toString()), + play: widget.autoPlay ?? false, + ); } - // flickManager ??= FlickManager( - // videoPlayerController: videoPlayerController, - // autoPlay: true, - // autoInitialize: true, - // ); - - // if (!flickManager! - // .flickVideoManager!.videoPlayerController!.value.isInitialized) { - // await flickManager!.flickVideoManager!.videoPlayerController! - // .initialize(); - // } } @override Widget build(BuildContext context) { - // final screenSize = MediaQuery.of(context).size; - return FutureBuilder( - future: initialiseControllers(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const LMPostMediaShimmer(); - } else if (snapshot.connectionState == ConnectionState.done) { - if (!initialiseOverlay) { - _timer = Timer.periodic(const Duration(milliseconds: 2500), (_) { - initialiseOverlay = true; - _onTouch = false; - rebuildOverlay.value = !rebuildOverlay.value; - }); - } - return Stack(children: [ - VisibilityDetector( - key: Key('post_video_${widget.videoUrl ?? widget.videoFile}'), - onVisibilityChanged: (visibilityInfo) async { - var visiblePercentage = visibilityInfo.visibleFraction * 100; - if (visiblePercentage <= 50) {} - if (visiblePercentage > 50) { - // if (!videoPlayerController.value.isInitialized) { - // await flickManager! - // .flickVideoManager!.videoPlayerController! - // .initialize(); - // } - // flickManager!.flickControlManager!.play(); - rebuildOverlay.value = !rebuildOverlay.value; + final screenSize = MediaQuery.of(context).size; + return ValueListenableBuilder( + valueListenable: rebuildVideo, + builder: (context, _, __) { + return FutureBuilder( + future: initialiseController, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const LMPostMediaShimmer(); + } else if (snapshot.connectionState == ConnectionState.done) { + if (!initialiseOverlay) { + _timer = + Timer.periodic(const Duration(milliseconds: 3000), (_) { + initialiseOverlay = true; + _onTouch = false; + rebuildOverlay.value = !rebuildOverlay.value; + }); } - }, - child: Container( - // width: widget.width ?? screenSize.width, - // height: widget.height ?? screenSize.width, - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(widget.borderRadius ?? 0), - border: Border.all( - color: widget.borderColor ?? Colors.transparent, - width: 0, - ), - ), - alignment: Alignment.center, - // child: FlickVideoPlayer( - // flickManager: flickManager!, - // flickVideoWithControls: - // widget.showControls != null && widget.showControls! - // ? FlickVideoWithControls( - // aspectRatioWhenLoading: widget.aspectRatio ?? 1, - // controls: const FlickPortraitControls(), - // videoFit: widget.boxFit ?? BoxFit.cover, - // ) - // : FlickVideoWithControls( - // aspectRatioWhenLoading: widget.aspectRatio ?? 1, - // controls: const SizedBox(), - // videoFit: widget.boxFit ?? BoxFit.cover, - // ), - child: Video( - controller: controller, - controls: widget.showControls != null && widget.showControls! - ? null - : media_kit_video_controls.NoVideoControls, - ), - // ), - ), - ), - Positioned( - top: 0, - bottom: 0, - left: 0, - right: 0, - child: ValueListenableBuilder( - valueListenable: rebuildOverlay, - builder: (context, _, __) { - return Visibility( - visible: _onTouch, - child: Container( - alignment: Alignment.center, - child: TextButton( - style: ButtonStyle( - shape: MaterialStateProperty.all(const CircleBorder( - side: BorderSide(color: Colors.white))), - ), - child: Icon( - controller.player.state.playing - ? Icons.pause - : Icons.play_arrow, - size: 30, - color: Colors.white, + return Stack(children: [ + VisibilityDetector( + key: ObjectKey(player), + //Key('post_video_${widget.videoUrl ?? widget.videoFile}'), + onVisibilityChanged: (visibilityInfo) async { + var visiblePercentage = + visibilityInfo.visibleFraction * 100; + if (visiblePercentage < 100) { + controller?.player.pause(); + } + if (visiblePercentage == 100) { + controller?.player.play(); + rebuildOverlay.value = !rebuildOverlay.value; + } + }, + child: Container( + width: widget.width ?? screenSize.width, + height: widget.height, + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(widget.borderRadius ?? 0), + border: Border.all( + color: widget.borderColor ?? Colors.transparent, + width: 0, + ), + ), + alignment: Alignment.center, + child: MaterialVideoControlsTheme( + normal: MaterialVideoControlsThemeData( + bottomButtonBar: [ + const MaterialPositionIndicator( + style: TextStyle( + color: kWhiteColor, + fontSize: 14, + ), + ), + const Spacer(), + IconButton( + onPressed: () { + if (player.state.volume > 0.0) { + player.setVolume(0); + isMuted.value = true; + } else { + player.setVolume(100); + isMuted.value = false; + } + }, + icon: ValueListenableBuilder( + valueListenable: isMuted, + builder: (context, isMuted, __) { + return LMIcon( + type: LMIconType.icon, + color: kWhiteColor, + icon: isMuted + ? Icons.volume_off + : Icons.volume_up, + ); + }), + ) + ], + seekBarMargin: const EdgeInsets.symmetric( + horizontal: 4, + vertical: 8, ), - onPressed: () { - _timer?.cancel(); - - // pause while video is playing, play while video is pausing - - controller.player.state.playing - ? controller.player.pause() - : controller.player.play(); - rebuildOverlay.value = !rebuildOverlay.value; - - // Auto dismiss overlay after 1 second - _timer = Timer.periodic( - const Duration(milliseconds: 2500), (_) { - _onTouch = false; - rebuildOverlay.value = !rebuildOverlay.value; - }); - }, + seekBarPositionColor: widget.seekBarColor ?? + const Color.fromARGB(255, 0, 137, 123), + seekBarThumbColor: widget.seekBarColor ?? + const Color.fromARGB(255, 0, 137, 123), + ), + fullscreen: const MaterialVideoControlsThemeData(), + child: Video( + controller: controller!, + filterQuality: FilterQuality.low, + controls: widget.showControls != null && + widget.showControls! + ? media_kit_video_controls.AdaptiveVideoControls + : (state) { + return ValueListenableBuilder( + valueListenable: rebuildOverlay, + builder: (context, _, __) { + return Visibility( + visible: _onTouch, + child: Container( + alignment: Alignment.center, + child: TextButton( + style: ButtonStyle( + shape: MaterialStateProperty.all( + const CircleBorder( + side: BorderSide( + color: Colors.white, + ), + ), + ), + ), + child: Icon( + controller != null && + controller! + .player.state.playing + ? Icons.pause + : Icons.play_arrow, + size: 28, + color: Colors.white, + ), + onPressed: () { + _timer?.cancel(); + if (controller == null) { + return; + } + controller!.player.state.playing + ? state + .widget.controller.player + .pause() + : state + .widget.controller.player + .play(); + rebuildOverlay.value = + !rebuildOverlay.value; + _timer = Timer.periodic( + const Duration( + milliseconds: 2500), + (_) { + _onTouch = false; + rebuildOverlay.value = + !rebuildOverlay.value; + }, + ); + }, + ), + ), + ); + }, + ); + }, ), ), - ); - }), - ) - ]); - } else { - return widget.errorWidget ?? const SizedBox(); - } - }, - ); + ), + ), + ]); + } else { + return widget.errorWidget ?? const SizedBox(); + } + }, + ); + }); } } diff --git a/lib/src/widgets/post/post_content.dart b/lib/src/widgets/post/post_content.dart index 710b545..2547044 100644 --- a/lib/src/widgets/post/post_content.dart +++ b/lib/src/widgets/post/post_content.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:likeminds_feed_ui_fl/packages/expandable_text/expandable_text.dart'; -import 'package:likeminds_feed_ui_fl/src/utils/theme.dart'; import 'package:likeminds_feed_ui_fl/src/widgets/post/post.dart'; class LMPostContent extends StatelessWidget { diff --git a/lib/src/widgets/post/post_media.dart b/lib/src/widgets/post/post_media.dart index 6437557..d8df8a4 100644 --- a/lib/src/widgets/post/post_media.dart +++ b/lib/src/widgets/post/post_media.dart @@ -45,30 +45,39 @@ class LMPostMedia extends StatefulWidget { } class _LMPostMediaState extends State { - late List attachments; + List? attachments; late Size screenSize; + void initialiseAttachments() { + attachments = [...widget.attachments]; + attachments?.removeWhere((element) => element.attachmentType == 5); + } + @override void initState() { super.initState(); + initialiseAttachments(); + } + + @override + void didUpdateWidget(LMPostMedia oldWidget) { + super.didUpdateWidget(oldWidget); + initialiseAttachments(); } @override Widget build(BuildContext context) { - attachments = [...widget.attachments]; - attachments.removeWhere((element) => element.attachmentType == 5); screenSize = MediaQuery.of(context).size; - ThemeData theme = Theme.of(context); - if (attachments.isEmpty) { + if (attachments == null || attachments!.isEmpty) { return const SizedBox(); } // attachments = InheritedPostProvider.of(context)?.post.attachments ?? []; - if (attachments.first.attachmentType == 3) { + if (attachments!.first.attachmentType == 3) { /// If the attachment is a document, we need to call the method 'getDocumentList' return getPostDocuments(); - } else if (attachments.first.attachmentType == 4) { + } else if (attachments!.first.attachmentType == 4) { return LMLinkPreview( - attachment: attachments[0], + attachment: attachments![0], borderRadius: widget.borderRadius, backgroundColor: widget.backgroundColor, showLinkUrl: widget.showLinkUrl, @@ -78,7 +87,7 @@ class _LMPostMediaState extends State { ); } else { return LMCarousel( - attachments: attachments, + attachments: attachments!, borderRadius: widget.borderRadius, activeIndicatorColor: widget.carouselActiveIndicatorColor, inactiveIndicatorColor: widget.carouselInactiveIndicatorColor, @@ -94,7 +103,7 @@ class _LMPostMediaState extends State { List documents; bool isCollapsed = true; - documents = attachments + documents = attachments! .map( (e) => LMDocument( // document: e, @@ -121,11 +130,11 @@ class _LMPostMediaState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Column( - children: documents != null && documents.length > 3 && isCollapsed + children: documents.length > 3 && isCollapsed ? documents.sublist(0, 3) : documents, ), - documents != null && documents.length > 3 && isCollapsed + documents.length > 3 && isCollapsed ? GestureDetector( onTap: () => setState(() { isCollapsed = false; diff --git a/pubspec.lock b/pubspec.lock index f433671..178cf7a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -72,7 +72,7 @@ packages: sha256: fd3d0dc1d451f9a252b32d95d3f0c3c487bc41a75eba2e6097cb0b9c71491b15 url: "https://pub.dev" source: hosted - version: "3.2.3" + version: "3.3.0" cached_network_image_platform_interface: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 17d1ab4..ca03023 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: likeminds_feed_ui_fl description: A Flutter package for Likeminds Feed UI widgets. Used alongside the Likeminds Feed SDK package (likeminds_feed) to build custom interfaces. -version: 1.3.5 +version: 1.3.6 publish_to: none homepage: "www.likeminds.community/" @@ -15,7 +15,6 @@ dependencies: timeago: shimmer: carousel_slider: - video_player: 2.7.1 visibility_detector: cached_network_image: 3.3.0 url_launcher: @@ -26,7 +25,8 @@ dependencies: extended_image: 8.1.1 media_kit: ^1.1.7 # Primary package. media_kit_video: ^1.1.8 # For video rendering. - media_kit_libs_video: ^1.0.1 + media_kit_libs_video: ^1.0.1 + visibility_aware_state: 1.0.5 likeminds_feed: 1.6.3 # path: ../LikeMinds-Flutter-Feed-SDK