diff --git a/packages/stream_core_flutter/lib/src/components/badge/stream_media_badge.dart b/packages/stream_core_flutter/lib/src/components/badge/stream_media_badge.dart index 2a6ad99..f048158 100644 --- a/packages/stream_core_flutter/lib/src/components/badge/stream_media_badge.dart +++ b/packages/stream_core_flutter/lib/src/components/badge/stream_media_badge.dart @@ -2,17 +2,41 @@ import 'package:flutter/widgets.dart'; import '../../../stream_core_flutter.dart'; +/// Controls how a duration value is formatted inside [StreamMediaBadge]. +enum MediaBadgeDurationFormat { + /// Compact contextual format — floored, no exact time shown. + /// + /// - < 1 minute → `Xs` (e.g. `8s`) + /// - < 1 hour → `Xm` (e.g. `1m`, `10m`) + /// - ≥ 1 hour → `Xh` (e.g. `1h`, `2h`) + compact, + + /// Exact time format — no rounding or truncation. + /// + /// - < 1 hour → `M:SS` (e.g. `0:08`, `10:08`) + /// - ≥ 1 hour → `H:MM:SS` (e.g. `1:00:08`) + exact, +} + class StreamMediaBadge extends StatelessWidget { - const StreamMediaBadge({super.key, required this.type, this.duration}); + const StreamMediaBadge({ + super.key, + required this.type, + this.duration, + this.durationFormat = MediaBadgeDurationFormat.compact, + }); final MediaBadgeType type; final Duration? duration; + /// How the [duration] value is formatted. Defaults to [MediaBadgeDurationFormat.compact]. + final MediaBadgeDurationFormat durationFormat; + @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( - color: context.streamColorScheme.chrome[10000], + color: context.streamColorScheme.chrome[1000], borderRadius: BorderRadius.all(context.streamRadius.max), ), padding: EdgeInsets.symmetric( @@ -33,7 +57,10 @@ class StreamMediaBadge extends StatelessWidget { if (duration case final duration?) Text( - duration.toReadableString(), + switch (durationFormat) { + MediaBadgeDurationFormat.compact => duration.toCompactString(), + MediaBadgeDurationFormat.exact => duration.toExactString(), + }, style: context.streamTextTheme.numericMd.copyWith(color: context.streamColorScheme.textInverse), ), ], @@ -43,7 +70,9 @@ class StreamMediaBadge extends StatelessWidget { } extension on Duration { - String toReadableString() { + /// Compact contextual format, always floored. + /// `8s`, `1m`, `10m`, `1h`, `2h` + String toCompactString() { if (inSeconds < 60) { return '${inSeconds}s'; } @@ -52,6 +81,18 @@ extension on Duration { } return '${inHours}h'; } + + /// Exact time format. + /// `0:08`, `10:08`, `1:00:08` + String toExactString() { + final h = inHours; + final m = inMinutes.remainder(60); + final s = inSeconds.remainder(60); + if (h > 0) { + return '$h:${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}'; + } + return '$m:${s.toString().padLeft(2, '0')}'; + } } enum MediaBadgeType { video, audio } diff --git a/packages/stream_core_flutter/test/components/badge/goldens/ci/stream_media_badge_compact_duration.png b/packages/stream_core_flutter/test/components/badge/goldens/ci/stream_media_badge_compact_duration.png new file mode 100644 index 0000000..2e3faf9 Binary files /dev/null and b/packages/stream_core_flutter/test/components/badge/goldens/ci/stream_media_badge_compact_duration.png differ diff --git a/packages/stream_core_flutter/test/components/badge/goldens/ci/stream_media_badge_dark_matrix.png b/packages/stream_core_flutter/test/components/badge/goldens/ci/stream_media_badge_dark_matrix.png new file mode 100644 index 0000000..144ff33 Binary files /dev/null and b/packages/stream_core_flutter/test/components/badge/goldens/ci/stream_media_badge_dark_matrix.png differ diff --git a/packages/stream_core_flutter/test/components/badge/goldens/ci/stream_media_badge_exact_duration.png b/packages/stream_core_flutter/test/components/badge/goldens/ci/stream_media_badge_exact_duration.png new file mode 100644 index 0000000..d477b07 Binary files /dev/null and b/packages/stream_core_flutter/test/components/badge/goldens/ci/stream_media_badge_exact_duration.png differ diff --git a/packages/stream_core_flutter/test/components/badge/goldens/ci/stream_media_badge_light_matrix.png b/packages/stream_core_flutter/test/components/badge/goldens/ci/stream_media_badge_light_matrix.png new file mode 100644 index 0000000..78e1cdb Binary files /dev/null and b/packages/stream_core_flutter/test/components/badge/goldens/ci/stream_media_badge_light_matrix.png differ diff --git a/packages/stream_core_flutter/test/components/badge/stream_media_badge_golden_test.dart b/packages/stream_core_flutter/test/components/badge/stream_media_badge_golden_test.dart new file mode 100644 index 0000000..c5c68da --- /dev/null +++ b/packages/stream_core_flutter/test/components/badge/stream_media_badge_golden_test.dart @@ -0,0 +1,216 @@ +// ignore_for_file: avoid_redundant_argument_values + +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'; + +void main() { + group('StreamMediaBadge Golden Tests', () { + goldenTest( + 'renders light theme type matrix', + fileName: 'stream_media_badge_light_matrix', + builder: () => GoldenTestGroup( + scenarioConstraints: const BoxConstraints(maxWidth: 200), + children: [ + for (final type in MediaBadgeType.values) ...[ + GoldenTestScenario( + name: '${type.name}_no_duration', + child: _buildInTheme(StreamMediaBadge(type: type)), + ), + GoldenTestScenario( + name: '${type.name}_with_duration', + child: _buildInTheme( + StreamMediaBadge( + type: type, + duration: const Duration(seconds: 8), + ), + ), + ), + ], + ], + ), + ); + + goldenTest( + 'renders dark theme type matrix', + fileName: 'stream_media_badge_dark_matrix', + builder: () => GoldenTestGroup( + scenarioConstraints: const BoxConstraints(maxWidth: 200), + children: [ + for (final type in MediaBadgeType.values) ...[ + GoldenTestScenario( + name: '${type.name}_no_duration', + child: _buildInTheme( + StreamMediaBadge(type: type), + brightness: Brightness.dark, + ), + ), + GoldenTestScenario( + name: '${type.name}_with_duration', + child: _buildInTheme( + StreamMediaBadge( + type: type, + duration: const Duration(seconds: 8), + ), + brightness: Brightness.dark, + ), + ), + ], + ], + ), + ); + + goldenTest( + 'renders compact duration format correctly', + fileName: 'stream_media_badge_compact_duration', + builder: () => GoldenTestGroup( + scenarioConstraints: const BoxConstraints(maxWidth: 200), + children: [ + GoldenTestScenario( + name: '8s', + child: _buildInTheme( + const StreamMediaBadge( + type: MediaBadgeType.video, + duration: Duration(seconds: 8), + durationFormat: MediaBadgeDurationFormat.compact, + ), + ), + ), + GoldenTestScenario( + name: '1m', + child: _buildInTheme( + const StreamMediaBadge( + type: MediaBadgeType.video, + duration: Duration(seconds: 60), + durationFormat: MediaBadgeDurationFormat.compact, + ), + ), + ), + GoldenTestScenario( + name: '10m', + child: _buildInTheme( + const StreamMediaBadge( + type: MediaBadgeType.video, + duration: Duration(seconds: 608), // 10:08 → 10m + durationFormat: MediaBadgeDurationFormat.compact, + ), + ), + ), + GoldenTestScenario( + name: '59m', + child: _buildInTheme( + const StreamMediaBadge( + type: MediaBadgeType.video, + duration: Duration(seconds: 3599), // 59:59 → 59m + durationFormat: MediaBadgeDurationFormat.compact, + ), + ), + ), + GoldenTestScenario( + name: '1h', + child: _buildInTheme( + const StreamMediaBadge( + type: MediaBadgeType.video, + duration: Duration(hours: 1), + durationFormat: MediaBadgeDurationFormat.compact, + ), + ), + ), + GoldenTestScenario( + name: '2h', + child: _buildInTheme( + const StreamMediaBadge( + type: MediaBadgeType.video, + duration: Duration(hours: 2), + durationFormat: MediaBadgeDurationFormat.compact, + ), + ), + ), + ], + ), + ); + + goldenTest( + 'renders exact duration format correctly', + fileName: 'stream_media_badge_exact_duration', + builder: () => GoldenTestGroup( + scenarioConstraints: const BoxConstraints(maxWidth: 200), + children: [ + GoldenTestScenario( + name: '0:08', + child: _buildInTheme( + const StreamMediaBadge( + type: MediaBadgeType.video, + duration: Duration(seconds: 8), + durationFormat: MediaBadgeDurationFormat.exact, + ), + ), + ), + GoldenTestScenario( + name: '10:08', + child: _buildInTheme( + const StreamMediaBadge( + type: MediaBadgeType.video, + duration: Duration(seconds: 608), + durationFormat: MediaBadgeDurationFormat.exact, + ), + ), + ), + GoldenTestScenario( + name: '59:59', + child: _buildInTheme( + const StreamMediaBadge( + type: MediaBadgeType.video, + duration: Duration(seconds: 3599), + durationFormat: MediaBadgeDurationFormat.exact, + ), + ), + ), + GoldenTestScenario( + name: '1:00:08', + child: _buildInTheme( + const StreamMediaBadge( + type: MediaBadgeType.video, + duration: Duration(hours: 1, seconds: 8), + durationFormat: MediaBadgeDurationFormat.exact, + ), + ), + ), + GoldenTestScenario( + name: '1:59:59', + child: _buildInTheme( + const StreamMediaBadge( + type: MediaBadgeType.video, + duration: Duration(hours: 1, minutes: 59, seconds: 59), + durationFormat: MediaBadgeDurationFormat.exact, + ), + ), + ), + ], + ), + ); + }); +} + +Widget _buildInTheme( + Widget child, { + 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: Center(child: child), + ), + ), + ), + ); +}