diff --git a/lib/views/home/widgets/audio/audio_player_widget.dart b/lib/views/home/widgets/audio/audio_player_widget.dart new file mode 100644 index 0000000..f8b0a69 --- /dev/null +++ b/lib/views/home/widgets/audio/audio_player_widget.dart @@ -0,0 +1,179 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/models/studio_details_data.dart'; +import 'package:didvan/services/media/media.dart'; +import 'package:didvan/views/home/widgets/audio/audio_slider.dart'; +import 'package:didvan/views/home/widgets/bookmark_button.dart'; +import 'package:didvan/views/widgets/didvan/icon_button.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:didvan/views/widgets/ink_wrapper.dart'; +import 'package:didvan/views/widgets/skeleton_image.dart'; +import 'package:flutter/material.dart'; + +class AudioPlayerWidget extends StatelessWidget { + final StudioDetailsData podcast; + const AudioPlayerWidget({Key? key, required this.podcast}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.vertical(top: Radius.circular(8)), + color: Theme.of(context).colorScheme.surface, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + margin: const EdgeInsets.symmetric(vertical: 20), + height: 3, + width: 50, + color: Theme.of(context).colorScheme.hint, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: SkeletonImage( + imageUrl: podcast.image, + aspectRatio: 1 / 1, + ), + ), + const SizedBox(height: 16), + DidvanText( + podcast.title, + style: Theme.of(context).textTheme.bodyText1, + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: AudioSlider( + tag: podcast.media, + showTimer: true, + duration: podcast.duration, + ), + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + DidvanIconButton( + icon: DidvanIcons.sleep_timer_regular, + onPressed: () {}, + ), + Column( + children: [ + DidvanIconButton( + size: 32, + icon: DidvanIcons.media_forward_solid, + onPressed: () { + MediaService.audioPlayer.seek( + Duration( + seconds: + MediaService.audioPlayer.position.inSeconds + 30, + ), + ); + }, + ), + const DidvanText('30', isEnglishFont: true), + ], + ), + _PlayPouseAnimatedIcon( + audioSource: podcast.media, + ), + Column( + children: [ + DidvanIconButton( + size: 32, + icon: DidvanIcons.media_backward_solid, + onPressed: () { + MediaService.audioPlayer.seek( + Duration( + seconds: + MediaService.audioPlayer.position.inSeconds - 10, + ), + ); + }, + ), + const DidvanText('10', isEnglishFont: true), + ], + ), + BookmarkButton( + gestureSize: 48, + value: podcast.marked, + onMarkChanged: (value) {}, + ), + ], + ), + DidvanIconButton( + size: 32, + icon: DidvanIcons.angle_down_regular, + onPressed: Navigator.of(context).pop, + ), + ], + ), + ); + } +} + +class _PlayPouseAnimatedIcon extends StatefulWidget { + final String audioSource; + const _PlayPouseAnimatedIcon({Key? key, required this.audioSource}) + : super(key: key); + + @override + State<_PlayPouseAnimatedIcon> createState() => __PlayPouseAnimatedIconState(); +} + +class __PlayPouseAnimatedIconState extends State<_PlayPouseAnimatedIcon> + with SingleTickerProviderStateMixin { + late final AnimationController _animationController; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + vsync: this, + duration: DesignConfig.lowAnimationDuration, + ); + if (MediaService.audioPlayer.playing) { + _animationController.forward(); + } + } + + @override + Widget build(BuildContext context) { + return InkWrapper( + borderRadius: BorderRadius.circular(100), + onPressed: () { + MediaService.handleAudioPlayback( + audioSource: widget.audioSource, + isVoiceMessage: false, + ); + if (MediaService.audioPlayer.playing) { + _animationController.forward(); + } else { + _animationController.reverse(); + } + }, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.title, + shape: BoxShape.circle, + ), + child: AnimatedIcon( + size: 40, + color: Theme.of(context).colorScheme.surface, + icon: AnimatedIcons.play_pause, + progress: _animationController, + ), + ), + ); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } +} diff --git a/lib/views/home/widgets/audio/audio_slider.dart b/lib/views/home/widgets/audio/audio_slider.dart new file mode 100644 index 0000000..9c80349 --- /dev/null +++ b/lib/views/home/widgets/audio/audio_slider.dart @@ -0,0 +1,63 @@ +import 'package:audio_video_progress_bar/audio_video_progress_bar.dart'; +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/services/media/media.dart'; +import 'package:flutter/material.dart'; + +class AudioSlider extends StatelessWidget { + final String tag; + final bool showTimer; + final int? duration; + final bool disableThumb; + const AudioSlider({ + Key? key, + required this.tag, + this.showTimer = false, + this.duration, + this.disableThumb = false, + }) : super(key: key); + + bool get _isPlaying => MediaService.audioPlayerTag == tag; + + @override + Widget build(BuildContext context) { + return IgnorePointer( + ignoring: MediaService.audioPlayerTag != tag, + child: Directionality( + textDirection: TextDirection.ltr, + child: StreamBuilder( + stream: _isPlaying ? MediaService.audioPlayer.positionStream : null, + builder: (context, snapshot) => ProgressBar( + thumbColor: Theme.of(context).colorScheme.title, + progressBarColor: DesignConfig.isDark + ? Theme.of(context).colorScheme.title + : Theme.of(context).colorScheme.primary, + baseBarColor: Theme.of(context).colorScheme.border, + bufferedBarColor: Theme.of(context).colorScheme.splash, + total: MediaService.audioPlayer.duration ?? + Duration(seconds: duration ?? 0), + progress: snapshot.data ?? Duration.zero, + buffered: _isPlaying + ? MediaService.audioPlayer.bufferedPosition + : Duration.zero, + thumbRadius: disableThumb ? 0 : 6, + barHeight: 3, + timeLabelTextStyle: TextStyle( + fontSize: showTimer ? null : 0, + height: showTimer ? 3 : 0, + fontFamily: DesignConfig.fontFamily.replaceAll( + '-FA', + '', + ), + ), + onSeek: (value) => _onSeek(value.inMilliseconds), + ), + ), + ), + ); + } + + void _onSeek(int value) { + MediaService.audioPlayer.seek(Duration(milliseconds: value)); + } +} diff --git a/lib/views/home/widgets/audio/audio_visualizer.dart b/lib/views/home/widgets/audio/audio_visualizer.dart new file mode 100644 index 0000000..c39ca3d --- /dev/null +++ b/lib/views/home/widgets/audio/audio_visualizer.dart @@ -0,0 +1,314 @@ +// import 'dart:io'; +// import 'dart:math'; + +// import 'package:didvan/config/design_config.dart'; +// import 'package:didvan/config/theme_data.dart'; +// import 'package:didvan/constants/app_icons.dart'; +// import 'package:didvan/constants/assets.dart'; +// import 'package:didvan/pages/home/direct/direct_state.dart'; +// import 'package:didvan/services/media/media.dart'; +// import 'package:didvan/services/storage/storage.dart'; +// import 'package:didvan/utils/date_time.dart'; +// import 'package:didvan/widgets/didvan/icon_button.dart'; +// import 'package:didvan/widgets/didvan/text.dart'; +// import 'package:flutter/foundation.dart'; +// import 'package:flutter/material.dart'; +// import 'package:flutter_svg/flutter_svg.dart'; +// import 'package:just_waveform/just_waveform.dart'; +// import 'package:provider/provider.dart'; + +// class AudioVisualizer extends StatefulWidget { +// final File? audioFile; +// final Waveform? waveform; +// final String? audioUrl; +// final int? duration; +// final Color? backgroundColor; + +// const AudioVisualizer({ +// Key? key, +// this.audioFile, +// this.waveform, +// this.audioUrl, +// this.duration, +// this.backgroundColor, +// }) : super(key: key); + +// @override +// State createState() => _AudioVisualizerState(); +// } + +// class _AudioVisualizerState extends State { +// Stream? waveDataStream; + +// @override +// void initState() { +// if (!kIsWeb && widget.audioFile != null) { +// waveDataStream = JustWaveform.extract( +// audioInFile: widget.audioFile!, +// waveOutFile: File(StorageService.appTempsDir + '/rec-wave.wave'), +// zoom: const WaveformZoom.pixelsPerSecond(100), +// ); +// } +// super.initState(); +// } + +// bool get _nowPlaying => +// MediaService.lastAudioPath == widget.audioFile || +// MediaService.lastAudioPath == widget.audioUrl; + +// @override +// Widget build(BuildContext context) { +// return Container( +// decoration: BoxDecoration( +// color: widget.backgroundColor ?? +// (DesignConfig.isDark +// ? Theme.of(context).colorScheme.black +// : Theme.of(context).colorScheme.background), +// borderRadius: DesignConfig.mediumBorderRadius, +// ), +// child: Row( +// children: [ +// const SizedBox(width: 12), +// StreamBuilder( +// stream: +// _nowPlaying ? MediaService.audioPlayer.positionStream : null, +// builder: (context, snapshot) { +// String text = ''; +// if (MediaService.audioPlayer.duration == null) { +// Future.delayed(Duration.zero, () { +// if (mounted) { +// setState(() {}); +// } +// }); +// } +// if (snapshot.data == null || snapshot.data == Duration.zero) { +// text = DateTimeUtils.normalizeTimeDuration( +// MediaService.audioPlayer.duration ?? +// widget.waveform?.duration ?? +// Duration.zero); +// } else { +// text = DateTimeUtils.normalizeTimeDuration(snapshot.data!); +// } +// return DidvanText( +// text, +// color: Theme.of(context).colorScheme.focusedBorder, +// isEnglishFont: true, +// ); +// }, +// ), +// const SizedBox(width: 12), +// Expanded( +// child: Builder( +// builder: (context) { +// if (kIsWeb) { +// return SvgPicture.asset(Assets.record); +// } +// if (widget.audioFile != null) { +// return StreamBuilder( +// stream: waveDataStream, +// builder: (context, snapshot) { +// if (snapshot.data == null || +// snapshot.data!.waveform == null) { +// return const SizedBox(); +// } +// final waveform = snapshot.data!.waveform!; +// context.read().waveform = waveform; +// return _waveWidget(waveform); +// }, +// ); +// } +// if (widget.waveform == null && waveDataStream == null) { +// return SvgPicture.asset(Assets.record); +// } +// return _waveWidget(widget.waveform!); +// }, +// ), +// ), +// StreamBuilder( +// stream: _nowPlaying ? MediaService.audioPlayer.playingStream : null, +// builder: (context, snapshot) { +// return DidvanIconButton( +// icon: snapshot.data == true +// ? DidvanIcons.pause_circle_solid +// : DidvanIcons.play_circle_solid, +// color: Theme.of(context).colorScheme.focusedBorder, +// onPressed: () { +// MediaService.handleAudioPlayback( +// audioSource: widget.audioFile ?? widget.audioUrl, +// isNetworkAudio: widget.audioFile == null, +// ); +// setState(() {}); +// }, +// ); +// }, +// ), +// ], +// ), +// ); +// } + +// Widget _waveWidget(Waveform waveform) => IgnorePointer( +// ignoring: !_nowPlaying, +// child: GestureDetector( +// onHorizontalDragUpdate: _changePosition, +// onTapDown: _changePosition, +// child: SizedBox( +// height: double.infinity, +// width: double.infinity, +// child: _AudioWaveformWidget( +// waveform: waveform, +// start: Duration.zero, +// scale: 2, +// strokeWidth: 3, +// nowPlaying: _nowPlaying, +// duration: waveform.duration, +// waveColor: Theme.of(context).colorScheme.focusedBorder, +// ), +// ), +// ), +// ); + +// void _changePosition(details) { +// if (MediaService.audioPlayer.audioSource == null) return; +// double posper = +// details.localPosition.dx / (MediaQuery.of(context).size.width - 200); +// if (posper >= 1 || posper < 0) return; +// final position = MediaService.audioPlayer.duration!.inMilliseconds; +// MediaService.audioPlayer.seek( +// Duration(milliseconds: (posper * position).toInt()), +// ); +// } +// } + +// class _AudioWaveformWidget extends StatelessWidget { +// final Color waveColor; +// final double scale; +// final double strokeWidth; +// final double pixelsPerStep; +// final Waveform waveform; +// final Duration start; +// final bool nowPlaying; +// final Duration duration; + +// const _AudioWaveformWidget({ +// Key? key, +// required this.waveform, +// required this.start, +// required this.duration, +// required this.nowPlaying, +// this.waveColor = Colors.blue, +// this.scale = 1.0, +// this.strokeWidth = 5.0, +// this.pixelsPerStep = 8.0, +// }) : super(key: key); + +// @override +// Widget build(BuildContext context) { +// return ClipRect( +// child: StreamBuilder( +// stream: nowPlaying ? MediaService.audioPlayer.positionStream : null, +// builder: (context, snapshot) { +// double progress = 0; +// if (snapshot.data == null || +// MediaService.audioPlayer.duration == null) { +// progress = 0; +// } else { +// progress = snapshot.data!.inMilliseconds / +// MediaService.audioPlayer.duration!.inMilliseconds * +// 100; +// } +// if (progress >= 100) { +// progress = 0; +// MediaService.audioPlayer.stop(); +// MediaService.audioPlayer.seek(Duration.zero); +// } +// return CustomPaint( +// painter: _AudioWaveformPainter( +// waveColor: waveColor, +// waveform: waveform, +// start: start, +// duration: duration, +// scale: scale, +// strokeWidth: strokeWidth, +// pixelsPerStep: pixelsPerStep, +// progressPercentage: progress, +// progressColor: Theme.of(context).colorScheme.focusedBorder, +// color: Theme.of(context).colorScheme.border, +// ), +// ); +// }), +// ); +// } +// } + +// class _AudioWaveformPainter extends CustomPainter { +// final double scale; +// final double strokeWidth; +// final double pixelsPerStep; +// final Waveform waveform; +// final Duration start; +// final Duration duration; +// final double progressPercentage; +// final Color progressColor; +// final Color color; + +// _AudioWaveformPainter({ +// required this.waveform, +// required this.start, +// required this.duration, +// required this.progressPercentage, +// required this.color, +// required this.progressColor, +// Color waveColor = Colors.blue, +// this.scale = 1.0, +// this.strokeWidth = 5.0, +// this.pixelsPerStep = 8.0, +// }); + +// @override +// void paint(Canvas canvas, Size size) { +// if (duration == Duration.zero) return; +// double width = size.width; +// double height = size.height; + +// final waveformPixelsPerWindow = waveform.positionToPixel(duration).toInt(); +// final waveformPixelsPerDevicePixel = waveformPixelsPerWindow / width; +// final waveformPixelsPerStep = waveformPixelsPerDevicePixel * pixelsPerStep; +// final sampleOffset = waveform.positionToPixel(start); +// final sampleStart = -sampleOffset % waveformPixelsPerStep; +// final totalLength = waveformPixelsPerWindow; +// final wavePaintB = Paint() +// ..style = PaintingStyle.stroke +// ..strokeWidth = strokeWidth +// ..strokeCap = StrokeCap.round +// ..color = progressColor; +// final wavePaintA = Paint() +// ..style = PaintingStyle.stroke +// ..strokeWidth = strokeWidth +// ..strokeCap = StrokeCap.round +// ..color = color; +// for (var i = sampleStart.toDouble(); +// i <= waveformPixelsPerWindow + 1.0; +// i += waveformPixelsPerStep) { +// final sampleIdx = (sampleOffset + i).toInt(); +// final x = i / waveformPixelsPerDevicePixel; +// final minY = normalise(waveform.getPixelMin(sampleIdx), height); +// final maxY = normalise(waveform.getPixelMax(sampleIdx), height); +// canvas.drawLine( +// Offset(x + strokeWidth / 2, max(strokeWidth * 0.75, minY)), +// Offset(x + strokeWidth / 2, min(height - strokeWidth * 0.75, maxY)), +// i / totalLength < progressPercentage / 100 ? wavePaintB : wavePaintA, +// ); +// } +// } + +// @override +// bool shouldRepaint(covariant _AudioWaveformPainter oldDelegate) { +// return oldDelegate.progressPercentage != progressPercentage; +// } + +// double normalise(int s, double height) { +// final y = 32768 + (scale * s).clamp(-32768.0, 32767.0).toDouble(); +// return height - 1 - y * height / 65536; +// } +// } diff --git a/lib/views/home/widgets/bookmark_button.dart b/lib/views/home/widgets/bookmark_button.dart index 06d7604..ca98b06 100644 --- a/lib/views/home/widgets/bookmark_button.dart +++ b/lib/views/home/widgets/bookmark_button.dart @@ -8,14 +8,14 @@ import 'package:flutter/material.dart'; class BookmarkButton extends StatefulWidget { final bool value; final void Function(bool value) onMarkChanged; - final bool bigGestureSize; final bool askForConfirmation; + final double gestureSize; const BookmarkButton({ Key? key, required this.value, - this.bigGestureSize = false, required this.onMarkChanged, this.askForConfirmation = false, + required this.gestureSize, }) : super(key: key); @override @@ -40,7 +40,7 @@ class _BookmarkButtonState extends State { @override Widget build(BuildContext context) { return DidvanIconButton( - gestureSize: widget.bigGestureSize ? 32 : 28, + gestureSize: widget.gestureSize, icon: _value ? DidvanIcons.bookmark_solid : DidvanIcons.bookmark_regular, onPressed: () async { bool confirm = false; diff --git a/lib/views/home/widgets/floating_navigation_bar.dart b/lib/views/home/widgets/floating_navigation_bar.dart index 95727d1..28a88e4 100644 --- a/lib/views/home/widgets/floating_navigation_bar.dart +++ b/lib/views/home/widgets/floating_navigation_bar.dart @@ -112,7 +112,7 @@ class _FloatingNavigationBarState extends State { Navigator.of(context).pop(); } }, - bigGestureSize: true, + gestureSize: 32, ), SizedBox( width: 60, @@ -151,7 +151,7 @@ class _FloatingNavigationBarState extends State { Navigator.of(context).pop(); } }, - bigGestureSize: true, + gestureSize: 32, ), if (widget.isRadar) DidvanIconButton( diff --git a/lib/views/widgets/didvan/app_bar.dart b/lib/views/widgets/didvan/app_bar.dart index 7667dd8..f7f00e3 100644 --- a/lib/views/widgets/didvan/app_bar.dart +++ b/lib/views/widgets/didvan/app_bar.dart @@ -52,6 +52,7 @@ class DidvanAppBar extends StatelessWidget { appBarData.title!, style: Theme.of(context).textTheme.headline3, color: Theme.of(context).colorScheme.title, + overflow: TextOverflow.ellipsis, ), if (appBarData.subtitle != null) DidvanText( diff --git a/lib/views/widgets/didvan/page_view.dart b/lib/views/widgets/didvan/page_view.dart index 7fc6b19..580a3e1 100644 --- a/lib/views/widgets/didvan/page_view.dart +++ b/lib/views/widgets/didvan/page_view.dart @@ -3,7 +3,7 @@ import 'package:didvan/config/design_config.dart'; import 'package:didvan/config/theme_data.dart'; import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/utils/date_time.dart'; -import 'package:didvan/views/home/widgets/multitype_overview.dart'; +import 'package:didvan/views/home/widgets/overview/multitype.dart'; import 'package:didvan/views/home/widgets/tag_item.dart'; import 'package:didvan/views/widgets/animated_visibility.dart'; import 'package:didvan/views/widgets/didvan/card.dart'; diff --git a/lib/views/widgets/didvan/scaffold.dart b/lib/views/widgets/didvan/scaffold.dart index b1d8b28..0dabcf2 100644 --- a/lib/views/widgets/didvan/scaffold.dart +++ b/lib/views/widgets/didvan/scaffold.dart @@ -42,17 +42,14 @@ class _DidvanScaffoldState extends State { slivers: [ if (!widget.reverse && widget.appBarData != null) SliverAppBar( - toolbarHeight: widget.appBarData!.isSmall ? 56 : 72, + toolbarHeight: (widget.appBarData!.isSmall ? 56 : 72) - + statusBarHeight, backgroundColor: widget.backgroundColor ?? Theme.of(context).colorScheme.background, automaticallyImplyLeading: false, pinned: true, flexibleSpace: DidvanAppBar(appBarData: widget.appBarData!), ), - if (!widget.reverse) - const SliverToBoxAdapter( - child: SizedBox(height: 16), - ), if (widget.children != null) SliverPadding( padding: widget.padding,