feat(tv): add poster to program details, translate EPG strings
This commit is contained in:
@@ -558,7 +558,15 @@
|
||||
"premiere": "NEU",
|
||||
"reloadGuide": "Programmführer neu laden",
|
||||
"guideReloaded": "Programmdaten neu geladen",
|
||||
"allChannels": "Alle Kanäle"
|
||||
"allChannels": "Alle Kanäle",
|
||||
"now": "Jetzt",
|
||||
"today": "Heute",
|
||||
"midnight": "Mitternacht",
|
||||
"overnight": "Nacht",
|
||||
"morning": "Morgen",
|
||||
"daytime": "Tagsüber",
|
||||
"evening": "Abend",
|
||||
"lateNight": "Spätnacht"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Downloads",
|
||||
|
||||
@@ -558,7 +558,15 @@
|
||||
"premiere": "NUEVO",
|
||||
"reloadGuide": "Recargar guía",
|
||||
"guideReloaded": "Datos de la guía recargados",
|
||||
"allChannels": "Todos los canales"
|
||||
"allChannels": "Todos los canales",
|
||||
"now": "Ahora",
|
||||
"today": "Hoy",
|
||||
"midnight": "Medianoche",
|
||||
"overnight": "Madrugada",
|
||||
"morning": "Mañana",
|
||||
"daytime": "Día",
|
||||
"evening": "Noche",
|
||||
"lateNight": "Trasnoche"
|
||||
},
|
||||
"collections": {
|
||||
"title": "Colecciones",
|
||||
|
||||
@@ -558,7 +558,15 @@
|
||||
"premiere": "NOUVEAU",
|
||||
"reloadGuide": "Recharger le guide",
|
||||
"guideReloaded": "Données du guide rechargées",
|
||||
"allChannels": "Toutes les chaînes"
|
||||
"allChannels": "Toutes les chaînes",
|
||||
"now": "Maintenant",
|
||||
"today": "Aujourd'hui",
|
||||
"midnight": "Minuit",
|
||||
"overnight": "Nuit",
|
||||
"morning": "Matin",
|
||||
"daytime": "Journée",
|
||||
"evening": "Soirée",
|
||||
"lateNight": "Nuit tardive"
|
||||
},
|
||||
"collections": {
|
||||
"title": "Collections",
|
||||
|
||||
@@ -558,7 +558,15 @@
|
||||
"premiere": "NUOVO",
|
||||
"reloadGuide": "Ricarica guida",
|
||||
"guideReloaded": "Dati della guida ricaricati",
|
||||
"allChannels": "Tutti i canali"
|
||||
"allChannels": "Tutti i canali",
|
||||
"now": "Ora",
|
||||
"today": "Oggi",
|
||||
"midnight": "Mezzanotte",
|
||||
"overnight": "Notte",
|
||||
"morning": "Mattina",
|
||||
"daytime": "Giorno",
|
||||
"evening": "Sera",
|
||||
"lateNight": "Notte tarda"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Download",
|
||||
|
||||
@@ -558,7 +558,15 @@
|
||||
"premiere": "신규",
|
||||
"reloadGuide": "편성표 새로고침",
|
||||
"guideReloaded": "편성표 데이터가 새로고침되었습니다",
|
||||
"allChannels": "전체 채널"
|
||||
"allChannels": "전체 채널",
|
||||
"now": "지금",
|
||||
"today": "오늘",
|
||||
"midnight": "자정",
|
||||
"overnight": "심야",
|
||||
"morning": "아침",
|
||||
"daytime": "낮",
|
||||
"evening": "저녁",
|
||||
"lateNight": "심야 방송"
|
||||
},
|
||||
"collections": {
|
||||
"title": "컬렉션",
|
||||
|
||||
@@ -558,7 +558,15 @@
|
||||
"premiere": "NIEUW",
|
||||
"reloadGuide": "Gids herladen",
|
||||
"guideReloaded": "Gidsgegevens herladen",
|
||||
"allChannels": "Alle zenders"
|
||||
"allChannels": "Alle zenders",
|
||||
"now": "Nu",
|
||||
"today": "Vandaag",
|
||||
"midnight": "Middernacht",
|
||||
"overnight": "Nacht",
|
||||
"morning": "Ochtend",
|
||||
"daytime": "Overdag",
|
||||
"evening": "Avond",
|
||||
"lateNight": "Late avond"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Downloads",
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
/// To regenerate, run: `dart run slang`
|
||||
///
|
||||
/// Locales: 9
|
||||
/// Strings: 6488 (720 per locale)
|
||||
/// Strings: 6552 (728 per locale)
|
||||
///
|
||||
/// Built on 2026-02-12 at 12:55 UTC
|
||||
/// Built on 2026-02-12 at 13:29 UTC
|
||||
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint, unused_import
|
||||
|
||||
@@ -558,7 +558,15 @@
|
||||
"premiere": "NY",
|
||||
"reloadGuide": "Ladda om programguide",
|
||||
"guideReloaded": "Programdata omladdad",
|
||||
"allChannels": "Alla kanaler"
|
||||
"allChannels": "Alla kanaler",
|
||||
"now": "Nu",
|
||||
"today": "Idag",
|
||||
"midnight": "Midnatt",
|
||||
"overnight": "Natt",
|
||||
"morning": "Morgon",
|
||||
"daytime": "Dagtid",
|
||||
"evening": "Kväll",
|
||||
"lateNight": "Sen kväll"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Nedladdningar",
|
||||
|
||||
@@ -558,7 +558,15 @@
|
||||
"premiere": "新",
|
||||
"reloadGuide": "重新加载节目指南",
|
||||
"guideReloaded": "节目指南已重新加载",
|
||||
"allChannels": "所有频道"
|
||||
"allChannels": "所有频道",
|
||||
"now": "现在",
|
||||
"today": "今天",
|
||||
"midnight": "午夜",
|
||||
"overnight": "凌晨",
|
||||
"morning": "上午",
|
||||
"daytime": "白天",
|
||||
"evening": "晚上",
|
||||
"lateNight": "深夜"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "下载",
|
||||
|
||||
@@ -724,7 +724,7 @@ class _LiveTvScreenState extends State<LiveTvScreen> {
|
||||
width: width.clamp(2.0, double.infinity),
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
child: _buildProgramBlock(channel, program, theme),
|
||||
child: _buildProgramBlock(channel, program, theme, isLast: program == programs.last),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -746,7 +746,7 @@ class _LiveTvScreenState extends State<LiveTvScreen> {
|
||||
}
|
||||
|
||||
Widget _buildProgramBlock(
|
||||
LiveTvChannel channel, LiveTvProgram program, ThemeData theme) {
|
||||
LiveTvChannel channel, LiveTvProgram program, ThemeData theme, {bool isLast = false}) {
|
||||
final isCurrentlyAiring = program.isCurrentlyAiring;
|
||||
final isPast = program.endsAt != null &&
|
||||
program.endsAt! < DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
@@ -763,7 +763,10 @@ class _LiveTvScreenState extends State<LiveTvScreen> {
|
||||
onTap: () => _showProgramDetails(channel, program),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(left: BorderSide(color: theme.dividerColor.withValues(alpha: 0.3))),
|
||||
border: Border(
|
||||
left: BorderSide(color: theme.dividerColor.withValues(alpha: 0.3)),
|
||||
right: isLast ? BorderSide(color: theme.dividerColor.withValues(alpha: 0.3)) : BorderSide.none,
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
||||
child: Column(
|
||||
@@ -816,6 +819,20 @@ class _LiveTvScreenState extends State<LiveTvScreen> {
|
||||
void _showProgramDetails(LiveTvChannel channel, LiveTvProgram program) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final multiServer = context.read<MultiServerProvider>();
|
||||
final client = multiServer.getClientForServer(channel.serverId ?? '');
|
||||
String? posterUrl;
|
||||
if (program.thumb != null && client != null) {
|
||||
posterUrl = PlexImageHelper.getOptimizedImageUrl(
|
||||
client: client,
|
||||
thumbPath: program.thumb,
|
||||
maxWidth: 80,
|
||||
maxHeight: 120,
|
||||
devicePixelRatio: PlexImageHelper.effectiveDevicePixelRatio(context),
|
||||
imageType: ImageType.poster,
|
||||
);
|
||||
}
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (sheetContext) {
|
||||
@@ -826,47 +843,72 @@ class _LiveTvScreenState extends State<LiveTvScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (posterUrl != null) ...[
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: Image.network(
|
||||
posterUrl,
|
||||
width: 80,
|
||||
height: 120,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
],
|
||||
Expanded(
|
||||
child: Text(
|
||||
program.displayTitle,
|
||||
style: theme.textTheme.titleMedium,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
program.displayTitle,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
if (program.isCurrentlyAiring)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
t.liveTv.live,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 11),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${channel.displayName} · ${program.startTime?.hour.toString().padLeft(2, '0')}:${program.startTime?.minute.toString().padLeft(2, '0')} - ${program.endTime?.hour.toString().padLeft(2, '0')}:${program.endTime?.minute.toString().padLeft(2, '0')} · ${formatDurationTextual(program.durationMinutes * 60000)}',
|
||||
style: theme.textTheme.bodySmall
|
||||
?.copyWith(color: theme.colorScheme.onSurfaceVariant),
|
||||
),
|
||||
if (program.summary != null &&
|
||||
program.summary!.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
program.summary!,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
maxLines: 4,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (program.isCurrentlyAiring)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
t.liveTv.live,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 11),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${channel.displayName} · ${program.startTime?.hour.toString().padLeft(2, '0')}:${program.startTime?.minute.toString().padLeft(2, '0')} - ${program.endTime?.hour.toString().padLeft(2, '0')}:${program.endTime?.minute.toString().padLeft(2, '0')} · ${formatDurationTextual(program.durationMinutes * 60000)}',
|
||||
style: theme.textTheme.bodySmall
|
||||
?.copyWith(color: theme.colorScheme.onSurfaceVariant),
|
||||
),
|
||||
if (program.summary != null &&
|
||||
program.summary!.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
program.summary!,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
maxLines: 4,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
|
||||
Reference in New Issue
Block a user