From 6893758a16a1a9fab748d9dfecc9451852723e83 Mon Sep 17 00:00:00 2001 From: kfj001 Date: Fri, 8 May 2026 22:42:10 -0700 Subject: [PATCH] Refactors visual widgets from live_info_panel into their own widgets. removes the play history widget from the live info panel. Co-authored-by: Copilot --- lib/live_info_panel.dart | 191 ++-------------------------------- lib/recently_played_list.dart | 87 ++++++++++++++++ lib/show_card.dart | 50 +++++++++ lib/time_formatter.dart | 11 ++ lib/track_card.dart | 68 ++++++++++++ 5 files changed, 224 insertions(+), 183 deletions(-) create mode 100644 lib/recently_played_list.dart create mode 100644 lib/show_card.dart create mode 100644 lib/time_formatter.dart create mode 100644 lib/track_card.dart diff --git a/lib/live_info_panel.dart b/lib/live_info_panel.dart index ef9d0bb..687bcc6 100644 --- a/lib/live_info_panel.dart +++ b/lib/live_info_panel.dart @@ -2,6 +2,10 @@ import 'package:flutter/material.dart'; import 'app_theme_business_object.dart'; import 'live_info_business_object.dart'; +import 'recently_played_list.dart'; +import 'show_card.dart'; +import 'time_formatter.dart'; +import 'track_card.dart'; class LiveInfoPanel extends StatelessWidget { const LiveInfoPanel({ @@ -88,7 +92,7 @@ class LiveInfoPanel extends StatelessWidget { child: Row( children: [ Expanded( - child: _TrackCard( + child: TrackCard( label: 'Now Playing', track: current, titleColor: themeColors.statusText, @@ -96,7 +100,7 @@ class LiveInfoPanel extends StatelessWidget { ), const SizedBox(width: 8), Expanded( - child: _TrackCard( + child: TrackCard( label: 'Coming Next', track: next, titleColor: themeColors.statusText, @@ -106,77 +110,17 @@ class LiveInfoPanel extends StatelessWidget { ), ), const SizedBox(height: 8), - _ShowCard( + ShowCard( label: 'On Air Show', show: currentShow, titleColor: themeColors.statusText, ), const SizedBox(height: 6), - _ShowCard( + ShowCard( label: 'Up Next Show', show: nextShow, titleColor: themeColors.statusText, ), - const SizedBox(height: 10), - Text( - 'Recently Played (runtime)', - style: Theme.of( - context, - ).textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w700), - ), - const SizedBox(height: 6), - Expanded( - child: Container( - decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.08), - borderRadius: BorderRadius.circular(12), - ), - child: liveInfo.recentPlayed.isEmpty - ? Center( - child: Text( - liveInfo.isLoading - ? 'Loading recent items...' - : 'No recent tracks captured yet', - style: Theme.of(context).textTheme.bodySmall, - ), - ) - : ListView.separated( - itemCount: liveInfo.recentPlayed.length, - separatorBuilder: (BuildContext context, int index) => - Divider( - height: 1, - color: Colors.black.withValues(alpha: 0.08), - ), - itemBuilder: (BuildContext context, int index) { - final RecentPlayedItem item = - liveInfo.recentPlayed[index]; - return ListTile( - dense: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 0, - ), - title: Text( - item.title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodyMedium - ?.copyWith(fontWeight: FontWeight.w600), - ), - subtitle: Text( - item.subtitle.isEmpty - ? _formatTimeWindow(item.starts, item.ends) - : '${item.subtitle} | ${_formatTimeWindow(item.starts, item.ends)}', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodySmall, - ), - leading: const Icon(Icons.queue_music_rounded), - ); - }, - ), - ), - ), const SizedBox(height: 6), Text( liveInfo.lastUpdatedAt == null @@ -190,122 +134,3 @@ class LiveInfoPanel extends StatelessWidget { ); } } - -class _TrackCard extends StatelessWidget { - const _TrackCard({ - required this.label, - required this.track, - required this.titleColor, - }); - - final String label; - final LiveInfoTrackInfo? track; - final Color titleColor; - - @override - Widget build(BuildContext context) { - final String title = track?.displayTitle ?? 'Unavailable'; - final String subtitle = (track?.displaySubtitle ?? '').trim(); - final String window = _formatTimeWindow(track?.starts, track?.ends); - - return Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.08), - borderRadius: BorderRadius.circular(14), - border: Border.all(color: Colors.black.withValues(alpha: 0.12)), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: Theme.of(context).textTheme.labelMedium?.copyWith( - fontWeight: FontWeight.w700, - color: titleColor, - ), - ), - const SizedBox(height: 6), - Text( - title, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: Theme.of( - context, - ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700), - ), - const SizedBox(height: 3), - Text( - subtitle.isEmpty ? 'No metadata provided' : subtitle, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodySmall, - ), - const Spacer(), - Text( - window, - style: Theme.of( - context, - ).textTheme.labelMedium?.copyWith(fontWeight: FontWeight.w600), - ), - ], - ), - ); - } -} - -class _ShowCard extends StatelessWidget { - const _ShowCard({ - required this.label, - required this.show, - required this.titleColor, - }); - - final String label; - final LiveInfoShowInfo? show; - final Color titleColor; - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: Colors.black.withValues(alpha: 0.06), - border: Border.all(color: Colors.black.withValues(alpha: 0.08)), - ), - child: Row( - children: [ - Icon(Icons.mic_external_on_rounded, color: titleColor, size: 18), - const SizedBox(width: 8), - Expanded( - child: Text( - '$label: ${show?.name ?? 'No scheduled show'}', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of( - context, - ).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600), - ), - ), - const SizedBox(width: 8), - Text( - _formatTimeWindow(show?.starts, show?.ends), - style: Theme.of(context).textTheme.labelSmall, - ), - ], - ), - ); - } -} - -String _formatTimeWindow(String? starts, String? ends) { - String formatOne(String? value) { - if (value == null || value.length < 16) { - return '--:--'; - } - return value.substring(11, 16); - } - - return '${formatOne(starts)} - ${formatOne(ends)}'; -} diff --git a/lib/recently_played_list.dart b/lib/recently_played_list.dart new file mode 100644 index 0000000..0b071fd --- /dev/null +++ b/lib/recently_played_list.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; + +import 'live_info_business_object.dart'; +import 'time_formatter.dart'; + +class RecentlyPlayedList extends StatelessWidget { + const RecentlyPlayedList({ + super.key, + required this.recentPlayed, + required this.isLoading, + }); + + final List recentPlayed; + final bool isLoading; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Recently Played (runtime)', + style: Theme.of( + context, + ).textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w700), + ), + const SizedBox(height: 6), + Expanded( + child: Container( + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(12), + ), + child: recentPlayed.isEmpty + ? Center( + child: Text( + isLoading + ? 'Loading recent items...' + : 'No recent tracks captured yet', + style: Theme.of(context).textTheme.bodySmall, + ), + ) + : ListView.separated( + itemCount: recentPlayed.length, + separatorBuilder: (BuildContext context, int index) => + Divider( + height: 1, + color: Colors.black.withValues(alpha: 0.08), + ), + itemBuilder: (BuildContext context, int index) { + final RecentPlayedItem item = recentPlayed[index]; + final String window = formatTimeWindow( + item.starts, + item.ends, + ); + + return ListTile( + dense: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 0, + ), + title: Text( + item.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyMedium + ?.copyWith(fontWeight: FontWeight.w600), + ), + subtitle: Text( + item.subtitle.isEmpty + ? window + : '${item.subtitle} | $window', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodySmall, + ), + leading: const Icon(Icons.queue_music_rounded), + ); + }, + ), + ), + ), + ], + ); + } +} diff --git a/lib/show_card.dart b/lib/show_card.dart new file mode 100644 index 0000000..f516e3f --- /dev/null +++ b/lib/show_card.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; + +import 'live_info_business_object.dart'; +import 'time_formatter.dart'; + +class ShowCard extends StatelessWidget { + const ShowCard({ + super.key, + required this.label, + required this.show, + required this.titleColor, + }); + + final String label; + final LiveInfoShowInfo? show; + final Color titleColor; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Colors.black.withValues(alpha: 0.06), + border: Border.all(color: Colors.black.withValues(alpha: 0.08)), + ), + child: Row( + children: [ + Icon(Icons.mic_external_on_rounded, color: titleColor, size: 18), + const SizedBox(width: 8), + Expanded( + child: Text( + '$label: ${show?.name ?? 'No scheduled show'}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of( + context, + ).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600), + ), + ), + const SizedBox(width: 8), + Text( + formatTimeWindow(show?.starts, show?.ends), + style: Theme.of(context).textTheme.labelSmall, + ), + ], + ), + ); + } +} diff --git a/lib/time_formatter.dart b/lib/time_formatter.dart new file mode 100644 index 0000000..3ca5bb3 --- /dev/null +++ b/lib/time_formatter.dart @@ -0,0 +1,11 @@ +String formatTimeWindow(String? starts, String? ends) { + String formatOne(String? value) { + if (value == null || value.length < 16) { + return '--:--'; + } + + return value.substring(11, 16); + } + + return '${formatOne(starts)} - ${formatOne(ends)}'; +} diff --git a/lib/track_card.dart b/lib/track_card.dart new file mode 100644 index 0000000..e0376f0 --- /dev/null +++ b/lib/track_card.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; + +import 'live_info_business_object.dart'; +import 'time_formatter.dart'; + +class TrackCard extends StatelessWidget { + const TrackCard({ + super.key, + required this.label, + required this.track, + required this.titleColor, + }); + + final String label; + final LiveInfoTrackInfo? track; + final Color titleColor; + + @override + Widget build(BuildContext context) { + final String title = track?.displayTitle ?? 'Unavailable'; + final String subtitle = (track?.displaySubtitle ?? '').trim(); + final String window = formatTimeWindow(track?.starts, track?.ends); + + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(14), + border: Border.all(color: Colors.black.withValues(alpha: 0.12)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: Theme.of(context).textTheme.labelMedium?.copyWith( + fontWeight: FontWeight.w700, + color: titleColor, + ), + ), + const SizedBox(height: 6), + Text( + title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: Theme.of( + context, + ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700), + ), + const SizedBox(height: 3), + Text( + subtitle.isEmpty ? 'No metadata provided' : subtitle, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodySmall, + ), + const Spacer(), + Text( + window, + style: Theme.of( + context, + ).textTheme.labelMedium?.copyWith(fontWeight: FontWeight.w600), + ), + ], + ), + ); + } +}