Show elapsed, remaining, and total time
This commit is contained in:
@@ -121,15 +121,23 @@ String formatDurationWithSeconds(Duration duration) {
|
||||
///
|
||||
/// Used for: video controls, chapters, episode durations.
|
||||
String formatDurationTimestamp(Duration duration) {
|
||||
final hours = duration.inHours;
|
||||
final minutes = duration.inMinutes.remainder(60);
|
||||
final seconds = duration.inSeconds.remainder(60);
|
||||
// Handle negative durations
|
||||
final isNegative = duration.isNegative;
|
||||
final absoluteDuration = duration.abs();
|
||||
|
||||
final hours = absoluteDuration.inHours;
|
||||
final minutes = absoluteDuration.inMinutes.remainder(60);
|
||||
final seconds = absoluteDuration.inSeconds.remainder(60);
|
||||
|
||||
final result;
|
||||
|
||||
if (hours > 0) {
|
||||
return '$hours:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
||||
result = '$hours:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
||||
} else {
|
||||
return '$minutes:${seconds.toString().padLeft(2, '0')}';
|
||||
result = '$minutes:${seconds.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
return isNegative ? '-$result' : result;
|
||||
}
|
||||
|
||||
/// Formats a sync offset in milliseconds with sign indicator (e.g., "+150ms", "-15.1s").
|
||||
@@ -170,3 +178,8 @@ DurationLocale _getDurationLocale() {
|
||||
return const EnglishDurationLocale();
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes a list of strings and returns one long string with each item in the list concatenated by a bullet
|
||||
String toBulletedString(List<String> parts) {
|
||||
return parts.join(' · ');
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:plezy/utils/formatters.dart';
|
||||
|
||||
import '../../../models/plex_metadata.dart';
|
||||
import '../../../i18n/strings.g.dart';
|
||||
@@ -56,12 +57,19 @@ class VideoControlsHeader extends StatelessWidget {
|
||||
final seriesName = metadata.grandparentTitle ?? metadata.title;
|
||||
final hasEpisodeInfo = metadata.parentIndex != null && metadata.index != null;
|
||||
|
||||
final titleText = hasEpisodeInfo
|
||||
? '$seriesName · S${metadata.parentIndex} E${metadata.index} · ${metadata.title}'
|
||||
: seriesName;
|
||||
List<String> parts = [seriesName];
|
||||
|
||||
if (hasEpisodeInfo) {
|
||||
parts.add('S${metadata.parentIndex}');
|
||||
parts.add('E${metadata.index}');
|
||||
}
|
||||
|
||||
if (metadata.duration != null) {
|
||||
parts.add(formatDurationTextual(metadata.duration!));
|
||||
}
|
||||
|
||||
return Text(
|
||||
titleText,
|
||||
toBulletedString(parts),
|
||||
style: const TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w500),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
@@ -69,6 +77,18 @@ class VideoControlsHeader extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildMultiLineTitle() {
|
||||
List<String> secondLineParts = [];
|
||||
|
||||
if (metadata.parentIndex != null && metadata.index != null) {
|
||||
secondLineParts.add('S${metadata.parentIndex}');
|
||||
secondLineParts.add('E${metadata.index}');
|
||||
secondLineParts.add(metadata.title);
|
||||
}
|
||||
|
||||
if (metadata.duration != null) {
|
||||
secondLineParts.add(formatDurationTextual(metadata.duration!));
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -78,9 +98,9 @@ class VideoControlsHeader extends StatelessWidget {
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (metadata.parentIndex != null && metadata.index != null)
|
||||
if (secondLineParts.isNotEmpty)
|
||||
Text(
|
||||
'S${metadata.parentIndex} · E${metadata.index} · ${metadata.title}',
|
||||
toBulletedString(secondLineParts),
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 14),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
|
||||
@@ -59,11 +59,12 @@ class VideoTimelineBar extends StatelessWidget {
|
||||
builder: (context, durationSnapshot) {
|
||||
final position = positionSnapshot.data ?? Duration.zero;
|
||||
final duration = durationSnapshot.data ?? Duration.zero;
|
||||
final remaining = position - duration; // We want this to be negative
|
||||
|
||||
if (horizontalLayout) {
|
||||
return _buildHorizontalLayout(position, duration);
|
||||
return _buildHorizontalLayout(position, duration, remaining);
|
||||
} else {
|
||||
return _buildVerticalLayout(position, duration);
|
||||
return _buildVerticalLayout(position, duration, remaining);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -71,19 +72,19 @@ class VideoTimelineBar extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHorizontalLayout(Duration position, Duration duration) {
|
||||
Widget _buildHorizontalLayout(Duration position, Duration duration, Duration remaining) {
|
||||
return Row(
|
||||
children: [
|
||||
_buildTimestamp(position),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: _buildSlider(position, duration)),
|
||||
const SizedBox(width: 12),
|
||||
_buildTimestamp(duration),
|
||||
_buildTimestamp(remaining),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVerticalLayout(Duration position, Duration duration) {
|
||||
Widget _buildVerticalLayout(Duration position, Duration duration, Duration remaining) {
|
||||
return Column(
|
||||
children: [
|
||||
_buildSlider(position, duration),
|
||||
@@ -91,7 +92,7 @@ class VideoTimelineBar extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [_buildTimestamp(position), _buildTimestamp(duration)],
|
||||
children: [_buildTimestamp(position), _buildTimestamp(remaining)],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user