From 596297e8b2a656a96c8699d8e1027bf5d9788e58 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 20 Feb 2026 14:29:55 +0100 Subject: [PATCH 1/5] separate the attachment container --- .../lib/src/components/message_composer.dart | 1 + ...message_composer_attachment_container.dart | 50 ++++++++ .../message_composer_file_attachment.dart | 68 ++++------ ...sage_composer_link_preview_attachment.dart | 119 ++++++++---------- .../message_composer_reply_attachment.dart | 105 +++++++--------- 5 files changed, 175 insertions(+), 168 deletions(-) create mode 100644 packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_attachment_container.dart diff --git a/packages/stream_core_flutter/lib/src/components/message_composer.dart b/packages/stream_core_flutter/lib/src/components/message_composer.dart index 5cf1e94..9a681bc 100644 --- a/packages/stream_core_flutter/lib/src/components/message_composer.dart +++ b/packages/stream_core_flutter/lib/src/components/message_composer.dart @@ -1,3 +1,4 @@ +export 'message_composer/attachment/message_composer_attachment_container.dart'; export 'message_composer/attachment/message_composer_file_attachment.dart'; export 'message_composer/attachment/message_composer_link_preview_attachment.dart'; export 'message_composer/attachment/message_composer_media_file_attachment.dart'; diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_attachment_container.dart b/packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_attachment_container.dart new file mode 100644 index 0000000..570bcbd --- /dev/null +++ b/packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_attachment_container.dart @@ -0,0 +1,50 @@ +import 'package:flutter/widgets.dart'; + +import '../../../../stream_core_flutter.dart'; + +class StreamMessageComposerAttachmentContainer extends StatelessWidget { + const StreamMessageComposerAttachmentContainer({ + super.key, + required this.child, + this.onRemovePressed, + this.backgroundColor, + this.borderColor, + this.padding, + }); + + final Widget child; + final VoidCallback? onRemovePressed; + final Color? backgroundColor; + final Color? borderColor; + final EdgeInsets? padding; + + @override + Widget build(BuildContext context) { + final spacing = context.streamSpacing; + + return Stack( + children: [ + Container( + margin: EdgeInsets.all(spacing.xxs), + padding: padding ?? EdgeInsets.all(spacing.xs), + foregroundDecoration: borderColor != null + ? BoxDecoration( + borderRadius: BorderRadius.all(context.streamRadius.lg), + border: Border.all(color: borderColor!), + ) + : null, + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.all(context.streamRadius.lg), + ), + child: child, + ), + if (onRemovePressed case final VoidCallback onRemovePressed?) + Align( + alignment: Alignment.topRight, + child: StreamRemoveControl(onPressed: onRemovePressed), + ), + ], + ); + } +} diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_file_attachment.dart b/packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_file_attachment.dart index f5f2ef0..e3798b5 100644 --- a/packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_file_attachment.dart +++ b/packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_file_attachment.dart @@ -19,53 +19,35 @@ class MessageComposerFileAttachment extends StatelessWidget { @override Widget build(BuildContext context) { final textColor = context.streamColorScheme.textPrimary; - final titleStyle = context.streamTextTheme.captionEmphasis.copyWith(color: textColor); - final spacing = context.streamSpacing; - return Stack( - children: [ - Container( - margin: EdgeInsets.all(spacing.xxs), - padding: EdgeInsets.fromLTRB(spacing.md, spacing.md, spacing.sm, spacing.md), - foregroundDecoration: BoxDecoration( - borderRadius: BorderRadius.all(context.streamRadius.lg), - border: Border.all( - color: context.streamColorScheme.borderDefault, + + return StreamMessageComposerAttachmentContainer( + padding: EdgeInsets.fromLTRB(spacing.md, spacing.md, spacing.sm, spacing.md), + onRemovePressed: onRemovePressed, + borderColor: context.streamColorScheme.borderDefault, + child: Row( + children: [ + ?fileTypeIcon, + SizedBox(width: spacing.xs), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (title case final title?) + Text( + title, + style: titleStyle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ?subtitle, + ], ), ), - decoration: BoxDecoration( - borderRadius: BorderRadius.all(context.streamRadius.lg), - ), - child: Row( - children: [ - ?fileTypeIcon, - SizedBox(width: spacing.xs), - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (title case final title?) - Text( - title, - style: titleStyle, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ?subtitle, - ], - ), - ), - ], - ), - ), - if (onRemovePressed case final VoidCallback onRemovePressed?) - Align( - alignment: Alignment.topRight, - child: StreamRemoveControl(onPressed: onRemovePressed), - ), - ], + ], + ), ); } } diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_link_preview_attachment.dart b/packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_link_preview_attachment.dart index cde9dbc..13eac27 100644 --- a/packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_link_preview_attachment.dart +++ b/packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_link_preview_attachment.dart @@ -28,75 +28,62 @@ class MessageComposerLinkPreviewAttachment extends StatelessWidget { final bodyStyle = context.streamTextTheme.metadataDefault.copyWith(color: textColor); final spacing = context.streamSpacing; - return Stack( - children: [ - Container( - margin: EdgeInsets.all(spacing.xxs), - padding: EdgeInsets.all(spacing.xs), - decoration: BoxDecoration( - color: backgroundColor, - borderRadius: BorderRadius.all(context.streamRadius.lg), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (image != null) ...[ - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - borderRadius: BorderRadius.all(context.streamRadius.md), - image: DecorationImage(image: image!, fit: BoxFit.cover), + return StreamMessageComposerAttachmentContainer( + onRemovePressed: onRemovePressed, + backgroundColor: backgroundColor, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (image != null) ...[ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(context.streamRadius.md), + image: DecorationImage(image: image!, fit: BoxFit.cover), + ), + ), + SizedBox(width: spacing.xs), + ], + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (title case final title?) + Text( + title, + style: titleStyle, + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - ), - SizedBox(width: spacing.xs), - ], - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (title case final title?) - Text( - title, - style: titleStyle, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - if (subtitle case final subtitle?) - Text( - subtitle, - style: bodyStyle, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - if (url case final url?) - Row( - children: [ - Icon(context.streamIcons.chainLink3, size: 12), - SizedBox(width: spacing.xxs), - Expanded( - child: Text( - url, - style: bodyStyle, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ], + if (subtitle case final subtitle?) + Text( + subtitle, + style: bodyStyle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (url case final url?) + Row( + children: [ + Icon(context.streamIcons.chainLink3, size: 12), + SizedBox(width: spacing.xxs), + Expanded( + child: Text( + url, + style: bodyStyle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), ), - ], - ), - ), - ], - ), - ), - if (onRemovePressed case final VoidCallback onRemovePressed?) - Align( - alignment: Alignment.topRight, - child: StreamRemoveControl(onPressed: onRemovePressed), + ], + ), + ], + ), ), - ], + ], + ), ); } } diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_reply_attachment.dart b/packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_reply_attachment.dart index 3a298b7..a6d7a31 100644 --- a/packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_reply_attachment.dart +++ b/packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_reply_attachment.dart @@ -32,72 +32,59 @@ class MessageComposerReplyAttachment extends StatelessWidget { final textColor = messageStyle?.textColor; final spacing = context.streamSpacing; - return Stack( - children: [ - Container( - margin: EdgeInsets.all(spacing.xxs), - padding: EdgeInsets.all(spacing.xs), - decoration: BoxDecoration( - color: backgroundColor, - borderRadius: BorderRadius.all(context.streamRadius.lg), - ), - child: IntrinsicHeight( - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - margin: const EdgeInsets.only(top: 2, bottom: 2), - color: indicatorColor, - child: const SizedBox( - width: 2, - height: double.infinity, - ), - ), - SizedBox(width: spacing.xs), - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + return StreamMessageComposerAttachmentContainer( + onRemovePressed: onRemovePressed, + backgroundColor: backgroundColor, + child: IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + margin: const EdgeInsets.only(top: 2, bottom: 2), + color: indicatorColor, + child: const SizedBox( + width: 2, + height: double.infinity, + ), + ), + SizedBox(width: spacing.xs), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: context.streamTextTheme.metadataEmphasis.copyWith(color: textColor)), + Row( children: [ - Text(title, style: context.streamTextTheme.metadataEmphasis.copyWith(color: textColor)), - Row( - children: [ - if (image != null) ...[ - Icon(context.streamIcons.camera1, size: 12), - SizedBox(width: spacing.xxs), - ], - Expanded( - child: Text( - subtitle, - style: context.streamTextTheme.metadataDefault.copyWith(color: textColor), - ), - ), - ], + if (image != null) ...[ + Icon(context.streamIcons.camera1, size: 12), + SizedBox(width: spacing.xxs), + ], + Expanded( + child: Text( + subtitle, + style: context.streamTextTheme.metadataDefault.copyWith(color: textColor), + ), ), ], ), - ), - if (image != null) ...[ - SizedBox(width: spacing.xs), - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - borderRadius: BorderRadius.all(context.streamRadius.md), - image: DecorationImage(image: image!, fit: BoxFit.cover), - ), - ), ], - ], + ), ), - ), + if (image != null) ...[ + SizedBox(width: spacing.xs), + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(context.streamRadius.md), + image: DecorationImage(image: image!, fit: BoxFit.cover), + ), + ), + ], + ], ), - if (onRemovePressed case final VoidCallback onRemovePressed?) - Align( - alignment: Alignment.topRight, - child: StreamRemoveControl(onPressed: onRemovePressed), - ), - ], + ), ); } } From d38b39269db583a20e98e30eb6da97c5a4540023 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 25 Feb 2026 11:45:28 +0100 Subject: [PATCH 2/5] add audio waveform --- .../accessories/stream_audio_waveform.dart | 496 ++++++++++++++++++ .../stream_core_flutter/lib/src/theme.dart | 1 + .../stream_audio_waveform_theme.dart | 163 ++++++ .../stream_audio_waveform_theme.g.theme.dart | 130 +++++ .../lib/src/theme/stream_theme.dart | 9 + .../lib/src/theme/stream_theme.g.theme.dart | 13 +- .../src/theme/stream_theme_extensions.dart | 4 + packages/stream_core_flutter/pubspec.yaml | 1 + 8 files changed, 815 insertions(+), 2 deletions(-) create mode 100644 packages/stream_core_flutter/lib/src/components/accessories/stream_audio_waveform.dart create mode 100644 packages/stream_core_flutter/lib/src/theme/components/stream_audio_waveform_theme.dart create mode 100644 packages/stream_core_flutter/lib/src/theme/components/stream_audio_waveform_theme.g.theme.dart diff --git a/packages/stream_core_flutter/lib/src/components/accessories/stream_audio_waveform.dart b/packages/stream_core_flutter/lib/src/components/accessories/stream_audio_waveform.dart new file mode 100644 index 0000000..3f23f3e --- /dev/null +++ b/packages/stream_core_flutter/lib/src/components/accessories/stream_audio_waveform.dart @@ -0,0 +1,496 @@ +import 'dart:math' as math; +import 'dart:ui'; + +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; + +import '../../theme/components/stream_audio_waveform_theme.dart'; +import '../../theme/stream_theme_extensions.dart'; + +const _kAudioWaveformSliderThumbWidth = 12.0; + +/// {@template streamAudioWaveformSlider} +/// A widget that displays an audio waveform and allows the user to interact +/// with it using a slider. +/// {@endtemplate} +class StreamAudioWaveformSlider extends StatefulWidget { + /// {@macro streamAudioWaveformSlider} + const StreamAudioWaveformSlider({ + super.key, + required this.waveform, + this.onChangeStart, + required this.onChanged, + this.onChangeEnd, + this.limit = 100, + this.color, + this.progress = 0, + this.progressColor, + this.minBarHeight, + this.spacingRatio, + this.heightScale, + this.inverse = true, + this.isActive = false, + this.activeThumbColor, + this.idleThumbColor, + this.thumbBorderColor, + }); + + /// The waveform data to be drawn. + /// + /// Note: The values should be between 0 and 1. + final List waveform; + + /// Called when the thumb starts being dragged. + final ValueChanged? onChangeStart; + + /// Called while the thumb is being dragged. + final ValueChanged? onChanged; + + /// Called when the thumb stops being dragged. + final ValueChanged? onChangeEnd; + + /// The color of the wave bars. + /// + /// Defaults to [StreamAudioWaveformThemeData.color]. + final Color? color; + + /// The number of wave bars that will be draw in the screen. When the length + /// of [waveform] is bigger than [limit] only the X last bars will be shown. + /// + /// Defaults to 100. + final int limit; + + /// The progress of the audio track. Used to show the progress of the audio. + /// + /// Defaults to 0. + final double progress; + + /// The color of the progressed wave bars. + /// + /// Defaults to [StreamAudioWaveformThemeData.progressColor]. + final Color? progressColor; + + /// The minimum height of the bars. + /// + /// Defaults to [StreamAudioWaveformThemeData.minBarHeight]. + final double? minBarHeight; + + /// The ratio of the spacing between the bars. + /// + /// Defaults to [StreamAudioWaveformThemeData.spacingRatio]. + final double? spacingRatio; + + /// The scale of the height of the bars. + /// + /// Defaults to [StreamAudioWaveformThemeData.heightScale]. + final double? heightScale; + + /// If true, the bars grow from right to left otherwise they grow from left + /// to right. + /// + /// Defaults to true. + final bool inverse; + + /// Whether the waveform slider is in an active (playing) state. + /// + /// When true, the thumb uses [activeThumbColor]. When false, the thumb + /// uses [idleThumbColor]. + /// + /// Defaults to false. + final bool isActive; + + /// The color of the slider thumb when [isActive] is true. + /// + /// Defaults to [StreamAudioWaveformThemeData.activeThumbColor]. + final Color? activeThumbColor; + + /// The color of the slider thumb when [isActive] is false. + /// + /// Defaults to [StreamAudioWaveformThemeData.idleThumbColor]. + final Color? idleThumbColor; + + /// The color of the slider thumb border. + /// + /// Defaults to [StreamAudioWaveformThemeData.thumbBorderColor]. + final Color? thumbBorderColor; + + @override + State createState() => _StreamAudioWaveformSliderState(); +} + +class _StreamAudioWaveformSliderState extends State { + @override + Widget build(BuildContext context) { + final theme = StreamAudioWaveformTheme.of(context); + final colorScheme = context.streamColorScheme; + + final activeThumbColor = widget.activeThumbColor ?? theme.activeThumbColor ?? colorScheme.accentPrimary; + final idleThumbColor = widget.idleThumbColor ?? theme.idleThumbColor ?? colorScheme.accentNeutral; + final thumbColor = widget.isActive ? activeThumbColor : idleThumbColor; + final thumbBorderColor = widget.thumbBorderColor ?? theme.thumbBorderColor ?? colorScheme.borderOnAccent; + + return HorizontalSlider( + onChangeStart: widget.onChangeStart, + onChanged: widget.onChanged, + onChangeEnd: widget.onChangeEnd, + child: LayoutBuilder( + builder: (context, constraints) => Stack( + fit: StackFit.expand, + clipBehavior: Clip.none, + alignment: Alignment.center, + children: [ + StreamAudioWaveform( + waveform: widget.waveform, + limit: widget.limit, + color: widget.color, + progress: widget.progress, + progressColor: widget.progressColor, + minBarHeight: widget.minBarHeight, + spacingRatio: widget.spacingRatio, + heightScale: widget.heightScale, + inverse: widget.inverse, + ), + Builder( + // Just using it for the calculation of the thumb position. + builder: (context) { + final progressWidth = constraints.maxWidth * widget.progress; + return AnimatedPositioned( + curve: const ElasticOutCurve(1.05), + duration: const Duration(milliseconds: 300), + left: progressWidth - _kAudioWaveformSliderThumbWidth / 2, + child: StreamAudioWaveformSliderThumb( + color: thumbColor, + borderColor: thumbBorderColor, + ), + ); + }, + ), + ], + ), + ), + ); + } +} + +/// {@template streamAudioWaveformSliderThumb} +/// A widget that represents the thumb of the [StreamAudioWaveformSlider]. +/// {@endtemplate} +class StreamAudioWaveformSliderThumb extends StatelessWidget { + /// {@macro streamAudioWaveformSliderThumb} + const StreamAudioWaveformSliderThumb({ + super.key, + this.size = _kAudioWaveformSliderThumbWidth, + this.color = Colors.white, + this.borderColor = const Color(0xffecebeb), + }); + + /// The width of the thumb. + final double size; + + /// The color of the thumb. + final Color color; + + /// The border color of the thumb. + final Color borderColor; + + @override + Widget build(BuildContext context) { + return Container( + width: size, + height: size, + foregroundDecoration: BoxDecoration( + color: color, + border: Border.all( + color: borderColor, + strokeAlign: BorderSide.strokeAlignCenter, + width: 2, + ), + shape: BoxShape.circle, + ), + ); + } +} + +/// {@template streamAudioWaveform} +/// A widget that displays an audio waveform. +/// +/// The waveform is drawn using the [waveform] data. The waveform is drawn +/// horizontally and the bars grow from right to left. +/// {@endtemplate} +class StreamAudioWaveform extends StatelessWidget { + /// {@macro streamAudioWaveform} + const StreamAudioWaveform({ + super.key, + required this.waveform, + this.limit = 100, + this.color, + this.progress = 0, + this.progressColor, + this.minBarHeight, + this.spacingRatio, + this.heightScale, + this.inverse = true, + }); + + /// The waveform data to be drawn. + /// + /// Note: The values should be between 0 and 1. + final List waveform; + + /// The color of the wave bars. + /// + /// Defaults to [StreamAudioWaveformThemeData.color]. + final Color? color; + + /// The number of wave bars that will be drawn on the screen. When the length + /// of [waveform] is bigger than [limit] only the last [limit] bars will be + /// shown. + /// + /// Defaults to 100. + final int limit; + + /// The progress of the audio track. Used to show the progress of the audio. + /// + /// Defaults to 0. + final double progress; + + /// The color of the progressed wave bars. + /// + /// Defaults to [StreamAudioWaveformThemeData.progressColor]. + final Color? progressColor; + + /// The minimum height of the bars. + /// + /// Defaults to [StreamAudioWaveformThemeData.minBarHeight]. + final double? minBarHeight; + + /// The ratio of the spacing between the bars. + /// + /// Defaults to [StreamAudioWaveformThemeData.spacingRatio]. + final double? spacingRatio; + + /// The scale of the height of the bars. + /// + /// Defaults to [StreamAudioWaveformThemeData.heightScale]. + final double? heightScale; + + /// If true, the bars grow from right to left otherwise they grow from left + /// to right. + /// + /// Defaults to true. + final bool inverse; + + @override + Widget build(BuildContext context) { + final theme = StreamAudioWaveformTheme.of(context); + final colorScheme = context.streamColorScheme; + + final color = this.color ?? theme.color ?? colorScheme.borderOpacity25; + final progressColor = this.progressColor ?? theme.progressColor ?? colorScheme.accentPrimary; + final minBarHeight = this.minBarHeight ?? theme.minBarHeight ?? 2.0; + final spacingRatio = this.spacingRatio ?? theme.spacingRatio ?? 0.3; + final heightScale = this.heightScale ?? theme.heightScale ?? 1.0; + + return CustomPaint( + willChange: true, + painter: _WaveformPainter( + waveform: waveform.reversed, + limit: limit, + color: color, + progress: progress, + progressColor: progressColor, + minBarHeight: minBarHeight, + spacingRatio: spacingRatio, + heightScale: heightScale, + inverse: inverse, + ), + ); + } +} + +class _WaveformPainter extends CustomPainter { + _WaveformPainter({ + required Iterable waveform, + this.limit = 100, + this.color = const Color(0xff7E828B), + this.progress = 0, + this.progressColor = const Color(0xff005FFF), + this.minBarHeight = 2, + double spacingRatio = 0.3, + this.heightScale = 1, + this.inverse = true, + }) : waveform = [ + ...waveform.take(limit), + if (waveform.length < limit) + // Fill the remaining bars with 0 value + ...List.filled(limit - waveform.length, 0), + ], + spacingRatio = spacingRatio.clamp(0, 1); + + final List waveform; + final Color color; + final int limit; + final double progress; + final Color progressColor; + final double minBarHeight; + final double spacingRatio; + final bool inverse; + final double heightScale; + + @override + void paint(Canvas canvas, Size size) { + final canvasWidth = size.width; + final canvasHeight = size.height; + + // The total spacing between the bars in the canvas. + final spacingWidth = canvasWidth * spacingRatio; + final barsWidth = canvasWidth - spacingWidth; + final barWidth = barsWidth / limit; + final barSpacing = spacingWidth / (limit - 1); + final progressWidth = progress * canvasWidth; + + void _paintBar(int index, double barValue) { + var dx = index * (barWidth + barSpacing) + barWidth / 2; + if (inverse) dx = canvasWidth - dx; + final dy = canvasHeight / 2; + + final barHeight = math.max(barValue * canvasHeight, minBarHeight); + + final rect = RRect.fromRectAndRadius( + Rect.fromCenter( + center: Offset(dx, dy), + width: barWidth, + height: barHeight, + ), + const Radius.circular(2), + ); + + final waveColor = switch (dx <= progressWidth) { + true => progressColor, + false => color, + }; + + final wavePaint = Paint() + ..color = waveColor + ..strokeCap = StrokeCap.round; + + canvas.drawRRect(rect, wavePaint); + } + + // Paint all the bars + waveform.forEachIndexed(_paintBar); + } + + @override + bool shouldRepaint(covariant _WaveformPainter oldDelegate) => + !const ListEquality().equals(waveform, oldDelegate.waveform) || + color != oldDelegate.color || + limit != oldDelegate.limit || + progress != oldDelegate.progress || + progressColor != oldDelegate.progressColor || + minBarHeight != oldDelegate.minBarHeight || + spacingRatio != oldDelegate.spacingRatio || + heightScale != oldDelegate.heightScale || + inverse != oldDelegate.inverse; +} + +/// {@template horizontalSlider} +/// A widget that allows interactive horizontal sliding gestures. +/// +/// The `HorizontalSlider` widget wraps a child widget and allows users to +/// perform sliding gestures horizontally. It can be configured with callbacks +/// to notify the parent widget about the changes in the horizontal value. +/// {@endtemplate} +class HorizontalSlider extends StatefulWidget { + /// Creates a horizontal slider. + const HorizontalSlider({ + super.key, + required this.child, + required this.onChanged, + this.onChangeStart, + this.onChangeEnd, + }); + + /// The child widget wrapped by the slider. + final Widget child; + + /// Called when the horizontal value starts changing. + final ValueChanged? onChangeStart; + + /// Called when the horizontal value changes. + final ValueChanged? onChanged; + + /// Called when the horizontal value stops changing. + final ValueChanged? onChangeEnd; + + @override + State createState() => _HorizontalSliderState(); +} + +class _HorizontalSliderState extends State { + bool _active = false; + + /// Returns true if the slider is interactive. + bool get isInteractive => widget.onChanged != null; + + /// Converts the visual position to a value based on the text direction. + double _getValueFromVisualPosition(double visualPosition) { + final textDirection = Directionality.of(context); + final value = switch (textDirection) { + TextDirection.rtl => 1.0 - visualPosition, + TextDirection.ltr => visualPosition, + }; + + return clampDouble(value, 0, 1); + } + + /// Converts the local position to a horizontal value. + double _getValueFromLocalPosition(Offset globalPosition) { + final box = context.findRenderObject()! as RenderBox; + final localPosition = box.globalToLocal(globalPosition); + final visualPosition = localPosition.dx / box.size.width; + return _getValueFromVisualPosition(visualPosition); + } + + void _handleDragStart(DragStartDetails details) { + if (!_active && isInteractive) { + _active = true; + final value = _getValueFromLocalPosition(details.globalPosition); + widget.onChangeStart?.call(value); + } + } + + void _handleDragUpdate(DragUpdateDetails details) { + _handleHorizontalDrag(details.globalPosition); + } + + void _handleDragEnd(DragEndDetails details) { + if (!mounted) return; + + if (_active && mounted) { + final value = _getValueFromLocalPosition(details.globalPosition); + widget.onChangeEnd?.call(value); + _active = false; + } + } + + /// Handles the sliding gesture. + void _handleHorizontalDrag(Offset globalPosition) { + if (!mounted) return; + + if (isInteractive) { + final value = _getValueFromLocalPosition(globalPosition); + widget.onChanged?.call(value); + } + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onHorizontalDragStart: _handleDragStart, + onHorizontalDragUpdate: _handleDragUpdate, + onHorizontalDragEnd: _handleDragEnd, + child: widget.child, + ); + } +} diff --git a/packages/stream_core_flutter/lib/src/theme.dart b/packages/stream_core_flutter/lib/src/theme.dart index 78452e7..688f380 100644 --- a/packages/stream_core_flutter/lib/src/theme.dart +++ b/packages/stream_core_flutter/lib/src/theme.dart @@ -1,5 +1,6 @@ export 'factory/stream_component_factory.dart'; +export 'theme/components/stream_audio_waveform_theme.dart'; export 'theme/components/stream_avatar_theme.dart'; export 'theme/components/stream_badge_count_theme.dart'; export 'theme/components/stream_button_theme.dart'; diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_audio_waveform_theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_audio_waveform_theme.dart new file mode 100644 index 0000000..c74c4f5 --- /dev/null +++ b/packages/stream_core_flutter/lib/src/theme/components/stream_audio_waveform_theme.dart @@ -0,0 +1,163 @@ +import 'package:flutter/widgets.dart'; +import 'package:theme_extensions_builder_annotation/theme_extensions_builder_annotation.dart'; + +import '../stream_theme.dart'; + +part 'stream_audio_waveform_theme.g.theme.dart'; + +/// Applies an audio waveform theme to descendant [StreamAudioWaveform] and +/// [StreamAudioWaveformSlider] widgets. +/// +/// Wrap a subtree with [StreamAudioWaveformTheme] to override waveform +/// styling. Access the merged theme using +/// [BuildContext.streamAudioWaveformTheme]. +/// +/// {@tool snippet} +/// +/// Override waveform colors for a specific section: +/// +/// ```dart +/// StreamAudioWaveformTheme( +/// data: StreamAudioWaveformThemeData( +/// color: Colors.grey, +/// progressColor: Colors.blue, +/// activeThumbColor: Colors.blue, +/// idleThumbColor: Colors.grey, +/// ), +/// child: StreamAudioWaveformSlider( +/// waveform: waveformData, +/// onChanged: (value) {}, +/// ), +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [StreamAudioWaveformThemeData], which describes the waveform theme. +/// * [StreamAudioWaveform], the waveform widget affected by this theme. +/// * [StreamAudioWaveformSlider], the slider widget affected by this theme. +class StreamAudioWaveformTheme extends InheritedTheme { + /// Creates an audio waveform theme that controls descendant waveforms. + const StreamAudioWaveformTheme({ + super.key, + required this.data, + required super.child, + }); + + /// The audio waveform theme data for descendant widgets. + final StreamAudioWaveformThemeData data; + + /// Returns the [StreamAudioWaveformThemeData] merged from local and global + /// themes. + /// + /// Local values from the nearest [StreamAudioWaveformTheme] ancestor take + /// precedence over global values from [StreamTheme.of]. + /// + /// This allows partial overrides - for example, overriding only + /// [StreamAudioWaveformThemeData.color] while inheriting other properties + /// from the global theme. + static StreamAudioWaveformThemeData of(BuildContext context) { + final localTheme = context.dependOnInheritedWidgetOfExactType(); + return StreamTheme.of(context).audioWaveformTheme.merge(localTheme?.data); + } + + @override + Widget wrap(BuildContext context, Widget child) { + return StreamAudioWaveformTheme(data: data, child: child); + } + + @override + bool updateShouldNotify(StreamAudioWaveformTheme oldWidget) => data != oldWidget.data; +} + +/// Theme data for customizing [StreamAudioWaveform] and +/// [StreamAudioWaveformSlider] widgets. +/// +/// {@tool snippet} +/// +/// Customize waveform appearance globally: +/// +/// ```dart +/// StreamTheme( +/// audioWaveformTheme: StreamAudioWaveformThemeData( +/// color: Colors.grey, +/// progressColor: Colors.blue, +/// minBarHeight: 2, +/// spacingRatio: 0.3, +/// heightScale: 1, +/// activeThumbColor: Colors.blue, +/// idleThumbColor: Colors.grey, +/// thumbBorderColor: Colors.grey, +/// ), +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [StreamAudioWaveform], the widget that uses this theme data. +/// * [StreamAudioWaveformSlider], the slider widget that uses this theme data. +/// * [StreamAudioWaveformTheme], for overriding theme in a widget subtree. +@themeGen +@immutable +class StreamAudioWaveformThemeData with _$StreamAudioWaveformThemeData { + /// Creates audio waveform theme data with optional style overrides. + const StreamAudioWaveformThemeData({ + this.color, + this.progressColor, + this.minBarHeight, + this.spacingRatio, + this.heightScale, + this.activeThumbColor, + this.idleThumbColor, + this.thumbBorderColor, + }); + + /// The color of the waveform bars. + /// + /// Falls back to [StreamColorScheme.borderOpacity25]. + final Color? color; + + /// The color of the progressed waveform bars. + /// + /// Falls back to [StreamColorScheme.accentPrimary]. + final Color? progressColor; + + /// The minimum height of the waveform bars. + /// + /// Falls back to 2 logical pixels. + final double? minBarHeight; + + /// The ratio of the spacing between the waveform bars. + /// + /// Falls back to 0.3. + final double? spacingRatio; + + /// The scale of the height of the waveform bars. + /// + /// Falls back to 1. + final double? heightScale; + + /// The color of the slider thumb when the waveform is active (playing). + /// + /// Falls back to [StreamColorScheme.accentPrimary]. + final Color? activeThumbColor; + + /// The color of the slider thumb when the waveform is idle (not playing). + /// + /// Falls back to [StreamColorScheme.accentNeutral]. + final Color? idleThumbColor; + + /// The border color of the slider thumb. + /// + /// Falls back to [StreamColorScheme.borderOnAccent]. + final Color? thumbBorderColor; + + /// Linearly interpolate between two [StreamAudioWaveformThemeData] objects. + static StreamAudioWaveformThemeData? lerp( + StreamAudioWaveformThemeData? a, + StreamAudioWaveformThemeData? b, + double t, + ) => _$StreamAudioWaveformThemeData.lerp(a, b, t); +} diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_audio_waveform_theme.g.theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_audio_waveform_theme.g.theme.dart new file mode 100644 index 0000000..c335d3c --- /dev/null +++ b/packages/stream_core_flutter/lib/src/theme/components/stream_audio_waveform_theme.g.theme.dart @@ -0,0 +1,130 @@ +// dart format width=80 +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, unused_element + +part of 'stream_audio_waveform_theme.dart'; + +// ************************************************************************** +// ThemeGenGenerator +// ************************************************************************** + +mixin _$StreamAudioWaveformThemeData { + bool get canMerge => true; + + static StreamAudioWaveformThemeData? lerp( + StreamAudioWaveformThemeData? a, + StreamAudioWaveformThemeData? b, + double t, + ) { + if (identical(a, b)) { + return a; + } + + if (a == null) { + return t == 1.0 ? b : null; + } + + if (b == null) { + return t == 0.0 ? a : null; + } + + return StreamAudioWaveformThemeData( + color: Color.lerp(a.color, b.color, t), + progressColor: Color.lerp(a.progressColor, b.progressColor, t), + minBarHeight: lerpDouble$(a.minBarHeight, b.minBarHeight, t), + spacingRatio: lerpDouble$(a.spacingRatio, b.spacingRatio, t), + heightScale: lerpDouble$(a.heightScale, b.heightScale, t), + activeThumbColor: Color.lerp(a.activeThumbColor, b.activeThumbColor, t), + idleThumbColor: Color.lerp(a.idleThumbColor, b.idleThumbColor, t), + thumbBorderColor: Color.lerp(a.thumbBorderColor, b.thumbBorderColor, t), + ); + } + + StreamAudioWaveformThemeData copyWith({ + Color? color, + Color? progressColor, + double? minBarHeight, + double? spacingRatio, + double? heightScale, + Color? activeThumbColor, + Color? idleThumbColor, + Color? thumbBorderColor, + }) { + final _this = (this as StreamAudioWaveformThemeData); + + return StreamAudioWaveformThemeData( + color: color ?? _this.color, + progressColor: progressColor ?? _this.progressColor, + minBarHeight: minBarHeight ?? _this.minBarHeight, + spacingRatio: spacingRatio ?? _this.spacingRatio, + heightScale: heightScale ?? _this.heightScale, + activeThumbColor: activeThumbColor ?? _this.activeThumbColor, + idleThumbColor: idleThumbColor ?? _this.idleThumbColor, + thumbBorderColor: thumbBorderColor ?? _this.thumbBorderColor, + ); + } + + StreamAudioWaveformThemeData merge(StreamAudioWaveformThemeData? other) { + final _this = (this as StreamAudioWaveformThemeData); + + if (other == null || identical(_this, other)) { + return _this; + } + + if (!other.canMerge) { + return other; + } + + return copyWith( + color: other.color, + progressColor: other.progressColor, + minBarHeight: other.minBarHeight, + spacingRatio: other.spacingRatio, + heightScale: other.heightScale, + activeThumbColor: other.activeThumbColor, + idleThumbColor: other.idleThumbColor, + thumbBorderColor: other.thumbBorderColor, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + + if (other.runtimeType != runtimeType) { + return false; + } + + final _this = (this as StreamAudioWaveformThemeData); + final _other = (other as StreamAudioWaveformThemeData); + + return _other.color == _this.color && + _other.progressColor == _this.progressColor && + _other.minBarHeight == _this.minBarHeight && + _other.spacingRatio == _this.spacingRatio && + _other.heightScale == _this.heightScale && + _other.activeThumbColor == _this.activeThumbColor && + _other.idleThumbColor == _this.idleThumbColor && + _other.thumbBorderColor == _this.thumbBorderColor; + } + + @override + int get hashCode { + final _this = (this as StreamAudioWaveformThemeData); + + return Object.hash( + runtimeType, + _this.color, + _this.progressColor, + _this.minBarHeight, + _this.spacingRatio, + _this.heightScale, + _this.activeThumbColor, + _this.idleThumbColor, + _this.thumbBorderColor, + ); + } +} diff --git a/packages/stream_core_flutter/lib/src/theme/stream_theme.dart b/packages/stream_core_flutter/lib/src/theme/stream_theme.dart index 3870084..ef74552 100644 --- a/packages/stream_core_flutter/lib/src/theme/stream_theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/stream_theme.dart @@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:theme_extensions_builder_annotation/theme_extensions_builder_annotation.dart'; +import 'components/stream_audio_waveform_theme.dart'; import 'components/stream_avatar_theme.dart'; import 'components/stream_badge_count_theme.dart'; import 'components/stream_button_theme.dart'; @@ -89,6 +90,7 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { StreamTextTheme? textTheme, StreamBoxShadow? boxShadow, // Components themes + StreamAudioWaveformThemeData? audioWaveformTheme, StreamAvatarThemeData? avatarTheme, StreamBadgeCountThemeData? badgeCountTheme, StreamButtonThemeData? buttonTheme, @@ -116,6 +118,7 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { boxShadow ??= isDark ? StreamBoxShadow.dark() : StreamBoxShadow.light(); // Components + audioWaveformTheme ??= const StreamAudioWaveformThemeData(); avatarTheme ??= const StreamAvatarThemeData(); badgeCountTheme ??= const StreamBadgeCountThemeData(); buttonTheme ??= const StreamButtonThemeData(); @@ -137,6 +140,7 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { colorScheme: colorScheme, textTheme: textTheme, boxShadow: boxShadow, + audioWaveformTheme: audioWaveformTheme, avatarTheme: avatarTheme, badgeCountTheme: badgeCountTheme, buttonTheme: buttonTheme, @@ -172,6 +176,7 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { required this.colorScheme, required this.textTheme, required this.boxShadow, + required this.audioWaveformTheme, required this.avatarTheme, required this.badgeCountTheme, required this.buttonTheme, @@ -243,6 +248,9 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { /// The box shadow (elevation) values for this theme. final StreamBoxShadow boxShadow; + /// The audio waveform theme for this theme. + final StreamAudioWaveformThemeData audioWaveformTheme; + /// The avatar theme for this theme. final StreamAvatarThemeData avatarTheme; @@ -305,6 +313,7 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { colorScheme: colorScheme, textTheme: newTextTheme, boxShadow: boxShadow, + audioWaveformTheme: audioWaveformTheme, avatarTheme: avatarTheme, badgeCountTheme: badgeCountTheme, buttonTheme: buttonTheme, diff --git a/packages/stream_core_flutter/lib/src/theme/stream_theme.g.theme.dart b/packages/stream_core_flutter/lib/src/theme/stream_theme.g.theme.dart index 5af58e2..f6742f5 100644 --- a/packages/stream_core_flutter/lib/src/theme/stream_theme.g.theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/stream_theme.g.theme.dart @@ -20,6 +20,7 @@ mixin _$StreamTheme on ThemeExtension { StreamColorScheme? colorScheme, StreamTextTheme? textTheme, StreamBoxShadow? boxShadow, + StreamAudioWaveformThemeData? audioWaveformTheme, StreamAvatarThemeData? avatarTheme, StreamBadgeCountThemeData? badgeCountTheme, StreamButtonThemeData? buttonTheme, @@ -43,6 +44,7 @@ mixin _$StreamTheme on ThemeExtension { colorScheme: colorScheme ?? _this.colorScheme, textTheme: textTheme ?? _this.textTheme, boxShadow: boxShadow ?? _this.boxShadow, + audioWaveformTheme: audioWaveformTheme ?? _this.audioWaveformTheme, avatarTheme: avatarTheme ?? _this.avatarTheme, badgeCountTheme: badgeCountTheme ?? _this.badgeCountTheme, buttonTheme: buttonTheme ?? _this.buttonTheme, @@ -79,6 +81,11 @@ mixin _$StreamTheme on ThemeExtension { (_this.colorScheme.lerp(other.colorScheme, t) as StreamColorScheme), textTheme: (_this.textTheme.lerp(other.textTheme, t) as StreamTextTheme), boxShadow: StreamBoxShadow.lerp(_this.boxShadow, other.boxShadow, t)!, + audioWaveformTheme: StreamAudioWaveformThemeData.lerp( + _this.audioWaveformTheme, + other.audioWaveformTheme, + t, + )!, avatarTheme: StreamAvatarThemeData.lerp( _this.avatarTheme, other.avatarTheme, @@ -150,6 +157,7 @@ mixin _$StreamTheme on ThemeExtension { _other.colorScheme == _this.colorScheme && _other.textTheme == _this.textTheme && _other.boxShadow == _this.boxShadow && + _other.audioWaveformTheme == _this.audioWaveformTheme && _other.avatarTheme == _this.avatarTheme && _other.badgeCountTheme == _this.badgeCountTheme && _other.buttonTheme == _this.buttonTheme && @@ -167,7 +175,7 @@ mixin _$StreamTheme on ThemeExtension { int get hashCode { final _this = (this as StreamTheme); - return Object.hash( + return Object.hashAll([ runtimeType, _this.brightness, _this.icons, @@ -177,6 +185,7 @@ mixin _$StreamTheme on ThemeExtension { _this.colorScheme, _this.textTheme, _this.boxShadow, + _this.audioWaveformTheme, _this.avatarTheme, _this.badgeCountTheme, _this.buttonTheme, @@ -188,6 +197,6 @@ mixin _$StreamTheme on ThemeExtension { _this.inputTheme, _this.onlineIndicatorTheme, _this.progressBarTheme, - ); + ]); } } diff --git a/packages/stream_core_flutter/lib/src/theme/stream_theme_extensions.dart b/packages/stream_core_flutter/lib/src/theme/stream_theme_extensions.dart index d6c414e..6149ec0 100644 --- a/packages/stream_core_flutter/lib/src/theme/stream_theme_extensions.dart +++ b/packages/stream_core_flutter/lib/src/theme/stream_theme_extensions.dart @@ -1,5 +1,6 @@ import 'package:flutter/widgets.dart'; +import 'components/stream_audio_waveform_theme.dart'; import 'components/stream_avatar_theme.dart'; import 'components/stream_badge_count_theme.dart'; import 'components/stream_button_theme.dart'; @@ -65,6 +66,9 @@ extension StreamThemeExtension on BuildContext { /// Returns the [StreamBoxShadow] from the current theme. StreamBoxShadow get streamBoxShadow => streamTheme.boxShadow; + /// Returns the [StreamAudioWaveformThemeData] from the nearest ancestor. + StreamAudioWaveformThemeData get streamAudioWaveformTheme => StreamAudioWaveformTheme.of(this); + /// Returns the [StreamAvatarThemeData] from the nearest ancestor. StreamAvatarThemeData get streamAvatarTheme => StreamAvatarTheme.of(this); diff --git a/packages/stream_core_flutter/pubspec.yaml b/packages/stream_core_flutter/pubspec.yaml index 0d19659..16b871d 100644 --- a/packages/stream_core_flutter/pubspec.yaml +++ b/packages/stream_core_flutter/pubspec.yaml @@ -9,6 +9,7 @@ environment: dependencies: cached_network_image: ^3.4.1 + collection: ^1.17.2 flutter: sdk: flutter flutter_svg: ^2.2.3 From 4e7e22b4b61e9b9569e2933407d49fff370e6bec Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 25 Feb 2026 13:22:34 +0100 Subject: [PATCH 3/5] Added waveform gallery page and tests --- .../lib/app/gallery_app.directories.g.dart | 19 + .../accessories/stream_audio_waveform.dart | 454 ++++++++++++++++++ .../lib/src/components.dart | 1 + packages/stream_core_flutter/pubspec.yaml | 2 +- .../stream_audio_waveform_golden_test.dart | 312 ++++++++++++ 5 files changed, 787 insertions(+), 1 deletion(-) create mode 100644 apps/design_system_gallery/lib/components/accessories/stream_audio_waveform.dart create mode 100644 packages/stream_core_flutter/test/components/accessories/stream_audio_waveform_golden_test.dart diff --git a/apps/design_system_gallery/lib/app/gallery_app.directories.g.dart b/apps/design_system_gallery/lib/app/gallery_app.directories.g.dart index acaa5b8..4fac9af 100644 --- a/apps/design_system_gallery/lib/app/gallery_app.directories.g.dart +++ b/apps/design_system_gallery/lib/app/gallery_app.directories.g.dart @@ -10,6 +10,8 @@ // ************************************************************************** // ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:design_system_gallery/components/accessories/stream_audio_waveform.dart' + as _design_system_gallery_components_accessories_stream_audio_waveform; import 'package:design_system_gallery/components/accessories/stream_emoji.dart' as _design_system_gallery_components_accessories_stream_emoji; import 'package:design_system_gallery/components/accessories/stream_file_type_icons.dart' @@ -170,6 +172,23 @@ final directories = <_widgetbook.WidgetbookNode>[ _widgetbook.WidgetbookFolder( name: 'Accessories', children: [ + _widgetbook.WidgetbookComponent( + name: 'StreamAudioWaveformSlider', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'Playground', + builder: + _design_system_gallery_components_accessories_stream_audio_waveform + .buildStreamAudioWaveformSliderPlayground, + ), + _widgetbook.WidgetbookUseCase( + name: 'Showcase', + builder: + _design_system_gallery_components_accessories_stream_audio_waveform + .buildStreamAudioWaveformSliderShowcase, + ), + ], + ), _widgetbook.WidgetbookComponent( name: 'StreamEmoji', useCases: [ diff --git a/apps/design_system_gallery/lib/components/accessories/stream_audio_waveform.dart b/apps/design_system_gallery/lib/components/accessories/stream_audio_waveform.dart new file mode 100644 index 0000000..c9c36b6 --- /dev/null +++ b/apps/design_system_gallery/lib/components/accessories/stream_audio_waveform.dart @@ -0,0 +1,454 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +// ============================================================================= +// Playground +// ============================================================================= + +@widgetbook.UseCase( + name: 'Playground', + type: StreamAudioWaveformSlider, + path: '[Components]/Accessories', +) +Widget buildStreamAudioWaveformSliderPlayground(BuildContext context) { + final isActive = context.knobs.boolean( + label: 'Is Active', + description: 'Whether the waveform is in the active (playing) state.', + ); + + final limit = context.knobs.double.slider( + label: 'Bar Limit', + min: 20, + max: 150, + initialValue: 100, + divisions: 13, + description: 'Maximum number of bars to display.', + ); + + final inverse = context.knobs.boolean( + label: 'Inverse', + initialValue: true, + description: 'If true, bars grow from right to left.', + ); + + return _SliderPlayground( + isActive: isActive, + limit: limit.toInt(), + inverse: inverse, + ); +} + +class _SliderPlayground extends StatefulWidget { + const _SliderPlayground({ + required this.isActive, + required this.limit, + required this.inverse, + }); + + final bool isActive; + final int limit; + final bool inverse; + + @override + State<_SliderPlayground> createState() => _SliderPlaygroundState(); +} + +class _SliderPlaygroundState extends State<_SliderPlayground> { + double _progress = 0.4; + + @override + Widget build(BuildContext context) { + final spacing = context.streamSpacing; + + return Center( + child: Padding( + padding: EdgeInsets.all(spacing.lg), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 300), + child: SizedBox( + height: 40, + child: StreamAudioWaveformSlider( + waveform: _sampleWaveform, + progress: _progress, + isActive: widget.isActive, + limit: widget.limit, + inverse: widget.inverse, + onChanged: (value) => setState(() => _progress = value), + ), + ), + ), + ), + ); + } +} + +// ============================================================================= +// Showcase +// ============================================================================= + +@widgetbook.UseCase( + name: 'Showcase', + type: StreamAudioWaveformSlider, + path: '[Components]/Accessories', +) +Widget buildStreamAudioWaveformSliderShowcase(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final spacing = context.streamSpacing; + + return DefaultTextStyle( + style: textTheme.bodyDefault.copyWith(color: colorScheme.textPrimary), + child: SingleChildScrollView( + padding: EdgeInsets.all(spacing.lg), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const _StatesSection(), + SizedBox(height: spacing.xl), + const _ProgressSection(), + SizedBox(height: spacing.xl), + const _WaveformOnlySection(), + ], + ), + ), + ); +} + +// ============================================================================= +// States Section +// ============================================================================= + +class _StatesSection extends StatelessWidget { + const _StatesSection(); + + @override + Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final boxShadow = context.streamBoxShadow; + final radius = context.streamRadius; + final spacing = context.streamSpacing; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const _SectionLabel(label: 'STATES'), + SizedBox(height: spacing.md), + Container( + width: double.infinity, + clipBehavior: Clip.antiAlias, + padding: EdgeInsets.all(spacing.md), + decoration: BoxDecoration( + color: colorScheme.backgroundSurface, + borderRadius: BorderRadius.all(radius.lg), + boxShadow: boxShadow.elevation1, + ), + foregroundDecoration: BoxDecoration( + borderRadius: BorderRadius.all(radius.lg), + border: Border.all(color: colorScheme.borderSubtle), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: spacing.md, + children: [ + _StateDemo( + label: 'Idle', + description: 'Not playing, thumb uses idle color', + child: SizedBox( + height: 36, + child: StreamAudioWaveformSlider( + waveform: _sampleWaveform, + progress: 0.3, + onChanged: (_) {}, + ), + ), + ), + Divider(height: 1, color: colorScheme.borderSubtle), + _StateDemo( + label: 'Active', + description: 'Playing, thumb uses active color', + child: SizedBox( + height: 36, + child: StreamAudioWaveformSlider( + waveform: _sampleWaveform, + progress: 0.5, + isActive: true, + onChanged: (_) {}, + ), + ), + ), + Divider(height: 1, color: colorScheme.borderSubtle), + _StateDemo( + label: 'Empty waveform', + description: 'No waveform data, bars show at minimum height', + child: SizedBox( + height: 36, + child: StreamAudioWaveformSlider( + waveform: const [], + progress: 0, + onChanged: (_) {}, + ), + ), + ), + ], + ), + ), + ], + ); + } +} + +class _StateDemo extends StatelessWidget { + const _StateDemo({ + required this.label, + required this.description, + required this.child, + }); + + final String label; + final String description; + final Widget child; + + @override + Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final spacing = context.streamSpacing; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: spacing.sm, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: textTheme.captionEmphasis.copyWith( + color: colorScheme.textPrimary, + ), + ), + Text( + description, + style: textTheme.metadataDefault.copyWith( + color: colorScheme.textTertiary, + ), + ), + ], + ), + child, + ], + ); + } +} + +// ============================================================================= +// Progress Section +// ============================================================================= + +class _ProgressSection extends StatelessWidget { + const _ProgressSection(); + + @override + Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final boxShadow = context.streamBoxShadow; + final radius = context.streamRadius; + final spacing = context.streamSpacing; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const _SectionLabel(label: 'PROGRESS SCALE'), + SizedBox(height: spacing.md), + Container( + width: double.infinity, + clipBehavior: Clip.antiAlias, + padding: EdgeInsets.all(spacing.md), + decoration: BoxDecoration( + color: colorScheme.backgroundSurface, + borderRadius: BorderRadius.all(radius.lg), + boxShadow: boxShadow.elevation1, + ), + foregroundDecoration: BoxDecoration( + borderRadius: BorderRadius.all(radius.lg), + border: Border.all(color: colorScheme.borderSubtle), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Progress fills bars from the leading edge', + style: textTheme.captionDefault.copyWith( + color: colorScheme.textSecondary, + ), + ), + SizedBox(height: spacing.md), + for (final percent in [0, 25, 50, 75, 100]) ...[ + _ProgressDemo(percentage: percent), + if (percent < 100) SizedBox(height: spacing.sm), + ], + ], + ), + ), + ], + ); + } +} + +class _ProgressDemo extends StatelessWidget { + const _ProgressDemo({required this.percentage}); + + final int percentage; + + @override + Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final spacing = context.streamSpacing; + + return Row( + children: [ + SizedBox( + width: 40, + child: Text( + '$percentage%', + style: textTheme.metadataEmphasis.copyWith( + color: colorScheme.accentPrimary, + fontFamily: 'monospace', + ), + ), + ), + SizedBox(width: spacing.sm), + Expanded( + child: SizedBox( + height: 32, + child: StreamAudioWaveformSlider( + waveform: _sampleWaveform, + progress: percentage / 100, + isActive: percentage > 0 && percentage < 100, + onChanged: (_) {}, + ), + ), + ), + ], + ); + } +} + +// ============================================================================= +// Waveform Only Section +// ============================================================================= + +class _WaveformOnlySection extends StatelessWidget { + const _WaveformOnlySection(); + + @override + Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final boxShadow = context.streamBoxShadow; + final radius = context.streamRadius; + final spacing = context.streamSpacing; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const _SectionLabel(label: 'WAVEFORM ONLY'), + SizedBox(height: spacing.md), + Container( + width: double.infinity, + clipBehavior: Clip.antiAlias, + padding: EdgeInsets.all(spacing.md), + decoration: BoxDecoration( + color: colorScheme.backgroundSurface, + borderRadius: BorderRadius.all(radius.lg), + boxShadow: boxShadow.elevation1, + ), + foregroundDecoration: BoxDecoration( + borderRadius: BorderRadius.all(radius.lg), + border: Border.all(color: colorScheme.borderSubtle), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: spacing.md, + children: [ + Text( + 'StreamAudioWaveform without the slider thumb', + style: textTheme.captionDefault.copyWith( + color: colorScheme.textSecondary, + ), + ), + SizedBox( + height: 32, + width: double.infinity, + child: StreamAudioWaveform( + waveform: _sampleWaveform, + progress: 0.6, + ), + ), + SizedBox( + height: 32, + width: double.infinity, + child: StreamAudioWaveform( + waveform: _sampleWaveform, + progress: 0, + ), + ), + ], + ), + ), + ], + ); + } +} + +// ============================================================================= +// Shared Widgets +// ============================================================================= + +class _SectionLabel extends StatelessWidget { + const _SectionLabel({required this.label}); + + final String label; + + @override + Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final radius = context.streamRadius; + final spacing = context.streamSpacing; + + return Container( + padding: EdgeInsets.symmetric( + horizontal: spacing.sm, + vertical: spacing.xs, + ), + decoration: BoxDecoration( + color: colorScheme.accentPrimary, + borderRadius: BorderRadius.all(radius.xs), + ), + child: Text( + label, + style: textTheme.metadataEmphasis.copyWith( + color: colorScheme.textOnAccent, + letterSpacing: 1, + fontSize: 9, + ), + ), + ); + } +} + +// ============================================================================= +// Sample Data +// ============================================================================= + +final List _sampleWaveform = List.generate( + 120, + (i) => (math.sin(i * 0.15) * 0.3 + 0.5 + math.sin(i * 0.4) * 0.2).clamp(0.0, 1.0), +); diff --git a/packages/stream_core_flutter/lib/src/components.dart b/packages/stream_core_flutter/lib/src/components.dart index 13fda19..23de79a 100644 --- a/packages/stream_core_flutter/lib/src/components.dart +++ b/packages/stream_core_flutter/lib/src/components.dart @@ -1,3 +1,4 @@ +export 'components/accessories/stream_audio_waveform.dart'; export 'components/accessories/stream_emoji.dart' hide DefaultStreamEmoji; export 'components/accessories/stream_file_type_icon.dart' hide DefaultStreamFileTypeIcon; export 'components/avatar/stream_avatar.dart' hide DefaultStreamAvatar; diff --git a/packages/stream_core_flutter/pubspec.yaml b/packages/stream_core_flutter/pubspec.yaml index 16b871d..46b8af0 100644 --- a/packages/stream_core_flutter/pubspec.yaml +++ b/packages/stream_core_flutter/pubspec.yaml @@ -9,7 +9,7 @@ environment: dependencies: cached_network_image: ^3.4.1 - collection: ^1.17.2 + collection: ^1.19.0 flutter: sdk: flutter flutter_svg: ^2.2.3 diff --git a/packages/stream_core_flutter/test/components/accessories/stream_audio_waveform_golden_test.dart b/packages/stream_core_flutter/test/components/accessories/stream_audio_waveform_golden_test.dart new file mode 100644 index 0000000..f9ce8cb --- /dev/null +++ b/packages/stream_core_flutter/test/components/accessories/stream_audio_waveform_golden_test.dart @@ -0,0 +1,312 @@ +import 'dart:math' as math; + +import 'package:alchemist/alchemist.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; + +final List _sampleWaveform = List.generate( + 120, + (i) => (math.sin(i * 0.15) * 0.3 + 0.5 + math.sin(i * 0.4) * 0.2).clamp(0.0, 1.0), +); + +void main() { + group('StreamAudioWaveformSlider Golden Tests', () { + goldenTest( + 'renders light theme states', + fileName: 'stream_audio_waveform_slider_light_states', + builder: () => GoldenTestGroup( + scenarioConstraints: const BoxConstraints(maxWidth: 300), + children: [ + GoldenTestScenario( + name: 'idle_no_progress', + child: _buildSliderInTheme( + StreamAudioWaveformSlider( + waveform: _sampleWaveform, + progress: 0, + onChanged: (_) {}, + ), + ), + ), + GoldenTestScenario( + name: 'idle_with_progress', + child: _buildSliderInTheme( + StreamAudioWaveformSlider( + waveform: _sampleWaveform, + progress: 0.4, + onChanged: (_) {}, + ), + ), + ), + GoldenTestScenario( + name: 'active_with_progress', + child: _buildSliderInTheme( + StreamAudioWaveformSlider( + waveform: _sampleWaveform, + progress: 0.5, + isActive: true, + onChanged: (_) {}, + ), + ), + ), + GoldenTestScenario( + name: 'active_full_progress', + child: _buildSliderInTheme( + StreamAudioWaveformSlider( + waveform: _sampleWaveform, + progress: 1.0, + isActive: true, + onChanged: (_) {}, + ), + ), + ), + GoldenTestScenario( + name: 'empty_waveform', + child: _buildSliderInTheme( + StreamAudioWaveformSlider( + waveform: const [], + progress: 0, + onChanged: (_) {}, + ), + ), + ), + ], + ), + ); + + goldenTest( + 'renders dark theme states', + fileName: 'stream_audio_waveform_slider_dark_states', + builder: () => GoldenTestGroup( + scenarioConstraints: const BoxConstraints(maxWidth: 300), + children: [ + GoldenTestScenario( + name: 'idle_no_progress', + child: _buildSliderInTheme( + StreamAudioWaveformSlider( + waveform: _sampleWaveform, + progress: 0, + onChanged: (_) {}, + ), + brightness: Brightness.dark, + ), + ), + GoldenTestScenario( + name: 'idle_with_progress', + child: _buildSliderInTheme( + StreamAudioWaveformSlider( + waveform: _sampleWaveform, + progress: 0.4, + onChanged: (_) {}, + ), + brightness: Brightness.dark, + ), + ), + GoldenTestScenario( + name: 'active_with_progress', + child: _buildSliderInTheme( + StreamAudioWaveformSlider( + waveform: _sampleWaveform, + progress: 0.5, + isActive: true, + onChanged: (_) {}, + ), + brightness: Brightness.dark, + ), + ), + GoldenTestScenario( + name: 'active_full_progress', + child: _buildSliderInTheme( + StreamAudioWaveformSlider( + waveform: _sampleWaveform, + progress: 1.0, + isActive: true, + onChanged: (_) {}, + ), + brightness: Brightness.dark, + ), + ), + ], + ), + ); + + goldenTest( + 'renders custom theme overrides', + fileName: 'stream_audio_waveform_slider_custom_theme', + builder: () => GoldenTestGroup( + scenarioConstraints: const BoxConstraints(maxWidth: 300), + children: [ + GoldenTestScenario( + name: 'custom_colors_idle', + child: _buildSliderInTheme( + StreamAudioWaveformTheme( + data: const StreamAudioWaveformThemeData( + color: Colors.purple, + progressColor: Colors.orange, + idleThumbColor: Colors.grey, + thumbBorderColor: Colors.black, + ), + child: StreamAudioWaveformSlider( + waveform: _sampleWaveform, + progress: 0.5, + onChanged: (_) {}, + ), + ), + ), + ), + GoldenTestScenario( + name: 'custom_colors_active', + child: _buildSliderInTheme( + StreamAudioWaveformTheme( + data: const StreamAudioWaveformThemeData( + color: Colors.purple, + progressColor: Colors.orange, + activeThumbColor: Colors.red, + thumbBorderColor: Colors.black, + ), + child: StreamAudioWaveformSlider( + waveform: _sampleWaveform, + progress: 0.5, + isActive: true, + onChanged: (_) {}, + ), + ), + ), + ), + ], + ), + ); + }); + + group('StreamAudioWaveform Golden Tests', () { + goldenTest( + 'renders light theme states', + fileName: 'stream_audio_waveform_light_states', + builder: () => GoldenTestGroup( + scenarioConstraints: const BoxConstraints(maxWidth: 300), + children: [ + GoldenTestScenario( + name: 'no_progress', + child: _buildWaveformInTheme( + StreamAudioWaveform( + waveform: _sampleWaveform, + progress: 0, + ), + ), + ), + GoldenTestScenario( + name: 'half_progress', + child: _buildWaveformInTheme( + StreamAudioWaveform( + waveform: _sampleWaveform, + progress: 0.5, + ), + ), + ), + GoldenTestScenario( + name: 'full_progress', + child: _buildWaveformInTheme( + StreamAudioWaveform( + waveform: _sampleWaveform, + progress: 1.0, + ), + ), + ), + GoldenTestScenario( + name: 'empty_waveform', + child: _buildWaveformInTheme( + const StreamAudioWaveform( + waveform: [], + progress: 0, + ), + ), + ), + ], + ), + ); + + goldenTest( + 'renders dark theme states', + fileName: 'stream_audio_waveform_dark_states', + builder: () => GoldenTestGroup( + scenarioConstraints: const BoxConstraints(maxWidth: 300), + children: [ + GoldenTestScenario( + name: 'no_progress', + child: _buildWaveformInTheme( + StreamAudioWaveform( + waveform: _sampleWaveform, + progress: 0, + ), + brightness: Brightness.dark, + ), + ), + GoldenTestScenario( + name: 'half_progress', + child: _buildWaveformInTheme( + StreamAudioWaveform( + waveform: _sampleWaveform, + progress: 0.5, + ), + brightness: Brightness.dark, + ), + ), + GoldenTestScenario( + name: 'full_progress', + child: _buildWaveformInTheme( + StreamAudioWaveform( + waveform: _sampleWaveform, + progress: 1.0, + ), + brightness: Brightness.dark, + ), + ), + ], + ), + ); + }); +} + +Widget _buildSliderInTheme( + Widget slider, { + Brightness brightness = Brightness.light, +}) { + final streamTheme = StreamTheme(brightness: brightness); + return Theme( + data: ThemeData( + brightness: brightness, + extensions: [streamTheme], + ), + child: Builder( + builder: (context) => Material( + color: StreamTheme.of(context).colorScheme.backgroundApp, + child: Padding( + padding: const EdgeInsets.all(8), + child: SizedBox(width: 280, height: 36, child: slider), + ), + ), + ), + ); +} + +Widget _buildWaveformInTheme( + Widget waveform, { + Brightness brightness = Brightness.light, +}) { + final streamTheme = StreamTheme(brightness: brightness); + return Theme( + data: ThemeData( + brightness: brightness, + extensions: [streamTheme], + ), + child: Builder( + builder: (context) => Material( + color: StreamTheme.of(context).colorScheme.backgroundApp, + child: Padding( + padding: const EdgeInsets.all(8), + child: SizedBox(width: 280, height: 32, child: waveform), + ), + ), + ), + ); +} From d3b1de20dec82f8ceb03534f96691604e3109d36 Mon Sep 17 00:00:00 2001 From: renefloor <15101411+renefloor@users.noreply.github.com> Date: Wed, 25 Feb 2026 13:34:26 +0000 Subject: [PATCH 4/5] chore: Update Goldens --- .../ci/stream_audio_waveform_dark_states.png | Bin 0 -> 11904 bytes .../ci/stream_audio_waveform_light_states.png | Bin 0 -> 11095 bytes ...tream_audio_waveform_slider_custom_theme.png | Bin 0 -> 9383 bytes ...stream_audio_waveform_slider_dark_states.png | Bin 0 -> 16192 bytes ...tream_audio_waveform_slider_light_states.png | Bin 0 -> 16378 bytes 5 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 packages/stream_core_flutter/test/components/accessories/goldens/ci/stream_audio_waveform_dark_states.png create mode 100644 packages/stream_core_flutter/test/components/accessories/goldens/ci/stream_audio_waveform_light_states.png create mode 100644 packages/stream_core_flutter/test/components/accessories/goldens/ci/stream_audio_waveform_slider_custom_theme.png create mode 100644 packages/stream_core_flutter/test/components/accessories/goldens/ci/stream_audio_waveform_slider_dark_states.png create mode 100644 packages/stream_core_flutter/test/components/accessories/goldens/ci/stream_audio_waveform_slider_light_states.png diff --git a/packages/stream_core_flutter/test/components/accessories/goldens/ci/stream_audio_waveform_dark_states.png b/packages/stream_core_flutter/test/components/accessories/goldens/ci/stream_audio_waveform_dark_states.png new file mode 100644 index 0000000000000000000000000000000000000000..a5140bff1f6fa0cb887a571f799743faae86de6e GIT binary patch literal 11904 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 Hi~|1~YhfL! literal 0 HcmV?d00001 diff --git a/packages/stream_core_flutter/test/components/accessories/goldens/ci/stream_audio_waveform_light_states.png b/packages/stream_core_flutter/test/components/accessories/goldens/ci/stream_audio_waveform_light_states.png new file mode 100644 index 0000000000000000000000000000000000000000..87fad475c59843b420eeed4cf25ff9cd1a5fa0fb GIT binary patch literal 11095 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 IrA*)d4_w^&umAu6 literal 0 HcmV?d00001 diff --git a/packages/stream_core_flutter/test/components/accessories/goldens/ci/stream_audio_waveform_slider_custom_theme.png b/packages/stream_core_flutter/test/components/accessories/goldens/ci/stream_audio_waveform_slider_custom_theme.png new file mode 100644 index 0000000000000000000000000000000000000000..76e8c24555e993507630b5dbaa0c752baa0c73dd GIT binary patch literal 9383 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 literal 0 HcmV?d00001 diff --git a/packages/stream_core_flutter/test/components/accessories/goldens/ci/stream_audio_waveform_slider_dark_states.png b/packages/stream_core_flutter/test/components/accessories/goldens/ci/stream_audio_waveform_slider_dark_states.png new file mode 100644 index 0000000000000000000000000000000000000000..c64a0361f059b6117dbf69ea8759599bae0a313c GIT binary patch literal 16192 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%0DVW~~z=OYsKM!34*G!E(uISjIrj2maaLI?da50 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 U`P|=u!}|~!2}SV|QKOIl3-d;srvLx| literal 0 HcmV?d00001 From 87415fe0d9a394f61e01ace823d5d6c63671837e Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 25 Feb 2026 14:39:35 +0100 Subject: [PATCH 5/5] fix analysis issues --- .../components/accessories/stream_audio_waveform.dart | 4 +--- .../components/accessories/stream_audio_waveform.dart | 6 +++--- .../accessories/stream_audio_waveform_golden_test.dart | 10 ++++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/design_system_gallery/lib/components/accessories/stream_audio_waveform.dart b/apps/design_system_gallery/lib/components/accessories/stream_audio_waveform.dart index c9c36b6..f26db0d 100644 --- a/apps/design_system_gallery/lib/components/accessories/stream_audio_waveform.dart +++ b/apps/design_system_gallery/lib/components/accessories/stream_audio_waveform.dart @@ -58,7 +58,7 @@ class _SliderPlayground extends StatefulWidget { } class _SliderPlaygroundState extends State<_SliderPlayground> { - double _progress = 0.4; + var _progress = 0.4; @override Widget build(BuildContext context) { @@ -188,7 +188,6 @@ class _StatesSection extends StatelessWidget { height: 36, child: StreamAudioWaveformSlider( waveform: const [], - progress: 0, onChanged: (_) {}, ), ), @@ -396,7 +395,6 @@ class _WaveformOnlySection extends StatelessWidget { width: double.infinity, child: StreamAudioWaveform( waveform: _sampleWaveform, - progress: 0, ), ), ], diff --git a/packages/stream_core_flutter/lib/src/components/accessories/stream_audio_waveform.dart b/packages/stream_core_flutter/lib/src/components/accessories/stream_audio_waveform.dart index 3f23f3e..73e4e9b 100644 --- a/packages/stream_core_flutter/lib/src/components/accessories/stream_audio_waveform.dart +++ b/packages/stream_core_flutter/lib/src/components/accessories/stream_audio_waveform.dart @@ -349,7 +349,7 @@ class _WaveformPainter extends CustomPainter { final barSpacing = spacingWidth / (limit - 1); final progressWidth = progress * canvasWidth; - void _paintBar(int index, double barValue) { + void paintBar(int index, double barValue) { var dx = index * (barWidth + barSpacing) + barWidth / 2; if (inverse) dx = canvasWidth - dx; final dy = canvasHeight / 2; @@ -378,7 +378,7 @@ class _WaveformPainter extends CustomPainter { } // Paint all the bars - waveform.forEachIndexed(_paintBar); + waveform.forEachIndexed(paintBar); } @override @@ -428,7 +428,7 @@ class HorizontalSlider extends StatefulWidget { } class _HorizontalSliderState extends State { - bool _active = false; + var _active = false; /// Returns true if the slider is interactive. bool get isInteractive => widget.onChanged != null; diff --git a/packages/stream_core_flutter/test/components/accessories/stream_audio_waveform_golden_test.dart b/packages/stream_core_flutter/test/components/accessories/stream_audio_waveform_golden_test.dart index f9ce8cb..c016b5f 100644 --- a/packages/stream_core_flutter/test/components/accessories/stream_audio_waveform_golden_test.dart +++ b/packages/stream_core_flutter/test/components/accessories/stream_audio_waveform_golden_test.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_redundant_argument_values + import 'dart:math' as math; import 'package:alchemist/alchemist.dart'; @@ -54,7 +56,7 @@ void main() { child: _buildSliderInTheme( StreamAudioWaveformSlider( waveform: _sampleWaveform, - progress: 1.0, + progress: 1, isActive: true, onChanged: (_) {}, ), @@ -119,7 +121,7 @@ void main() { child: _buildSliderInTheme( StreamAudioWaveformSlider( waveform: _sampleWaveform, - progress: 1.0, + progress: 1, isActive: true, onChanged: (_) {}, ), @@ -208,7 +210,7 @@ void main() { child: _buildWaveformInTheme( StreamAudioWaveform( waveform: _sampleWaveform, - progress: 1.0, + progress: 1, ), ), ), @@ -256,7 +258,7 @@ void main() { child: _buildWaveformInTheme( StreamAudioWaveform( waveform: _sampleWaveform, - progress: 1.0, + progress: 1, ), brightness: Brightness.dark, ),