diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
index 2a8d3d1..51b2b81 100644
Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
index 41aaeb5..d1d6a5f 100644
Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index 5ae6356..651bf24 100644
Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index a81c98f..4ee9f9e 100644
Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index 600ec21..167958a 100644
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/assets/icon/app_icon.png b/assets/icon/app_icon.png
index 1844599..e5817ad 100644
Binary files a/assets/icon/app_icon.png and b/assets/icon/app_icon.png differ
diff --git a/assets/icon/icon_2.svg b/assets/icon/icon_2.svg
index f98fd1c..e0d79b8 100644
--- a/assets/icon/icon_2.svg
+++ b/assets/icon/icon_2.svg
@@ -25,9 +25,9 @@
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
- inkscape:zoom="0.44427667"
- inkscape:cx="707.89222"
- inkscape:cy="284.73248"
+ inkscape:zoom="0.68550502"
+ inkscape:cx="485.04386"
+ inkscape:cy="511.30187"
inkscape:window-width="1920"
inkscape:window-height="962"
inkscape:window-x="0"
@@ -132,32 +132,32 @@
xml:space="preserve"
style="font-weight:bold;font-size:100.118px;line-height:0;font-family:'Noto Sans Mono CJK KR';-inkscape-font-specification:'Noto Sans Mono CJK KR Bold';letter-spacing:0px;writing-mode:lr-tb;direction:ltr;fill:#ffffff;stroke:#000000;stroke-width:1.65284"
x="22.209259"
- y="505.82516"
+ y="501.82516"
id="text8">98.5 LPFM
MariposaPublicCommunityRadio
+ id="tspan12" />
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
index 4dd40f4..dc74b54 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
index bff3b8d..0f4a6d9 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
index 95c0587..bfceeaf 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
index d0d28a1..6002b16 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
index 964a8d1..54b4fff 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
index 8c1aca9..394214b 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
index c2f30f2..8c5638f 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
index 95c0587..bfceeaf 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
index 0cf4ded..4061a5f 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
index 01d1b8c..aa462a1 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png
index e838012..18d8937 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png
index d674695..d5c6d22 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png
index dd9dfbd..251a88c 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png
index 4d4e717..9143b86 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
index 01d1b8c..aa462a1 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
index e1c10dd..b6bc3c9 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png
index 2a8d3d1..51b2b81 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png
index a81c98f..4ee9f9e 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
index 467ed04..e9e7a15 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
index b1213a2..d3d906b 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
index d04b5f7..3f62d68 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/lib/live_info_business_object.dart b/lib/live_info_business_object.dart
new file mode 100644
index 0000000..44f1bec
--- /dev/null
+++ b/lib/live_info_business_object.dart
@@ -0,0 +1,356 @@
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:flutter/foundation.dart';
+import 'package:http/http.dart' as http;
+
+class LiveInfoBusinessObject extends ChangeNotifier {
+ LiveInfoBusinessObject({
+ this.endpoint = const String.fromEnvironment(
+ 'KRYZ_LIVE_INFO_URL',
+ defaultValue: 'http://kryz.airtime.pro/api/live-info',
+ ),
+ int refreshIntervalSeconds = 15,
+ this.requestTimeout = const Duration(seconds: 8),
+ }) : _refreshIntervalSeconds = refreshIntervalSeconds;
+
+ final String endpoint;
+ final Duration requestTimeout;
+
+ Timer? _pollTimer;
+ LiveInfoSnapshot? _snapshot;
+ bool _isLoading = false;
+ String? _errorMessage;
+ DateTime? _lastUpdatedAt;
+ int _refreshIntervalSeconds;
+ final List _recentPlayed = [];
+ final Set _recentKeys = {};
+
+ LiveInfoSnapshot? get snapshot => _snapshot;
+ bool get isLoading => _isLoading;
+ String? get errorMessage => _errorMessage;
+ DateTime? get lastUpdatedAt => _lastUpdatedAt;
+ int get refreshIntervalSeconds => _refreshIntervalSeconds;
+ List get recentPlayed => List.unmodifiable(_recentPlayed);
+
+ Future start() async {
+ await refresh();
+ _restartTimer();
+ }
+
+ void setRefreshIntervalSeconds(int seconds) {
+ if (seconds <= 0 || seconds == _refreshIntervalSeconds) {
+ return;
+ }
+
+ _refreshIntervalSeconds = seconds;
+ _restartTimer();
+ notifyListeners();
+ }
+
+ Future refresh() async {
+ _isLoading = true;
+ notifyListeners();
+
+ try {
+ final response = await http
+ .get(Uri.parse(endpoint))
+ .timeout(requestTimeout);
+ if (response.statusCode < 200 || response.statusCode >= 300) {
+ throw StateError('HTTP ${response.statusCode} from live-info API');
+ }
+
+ final dynamic decoded = jsonDecode(response.body);
+ if (decoded is! Map) {
+ throw StateError('Unexpected live-info payload shape');
+ }
+
+ final LiveInfoSnapshot next = LiveInfoSnapshot.fromJson(decoded);
+ _snapshot = next;
+ _lastUpdatedAt = DateTime.now();
+ _errorMessage = null;
+ _updateRecentPlayed(next);
+ } catch (error) {
+ _errorMessage = error.toString();
+ } finally {
+ _isLoading = false;
+ notifyListeners();
+ }
+ }
+
+ void _restartTimer() {
+ _pollTimer?.cancel();
+ _pollTimer = Timer.periodic(
+ Duration(seconds: _refreshIntervalSeconds),
+ (_) => refresh(),
+ );
+ }
+
+ void _updateRecentPlayed(LiveInfoSnapshot snapshot) {
+ void addIfTrack(LiveInfoTrackInfo? track, {required String source}) {
+ if (track == null || track.type != 'track') {
+ return;
+ }
+
+ final String key =
+ '$source:${track.metadata.id ?? 'none'}:${track.starts ?? 'unknown'}';
+ if (_recentKeys.contains(key)) {
+ return;
+ }
+
+ _recentKeys.add(key);
+ _recentPlayed.insert(
+ 0,
+ RecentPlayedItem(
+ title: track.displayTitle,
+ subtitle: track.displaySubtitle,
+ starts: track.starts,
+ ends: track.ends,
+ source: source,
+ ),
+ );
+ }
+
+ addIfTrack(snapshot.current, source: 'current');
+ addIfTrack(snapshot.previous, source: 'previous');
+
+ const int maxItems = 40;
+ if (_recentPlayed.length > maxItems) {
+ final List removed = _recentPlayed
+ .sublist(maxItems)
+ .toList(growable: false);
+ _recentPlayed.removeRange(maxItems, _recentPlayed.length);
+ for (final RecentPlayedItem item in removed) {
+ _recentKeys.remove(item.key);
+ }
+ }
+ }
+
+ @override
+ void dispose() {
+ _pollTimer?.cancel();
+ super.dispose();
+ }
+}
+
+class LiveInfoSnapshot {
+ LiveInfoSnapshot({
+ required this.env,
+ required this.schedulerTime,
+ required this.previous,
+ required this.current,
+ required this.next,
+ required this.currentShow,
+ required this.nextShow,
+ required this.timezone,
+ required this.timezoneOffset,
+ required this.sourceEnabled,
+ });
+
+ final String? env;
+ final String? schedulerTime;
+ final LiveInfoTrackInfo? previous;
+ final LiveInfoTrackInfo? current;
+ final LiveInfoTrackInfo? next;
+ final List currentShow;
+ final List nextShow;
+ final String? timezone;
+ final String? timezoneOffset;
+ final String? sourceEnabled;
+
+ factory LiveInfoSnapshot.fromJson(Map json) {
+ List parseShows(dynamic value) {
+ if (value is! List) {
+ return const [];
+ }
+
+ return value
+ .whereType