|
|
|
|
@ -17,9 +17,12 @@ class OccasionConfigModel {
|
|
|
|
|
/// default/current theme, regardless of any other field below.
|
|
|
|
|
final bool enabled;
|
|
|
|
|
|
|
|
|
|
/// Optional activation window. When both are present, the theme is only
|
|
|
|
|
/// considered active if `DateTime.now()` falls within [startDate, endDate].
|
|
|
|
|
/// When null, the [enabled] flag alone controls activation.
|
|
|
|
|
/// Activation window. Both must be present and `DateTime.now()` must fall
|
|
|
|
|
/// within [startDate, endDate] for this occasion to be eligible — a
|
|
|
|
|
/// missing date on either side makes the occasion inactive (dates are
|
|
|
|
|
/// mandatory, not an optional override of [enabled]). This lets several
|
|
|
|
|
/// occasions be scheduled in advance in the same list (see
|
|
|
|
|
/// [OccasionConfigModel.resolveActive]) without colliding.
|
|
|
|
|
final DateTime? startDate;
|
|
|
|
|
final DateTime? endDate;
|
|
|
|
|
|
|
|
|
|
@ -66,13 +69,9 @@ class OccasionConfigModel {
|
|
|
|
|
return OccasionConfigModel(
|
|
|
|
|
key: (json['key'] as String?) ?? 'none',
|
|
|
|
|
enabled: (json['enabled'] as bool?) ?? false,
|
|
|
|
|
// enabled: true,
|
|
|
|
|
startDate: _parseDate(json['start_date']),
|
|
|
|
|
endDate: _parseDate(json['end_date']),
|
|
|
|
|
|
|
|
|
|
// enabled: true,
|
|
|
|
|
// startDate: _parseDate("2026-09-30T00:00:00Z"),
|
|
|
|
|
// endDate: _parseDate("2026-10-07T00:00:00Z"),
|
|
|
|
|
|
|
|
|
|
primaryColorHex: json['primary_color'] as String?,
|
|
|
|
|
backgroundOpacityImage: json['background_opacity_image'] as String?,
|
|
|
|
|
backgroundImageOpacity: (json['background_image_opacity'] as num?)?.toDouble() ?? 1.0,
|
|
|
|
|
@ -80,6 +79,25 @@ class OccasionConfigModel {
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Parses the remote-config value as a JSON array of occasions — lets
|
|
|
|
|
/// several occasions be scheduled in advance (e.g. National Day, Founding
|
|
|
|
|
/// Day, Eid), each with its own non-overlapping [startDate]/[endDate]
|
|
|
|
|
/// window, all under the same `occasion_config` remote-config key.
|
|
|
|
|
static List<OccasionConfigModel> listFromJson(List<dynamic> jsonList) {
|
|
|
|
|
return jsonList.map((e) => OccasionConfigModel.fromJson(e as Map<String, dynamic>)).toList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Picks whichever scheduled occasion is enabled and whose
|
|
|
|
|
/// [startDate]/[endDate] window contains `now` — first match in list
|
|
|
|
|
/// order wins, so entries are expected not to overlap. Returns
|
|
|
|
|
/// [OccasionConfigModel.disabled] when nothing matches.
|
|
|
|
|
static OccasionConfigModel resolveActive(List<OccasionConfigModel> configs, [DateTime? now]) {
|
|
|
|
|
for (final config in configs) {
|
|
|
|
|
if (config.enabled && config.isWithinActiveWindow(now)) return config;
|
|
|
|
|
}
|
|
|
|
|
return OccasionConfigModel.disabled();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static DateTime? _parseDate(dynamic value) {
|
|
|
|
|
if (value == null) return null;
|
|
|
|
|
if (value is String && value.isNotEmpty) {
|
|
|
|
|
@ -89,11 +107,13 @@ class OccasionConfigModel {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Whether "now" falls inside the configured activation window.
|
|
|
|
|
/// If no window is configured, the [enabled] flag alone decides.
|
|
|
|
|
/// Both [startDate] and [endDate] are required — if either is missing,
|
|
|
|
|
/// this occasion is never considered active (see field doc above).
|
|
|
|
|
bool isWithinActiveWindow([DateTime? now]) {
|
|
|
|
|
if (startDate == null || endDate == null) return false;
|
|
|
|
|
final current = now ?? DateTime.now();
|
|
|
|
|
if (startDate != null && current.isBefore(startDate!)) return false;
|
|
|
|
|
if (endDate != null && current.isAfter(endDate!)) return false;
|
|
|
|
|
if (current.isBefore(startDate!)) return false;
|
|
|
|
|
if (current.isAfter(endDate!)) return false;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -125,4 +145,3 @@ class OccasionConfigModel {
|
|
|
|
|
'header_image': headerImage,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|