Merge pull request #317 from micahmo/feature/show-remaining-time

Show elapsed, remaining, and total time
This commit is contained in:
edde746
2026-01-21 19:51:15 +01:00
committed by GitHub
3 changed files with 51 additions and 17 deletions
+18 -5
View File
@@ -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)],
),
),
],