diff --git a/lib/screens/livetv/tabs/guide_tab.dart b/lib/screens/livetv/tabs/guide_tab.dart index 5f7774b6..7b70457d 100644 --- a/lib/screens/livetv/tabs/guide_tab.dart +++ b/lib/screens/livetv/tabs/guide_tab.dart @@ -977,11 +977,12 @@ class GuideTabState extends State { 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 { 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 { 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 { 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, - ), - ], - ), + ); + }, ), ), ), diff --git a/lib/services/plex_client.dart b/lib/services/plex_client.dart index 9b64d359..8b1e7743 100644 --- a/lib/services/plex_client.dart +++ b/lib/services/plex_client.dart @@ -2316,11 +2316,23 @@ class PlexClient { final allChannels = []; 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;