fix: pin EPG titles, filter cloud channel list

close #614
This commit is contained in:
edde746
2026-04-17 04:28:19 +02:00
parent 2e429840d2
commit ee20965218
2 changed files with 63 additions and 34 deletions
+48 -31
View File
@@ -977,11 +977,12 @@ class GuideTabState extends State<GuideTab> {
final duration = progEnd - progStart;
final left = (startOffset / (_minutesPerSlot * 60)) * _slotWidth;
final width = (duration / (_minutesPerSlot * 60)) * _slotWidth;
final clampedWidth = width.clamp(2.0, double.infinity);
blocks.add(
Positioned(
left: left,
width: width.clamp(2.0, double.infinity),
width: clampedWidth,
top: 0,
bottom: 0,
child: _buildProgramBlock(
@@ -991,6 +992,8 @@ class GuideTabState extends State<GuideTab> {
isFirst: progStart == gridStartEpoch,
isLast: program == programs.last && progEnd != gridEndEpoch,
isFocused: identical(program, focusProg),
tileLeft: left,
tileWidth: clampedWidth,
),
),
);
@@ -1012,6 +1015,8 @@ class GuideTabState extends State<GuideTab> {
bool isFirst = false,
bool isLast = false,
bool isFocused = false,
double tileLeft = 0,
double tileWidth = 0,
}) {
final isCurrentlyAiring = program.isCurrentlyAiring;
final isPast = program.endsAt != null && program.endsAt! < DateTime.now().millisecondsSinceEpoch ~/ 1000;
@@ -1060,37 +1065,49 @@ class GuideTabState extends State<GuideTab> {
right: isLast ? BorderSide(color: theme.dividerColor.withValues(alpha: 0.3)) : BorderSide.none,
),
),
child: Container(
color: isFocused ? null : materialColor,
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
program.grandparentTitle ?? program.title,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
color: titleColor,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
child: ListenableBuilder(
listenable: _gridHorizontalController,
builder: (context, _) {
const basePadding = 6.0;
final scrollOffset = _gridHorizontalController.hasClients
? _gridHorizontalController.offset
: 0.0;
final maxInset = (tileWidth - 2 * basePadding - 20).clamp(0.0, double.infinity);
final leftInset = (scrollOffset - tileLeft).clamp(0.0, maxInset);
return Container(
color: isFocused ? null : materialColor,
padding: EdgeInsets.fromLTRB(basePadding + leftInset, 4, basePadding, 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
program.grandparentTitle ?? program.title,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
color: titleColor,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (program.grandparentTitle != null)
Text(
'${program.parentIndex != null && program.index != null ? 'S${program.parentIndex}E${program.index} · ' : ''}${program.title}',
style: theme.textTheme.labelSmall?.copyWith(color: subtitleColor),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (program.startTime != null)
Text(
'${formatClockTime(program.startTime!, is24Hour: MediaQuery.alwaysUse24HourFormatOf(context))} · ${formatDurationTextual(program.durationMinutes * 60000)}',
style: theme.textTheme.labelSmall?.copyWith(color: subtitleColor),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
if (program.grandparentTitle != null)
Text(
'${program.parentIndex != null && program.index != null ? 'S${program.parentIndex}E${program.index} · ' : ''}${program.title}',
style: theme.textTheme.labelSmall?.copyWith(color: subtitleColor),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (program.startTime != null)
Text(
'${formatClockTime(program.startTime!, is24Hour: MediaQuery.alwaysUse24HourFormatOf(context))} · ${formatDurationTextual(program.durationMinutes * 60000)}',
style: theme.textTheme.labelSmall?.copyWith(color: subtitleColor),
maxLines: 1,
),
],
),
);
},
),
),
),
+15 -3
View File
@@ -2316,11 +2316,23 @@ class PlexClient {
final allChannels = <LiveTvChannel>[];
for (final provider in _providerEpg) {
final isCloudGuide = provider.identifier.startsWith('tv.plex.providers.epg');
final primaryEndpoint = isCloudGuide
? '/lineups/plex/channels'
: '/${provider.identifier}/lineups/dvr/channels';
try {
final response = await _getWithFailover('/${provider.identifier}/lineups/dvr/channels');
allChannels.addAll(parseChannels(response));
final response = await _getWithFailover(primaryEndpoint);
final parsed = parseChannels(response);
if (parsed.isEmpty && isCloudGuide) {
// Fallback: the prefix matched but this server doesn't expose the
// cloud endpoint. Retry with the legacy DVR endpoint.
final fallback = await _getWithFailover('/${provider.identifier}/lineups/dvr/channels');
allChannels.addAll(parseChannels(fallback));
} else {
allChannels.addAll(parsed);
}
} catch (e) {
appLogger.e('Failed to get EPG channels from ${provider.identifier}', error: e);
appLogger.e('Failed to get EPG channels from ${provider.identifier} via $primaryEndpoint', error: e);
}
}
return allChannels;