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/app_theme_business_object.dart b/lib/app_theme_business_object.dart
index 279462f..3341bba 100644
--- a/lib/app_theme_business_object.dart
+++ b/lib/app_theme_business_object.dart
@@ -3,6 +3,12 @@ import 'package:flutter/material.dart';
class AppThemeColors {
const AppThemeColors({
required this.primarySeed,
+ required this.secondarySeed,
+ required this.scaffoldGradientStart,
+ required this.scaffoldGradientEnd,
+ required this.shellSurface,
+ required this.shellBorder,
+ required this.accent,
required this.webViewBorder,
required this.webViewBackground,
required this.controlsGradientStart,
@@ -12,6 +18,12 @@ class AppThemeColors {
});
final Color primarySeed;
+ final Color secondarySeed;
+ final Color scaffoldGradientStart;
+ final Color scaffoldGradientEnd;
+ final Color shellSurface;
+ final Color shellBorder;
+ final Color accent;
final Color webViewBorder;
final Color webViewBackground;
final Color controlsGradientStart;
@@ -22,23 +34,35 @@ class AppThemeColors {
class AppThemeBusinessObject {
static const AppThemeColors lightColors = AppThemeColors(
- primarySeed: Color(0xFF607D8B),
- webViewBorder: Color(0x42000000),
+ primarySeed: Color(0xFFE36A18),
+ secondarySeed: Color(0xFF1663C7),
+ scaffoldGradientStart: Color(0xFFFFF3E3),
+ scaffoldGradientEnd: Color(0xFFE6F2FF),
+ shellSurface: Color(0xF7FFFFFF),
+ shellBorder: Color(0x1A1A4D86),
+ accent: Color(0xFF0F5DB6),
+ webViewBorder: Color(0x261A4D86),
webViewBackground: Color(0xFFFFFFFF),
- controlsGradientStart: Color(0xFFE1F5FE),
- controlsGradientEnd: Color(0xFFB3E5FC),
- controlsShadow: Color(0x22000000),
- statusText: Color(0xDD000000),
+ controlsGradientStart: Color(0xFFFFB465),
+ controlsGradientEnd: Color(0xFF69ADFF),
+ controlsShadow: Color(0x24114B7A),
+ statusText: Color(0xFF13243D),
);
static const AppThemeColors darkColors = AppThemeColors(
- primarySeed: Color(0xFF37474F),
- webViewBorder: Color(0x66FFFFFF),
- webViewBackground: Color(0xFF0F1417),
- controlsGradientStart: Color(0xFF1B2731),
- controlsGradientEnd: Color(0xFF233744),
+ primarySeed: Color(0xFFFF9D59),
+ secondarySeed: Color(0xFF76B8FF),
+ scaffoldGradientStart: Color(0xFF0F1C30),
+ scaffoldGradientEnd: Color(0xFF31190C),
+ shellSurface: Color(0xD9162438),
+ shellBorder: Color(0x3366AAFF),
+ accent: Color(0xFFFFBC7E),
+ webViewBorder: Color(0x3378B7FF),
+ webViewBackground: Color(0xFF0C1420),
+ controlsGradientStart: Color(0xFF275792),
+ controlsGradientEnd: Color(0xFF9A4F1F),
controlsShadow: Color(0x44000000),
- statusText: Color(0xE6FFFFFF),
+ statusText: Color(0xFFF4F7FF),
);
static bool isDarkMode(BuildContext context) {
@@ -50,19 +74,62 @@ class AppThemeBusinessObject {
}
static ThemeData lightTheme() {
+ final ColorScheme scheme =
+ ColorScheme.fromSeed(
+ seedColor: lightColors.primarySeed,
+ brightness: Brightness.light,
+ ).copyWith(
+ primary: lightColors.primarySeed,
+ secondary: lightColors.secondarySeed,
+ tertiary: const Color(0xFF2C85E1),
+ surface: const Color(0xFFFFFBF7),
+ onSurface: lightColors.statusText,
+ );
+
return ThemeData(
- colorScheme: ColorScheme.fromSeed(seedColor: lightColors.primarySeed),
useMaterial3: true,
+ colorScheme: scheme,
+ scaffoldBackgroundColor: scheme.surface,
+ elevatedButtonTheme: ElevatedButtonThemeData(
+ style: ElevatedButton.styleFrom(
+ backgroundColor: scheme.primary,
+ foregroundColor: scheme.onPrimary,
+ padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(16),
+ ),
+ ),
+ ),
);
}
static ThemeData darkTheme() {
+ final ColorScheme scheme =
+ ColorScheme.fromSeed(
+ seedColor: darkColors.primarySeed,
+ brightness: Brightness.dark,
+ ).copyWith(
+ primary: darkColors.primarySeed,
+ secondary: darkColors.secondarySeed,
+ tertiary: const Color(0xFF4E97E8),
+ surface: const Color(0xFF111A27),
+ onSurface: darkColors.statusText,
+ );
+
return ThemeData(
- colorScheme: ColorScheme.fromSeed(
- seedColor: darkColors.primarySeed,
- brightness: Brightness.dark,
- ),
useMaterial3: true,
+ colorScheme: scheme,
+ scaffoldBackgroundColor: scheme.surface,
+ elevatedButtonTheme: ElevatedButtonThemeData(
+ style: ElevatedButton.styleFrom(
+ backgroundColor: scheme.primary,
+ foregroundColor: scheme.onPrimary,
+ padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(16),
+ ),
+ ),
+ ),
);
}
}
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