Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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),
),
],
Expand All @@ -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';
}
Expand All @@ -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 }
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -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),
),
),
),
);
}