diff --git a/lib/core/services/constants.dart b/lib/core/services/constants.dart index f665b82..eb6e096 100644 --- a/lib/core/services/constants.dart +++ b/lib/core/services/constants.dart @@ -1 +1 @@ -const String baseUrl = 'https://ea80-114-6-25-184.ngrok-free.app/'; +const String baseUrl = 'https://8b78-114-6-25-184.ngrok-free.app/'; diff --git a/lib/features/learning/modules/material/screens/material_screen.dart b/lib/features/learning/modules/material/screens/material_screen.dart index 5fb344b..882dc67 100644 --- a/lib/features/learning/modules/material/screens/material_screen.dart +++ b/lib/features/learning/modules/material/screens/material_screen.dart @@ -10,7 +10,7 @@ import 'package:english_learning/features/learning/modules/material/widgets/imag import 'package:english_learning/features/learning/modules/material/widgets/video_player_widget.dart'; import 'package:flutter/material.dart'; import 'package:english_learning/core/utils/styles/theme.dart'; -import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; import 'package:provider/provider.dart'; class MaterialScreen extends StatefulWidget { @@ -152,33 +152,53 @@ class _MaterialScreenState extends State child: Column( children: [ const SizedBox(height: 22), - Html( - data: level.content, - style: { - 'body': Style( - fontSize: FontSize(14), - fontWeight: FontWeight.w500, - ), + HtmlWidget( + level.content, + customStylesBuilder: (element) { + if (element.localName == 'table') { + return {'width': '100%', 'border-collapse': 'collapse'}; + } + if (element.localName == 'th' || + element.localName == 'td') { + return { + 'padding': '8px', + 'border': '1px solid #ddd', + 'text-align': 'center', + }; + } + if (element.localName == 'th') { + return { + 'background-color': '#f5f5f5', + 'font-weight': 'bold', + }; + } + return null; }, - extensions: [ - ImageExtension( - builder: (context) => ImageWidget( - imageFileName: context.attributes['src'] ?? '', - ), - ), - AudioExtension( - builder: (context) => AudioPlayerWidget( - key: ValueKey('audio_${context.attributes['src']}'), - audioFileName: context.attributes['src'] ?? '', - ), - ), - VideoExtension( - builder: (context) => VideoPlayerWidget( - key: ValueKey('video_${context.attributes['src']}'), - videoUrl: context.attributes['src'] ?? '', - ), - ), - ], + customWidgetBuilder: (element) { + if (element.localName == 'img') { + return ImageWidget( + imageFileName: element.attributes['src'] ?? '', + ); + } + if (element.localName == 'audio') { + return AudioPlayerWidget( + key: ValueKey('audio_${element.attributes['src']}'), + audioFileName: element.attributes['src'] ?? '', + ); + } + if (element.localName == 'iframe') { + return VideoPlayerWidget( + key: ValueKey('video_${element.attributes['src']}'), + videoUrl: element.attributes['src'] ?? '', + ); + } + return null; + }, + renderMode: RenderMode.column, + textStyle: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), ), const SizedBox(height: 32), if (!widget.isReview) @@ -211,49 +231,3 @@ class _MaterialScreenState extends State }); } } - -class ImageExtension extends HtmlExtension { - final Widget Function(ExtensionContext) builder; - - ImageExtension({required this.builder}); - - @override - Set get supportedTags => {'img'}; - - @override - InlineSpan build(ExtensionContext context) { - context.attributes.remove('width'); - context.attributes.remove('height'); - - return WidgetSpan(child: builder(context)); - } -} - -class AudioExtension extends HtmlExtension { - final Widget Function(ExtensionContext) builder; - - AudioExtension({required this.builder}); - - @override - Set get supportedTags => {'audio'}; - - @override - InlineSpan build(ExtensionContext context) => - WidgetSpan(child: builder(context)); -} - -class VideoExtension extends HtmlExtension { - final Widget Function(ExtensionContext) builder; - - VideoExtension({required this.builder}); - - @override - Set get supportedTags => {'iframe'}; - - @override - InlineSpan build(ExtensionContext context) { - context.attributes.remove('width'); - context.attributes.remove('height'); - return WidgetSpan(child: builder(context)); - } -} diff --git a/lib/features/learning/modules/material/widgets/audio_player_widget.dart b/lib/features/learning/modules/material/widgets/audio_player_widget.dart index 6fa190c..b1502f9 100644 --- a/lib/features/learning/modules/material/widgets/audio_player_widget.dart +++ b/lib/features/learning/modules/material/widgets/audio_player_widget.dart @@ -1,4 +1,5 @@ import 'package:audioplayers/audioplayers.dart'; +import 'package:english_learning/features/learning/modules/material/widgets/loading/media_loading_widget.dart'; import 'package:flutter/material.dart'; class AudioPlayerWidget extends StatefulWidget { @@ -15,65 +16,276 @@ class AudioPlayerWidget extends StatefulWidget { State createState() => AudioPlayerWidgetState(); } -class AudioPlayerWidgetState extends State { +class AudioPlayerWidgetState extends State + with TickerProviderStateMixin { late AudioPlayer _audioPlayer; + late AnimationController _loadingController; + late AnimationController _fadeController; + PlayerState _playerState = PlayerState.stopped; Duration _duration = Duration.zero; Duration _position = Duration.zero; bool _isAudioLoaded = false; String? _errorMessage; double _volume = 1.0; - - bool get wantKeepAlive => true; + bool _isDisposed = false; @override void initState() { super.initState(); _audioPlayer = AudioPlayer(); + _loadingController = AnimationController( + vsync: this, + duration: const Duration(seconds: 1), + )..repeat(); + + _fadeController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 300), + ); + _setupAudioPlayer(); } void _setupAudioPlayer() { if (widget.audioFileName.isNotEmpty) { - String fullAudioUrl = widget.audioFileName.startsWith('http') - ? widget.audioFileName - : '${widget.baseUrl ?? ''}${widget.audioFileName}'; + String fullAudioUrl = _getFullAudioUrl(); _audioPlayer.setSource(UrlSource(fullAudioUrl)).then((_) { - setState(() { - _isAudioLoaded = true; - }); + if (!_isDisposed) { + setState(() { + _isAudioLoaded = true; + _fadeController.forward(); + }); + } }).catchError((error) { - setState(() { - _errorMessage = "Failed to load audio"; - }); + if (!_isDisposed) { + setState(() { + _errorMessage = "Failed to load audio: $error"; + }); + } }); - _audioPlayer.onPlayerStateChanged.listen((state) { - setState(() { - _playerState = state; - }); - }); - - _audioPlayer.onDurationChanged.listen((newDuration) { - setState(() { - _duration = newDuration; - }); - }); - - _audioPlayer.onPositionChanged.listen((newPosition) { - setState(() { - _position = newPosition; - }); - }); + _setupAudioListeners(); } } + void _setupAudioListeners() { + _audioPlayer.onPlayerStateChanged.listen((state) { + if (!_isDisposed) { + setState(() => _playerState = state); + } + }); + + _audioPlayer.onDurationChanged.listen((newDuration) { + if (!_isDisposed) { + setState(() => _duration = newDuration); + } + }); + + _audioPlayer.onPositionChanged.listen((newPosition) { + if (!_isDisposed) { + setState(() => _position = newPosition); + } + }); + } + + String _getFullAudioUrl() { + return widget.audioFileName.startsWith('http') + ? widget.audioFileName + : '${widget.baseUrl ?? ''}${widget.audioFileName}'; + } + + Widget _buildErrorWidget() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(16), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.error_outline, + color: Colors.red[400], + size: 32, + ), + const SizedBox(height: 8), + Text( + _errorMessage ?? 'Error loading audio', + style: TextStyle( + color: Colors.red[400], + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 12), + TextButton.icon( + onPressed: () { + setState(() { + _errorMessage = null; + _isAudioLoaded = false; + _setupAudioPlayer(); + }); + }, + icon: const Icon(Icons.refresh), + label: const Text('Retry'), + style: TextButton.styleFrom( + foregroundColor: Colors.blue, + ), + ), + ], + ), + ); + } + + Widget _buildAudioControls() { + return FadeTransition( + opacity: _fadeController, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + _buildPlayButton(), + const SizedBox(width: 12), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildTimelineIndicator(), + const SizedBox(height: 4), + _buildProgressSlider(), + ], + ), + ), + const SizedBox(width: 12), + _buildVolumeButton(), + ], + ), + if (_errorMessage != null) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + _errorMessage!, + style: TextStyle( + color: Colors.red[400], + fontSize: 12, + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildPlayButton() { + return IconButton( + icon: Icon( + _playerState == PlayerState.playing + ? Icons.pause_rounded + : Icons.play_arrow_rounded, + color: Colors.blue, + size: 32, + ), + onPressed: _isAudioLoaded + ? () { + if (_playerState == PlayerState.playing) { + _audioPlayer.pause(); + } else { + _audioPlayer.resume(); + } + } + : null, + ); + } + + Widget _buildTimelineIndicator() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + _formatDuration(_position), + style: const TextStyle(fontSize: 12), + ), + Text( + _formatDuration(_duration), + style: const TextStyle(fontSize: 12), + ), + ], + ); + } + + Widget _buildProgressSlider() { + return SliderTheme( + data: const SliderThemeData( + trackHeight: 4, + thumbShape: RoundSliderThumbShape(enabledThumbRadius: 6), + overlayShape: RoundSliderOverlayShape(overlayRadius: 14), + ), + child: Slider( + value: _position.inSeconds.toDouble(), + min: 0.0, + max: _duration.inSeconds.toDouble(), + onChanged: _isAudioLoaded + ? (value) { + final position = Duration(seconds: value.toInt()); + _audioPlayer.seek(position); + } + : null, + activeColor: Colors.blue, + inactiveColor: Colors.grey[300], + ), + ); + } + + Widget _buildVolumeButton() { + return IconButton( + icon: Icon( + _volume == 0.0 ? Icons.volume_off : Icons.volume_up, + color: _volume == 0.0 ? Colors.grey : Colors.blue, + size: 24, + ), + onPressed: _isAudioLoaded + ? () { + if (_volume == 0.0) { + _audioPlayer.setVolume(1.0); + setState(() => _volume = 1.0); + } else { + _audioPlayer.setVolume(0.0); + setState(() => _volume = 0.0); + } + } + : null, + ); + } + @override - void dispose() { - _audioPlayer.stop(); - _audioPlayer.dispose(); - super.dispose(); + Widget build(BuildContext context) { + if (_errorMessage != null) { + return _buildErrorWidget(); + } + + if (!_isAudioLoaded) { + return const AudioShimmerLoader(); + } + + return _buildAudioControls(); } String _formatDuration(Duration duration) { @@ -89,123 +301,11 @@ class AudioPlayerWidgetState extends State { } @override - Widget build(BuildContext context) { - if (_errorMessage != null) { - return Text( - _errorMessage!, - style: const TextStyle( - color: Colors.red, - ), - ); - } - - if (!_isAudioLoaded) { - return const CircularProgressIndicator(); - } - - return Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: Row( - children: [ - // Play/Pause Button - IconButton( - icon: Icon( - _playerState == PlayerState.playing - ? Icons.pause - : Icons.play_arrow, - color: Colors.blue, - size: 32, - ), - onPressed: () { - if (_playerState == PlayerState.playing) { - _audioPlayer.pause(); - } else { - _audioPlayer.resume(); - } - }, - ), - const SizedBox(width: 12), - // Timeline and Slider - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Timeline - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - _formatDuration(_position), - style: const TextStyle( - fontSize: 12, - ), - ), - Text( - _formatDuration(_duration), - style: const TextStyle( - fontSize: 12, - ), - ), - ], - ), - const SizedBox(height: 4), - // Slider - SliderTheme( - data: const SliderThemeData( - trackHeight: 4, - thumbShape: RoundSliderThumbShape(enabledThumbRadius: 6), - overlayShape: RoundSliderOverlayShape(overlayRadius: 14), - ), - child: Slider( - value: _position.inSeconds.toDouble(), - min: 0.0, - max: _duration.inSeconds.toDouble(), - onChanged: (value) { - final position = Duration(seconds: value.toInt()); - _audioPlayer.seek(position); - }, - activeColor: Colors.blue, - inactiveColor: Colors.grey, - ), - ), - ], - ), - ), - const SizedBox(width: 12), - // Mute Button - IconButton( - icon: Icon( - Icons.volume_up, - color: _volume == 0.0 ? Colors.grey : Colors.blue, - size: 32, - ), - onPressed: () { - if (_volume == 0.0) { - _audioPlayer.setVolume(1.0); - setState(() { - _volume = 1.0; - }); - } else { - _audioPlayer.setVolume(0.0); - setState(() { - _volume = 0.0; - }); - } - }, - ), - ], - ), - ); + void dispose() { + _isDisposed = true; + _audioPlayer.dispose(); + _loadingController.dispose(); + _fadeController.dispose(); + super.dispose(); } } diff --git a/lib/features/learning/modules/material/widgets/image_widget.dart b/lib/features/learning/modules/material/widgets/image_widget.dart index 2be5795..f97c9ad 100644 --- a/lib/features/learning/modules/material/widgets/image_widget.dart +++ b/lib/features/learning/modules/material/widgets/image_widget.dart @@ -1,48 +1,204 @@ +// ignore_for_file: unused_field + import 'package:cached_network_image/cached_network_image.dart'; +import 'package:english_learning/features/learning/modules/material/widgets/loading/media_loading_widget.dart'; import 'package:flutter/material.dart'; -class ImageWidget extends StatelessWidget { +class ImageWidget extends StatefulWidget { final String imageFileName; final String? baseUrl; + final double? height; + final BoxFit fit; const ImageWidget({ super.key, required this.imageFileName, this.baseUrl, + this.height = 200, + this.fit = BoxFit.cover, }); @override - Widget build(BuildContext context) { - String fullImageUrl = imageFileName.startsWith('http') - ? imageFileName - : '${baseUrl ?? ''}$imageFileName'; + State createState() => _ImageWidgetState(); +} +class _ImageWidgetState extends State + with TickerProviderStateMixin { + late AnimationController _loadingController; + late AnimationController _fadeController; + bool _isImageLoaded = false; + String? _errorMessage; + final TransformationController _transformationController = + TransformationController(); + + @override + void initState() { + super.initState(); + _loadingController = AnimationController( + vsync: this, + duration: const Duration(seconds: 2), + )..repeat(); + + _fadeController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 300), + ); + } + + @override + void dispose() { + _loadingController.dispose(); + _fadeController.dispose(); + _transformationController.dispose(); + super.dispose(); + } + + String _getFullImageUrl() { + if (widget.imageFileName.isEmpty) { + throw Exception('Image file name cannot be empty'); + } + return widget.imageFileName.startsWith('http') + ? widget.imageFileName + : '${widget.baseUrl ?? ''}${widget.imageFileName}'; + } + + void _showFullScreenImage(BuildContext context, ImageProvider imageProvider) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => FullScreenImageView( + imageProvider: imageProvider, + imageUrl: _getFullImageUrl(), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { return Container( width: double.infinity, - height: 200, + height: widget.height, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 10, - offset: Offset(0, 5), + offset: const Offset(0, 5), + spreadRadius: 0, ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(12), - child: CachedNetworkImage( - imageUrl: fullImageUrl, - fit: BoxFit.cover, - placeholder: (context, url) => - const Center(child: CircularProgressIndicator()), - errorWidget: (context, url, error) => Container( - color: Colors.grey[300], - child: const Icon( - Icons.error_outline, - color: Colors.red, - size: 50, + child: Stack( + fit: StackFit.expand, + children: [ + Container(color: Colors.grey[200]), + CachedNetworkImage( + imageUrl: _getFullImageUrl(), + fit: widget.fit, + fadeInDuration: const Duration(milliseconds: 300), + fadeOutDuration: const Duration(milliseconds: 300), + imageBuilder: (context, imageProvider) { + _isImageLoaded = true; + _fadeController.forward(); + return GestureDetector( + onTap: () => _showFullScreenImage(context, imageProvider), + child: Image(image: imageProvider, fit: widget.fit), + ); + }, + placeholder: (context, url) => MediaLoadingWidget( + width: double.infinity, + height: widget.height ?? 200, + borderRadius: BorderRadius.circular(12), + ), + errorWidget: (context, url, error) => Container( + color: Colors.grey[300], + child: const Center( + child: Icon(Icons.error_outline, color: Colors.red), + ), + ), + ), + ], + ), + ), + ); + } +} + +class FullScreenImageView extends StatefulWidget { + final ImageProvider imageProvider; + final String imageUrl; + + const FullScreenImageView({ + super.key, + required this.imageProvider, + required this.imageUrl, + }); + + @override + State createState() => _FullScreenImageViewState(); +} + +class _FullScreenImageViewState extends State { + late TransformationController _controller; + late TapDownDetails _doubleTapDetails; + + @override + void initState() { + super.initState(); + _controller = TransformationController(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _handleDoubleTapDown(TapDownDetails details) { + _doubleTapDetails = details; + } + + void _handleDoubleTap() { + if (_controller.value != Matrix4.identity()) { + _controller.value = Matrix4.identity(); + } else { + final position = _doubleTapDetails.localPosition; + _controller.value = Matrix4.identity() + ..translate(-position.dx * 1, -position.dy * 1) + ..scale(2.0); + } + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + backgroundColor: Colors.black, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.close, color: Colors.white), + onPressed: () => Navigator.of(context).pop(), + ), + ), + body: GestureDetector( + onDoubleTapDown: _handleDoubleTapDown, + onDoubleTap: _handleDoubleTap, + child: Center( + child: InteractiveViewer( + transformationController: _controller, + minScale: 1.0, + maxScale: 4.0, + child: Hero( + tag: widget.imageUrl, + child: Image( + image: widget.imageProvider, + fit: BoxFit.contain, + ), ), ), ), diff --git a/lib/features/learning/modules/material/widgets/loading/media_loading_widget.dart b/lib/features/learning/modules/material/widgets/loading/media_loading_widget.dart new file mode 100644 index 0000000..29abbdb --- /dev/null +++ b/lib/features/learning/modules/material/widgets/loading/media_loading_widget.dart @@ -0,0 +1,170 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; + +class MediaLoadingWidget extends StatelessWidget { + final double width; + final double height; + final BorderRadius? borderRadius; + final EdgeInsets? padding; + const MediaLoadingWidget({ + super.key, + required this.width, + required this.height, + this.borderRadius, + this.padding, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: padding ?? EdgeInsets.zero, + child: Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + period: const Duration(milliseconds: 1500), + child: Container( + width: width, + height: height, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: borderRadius ?? BorderRadius.circular(12), + ), + ), + ), + ); + } +} + +class MediaShimmerLoader extends StatelessWidget { + final double aspectRatio; + final bool showPlayIcon; + + const MediaShimmerLoader({ + super.key, + this.aspectRatio = 16 / 9, + this.showPlayIcon = false, + }); + + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: aspectRatio, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Colors.grey[200], + ), + child: Stack( + alignment: Alignment.center, + children: [ + Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + period: const Duration(milliseconds: 1500), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + ), + ), + if (showPlayIcon) + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.black.withOpacity(0.5), + ), + child: const Icon( + Icons.play_arrow_rounded, + color: Colors.white, + size: 32, + ), + ), + ], + ), + ), + ); + } +} + +class AudioShimmerLoader extends StatelessWidget { + const AudioShimmerLoader({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + period: const Duration(milliseconds: 1500), + child: Row( + children: [ + // Play button shimmer + Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 12), + // Progress bar shimmer + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Time indicators + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + width: 30, + height: 10, + color: Colors.white, + ), + Container( + width: 30, + height: 10, + color: Colors.white, + ), + ], + ), + const SizedBox(height: 8), + // Progress bar + Container( + height: 4, + color: Colors.white, + ), + ], + ), + ), + const SizedBox(width: 12), + // Volume button shimmer + Container( + width: 32, + height: 32, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + ), + ], + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 1079e51..729e124 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -334,14 +334,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.4.1" - flutter_html: - dependency: "direct main" - description: - name: flutter_html - sha256: "02ad69e813ecfc0728a455e4bf892b9379983e050722b1dce00192ee2e41d1ee" - url: "https://pub.dev" - source: hosted - version: "3.0.0-beta.2" flutter_inappwebview: dependency: "direct main" description: @@ -577,7 +569,7 @@ packages: source: hosted version: "5.0.6" html: - dependency: transitive + dependency: "direct main" description: name: html sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" @@ -760,14 +752,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" - list_counter: - dependency: transitive - description: - name: list_counter - sha256: c447ae3dfcd1c55f0152867090e67e219d42fe6d4f2807db4bbe8b8d69912237 - url: "https://pub.dev" - source: hosted - version: "1.0.2" logging: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 337fb43..d264894 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,13 +37,13 @@ dependencies: flick_video_player: ^0.9.0 flutter: sdk: flutter - flutter_html: ^3.0.0-beta.2 flutter_inappwebview: ^6.0.0 flutter_secure_storage: ^9.2.2 flutter_svg: ^2.0.10+1 flutter_widget_from_html: ^0.15.2 google_fonts: ^6.2.1 google_nav_bar: ^5.0.6 + html: ^0.15.4 image_picker: ^1.1.2 intl: ^0.19.0 jwt_decoder: ^2.0.1