248 lines
12 KiB
Dart
248 lines
12 KiB
Dart
import 'package:cached_network_image/cached_network_image.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
import 'package:onsolgo/core/constants.dart';
|
|
|
|
/// Public feed of artist-scheduled releases (`upcoming` collection).
|
|
class ComingSoon extends StatelessWidget {
|
|
const ComingSoon({super.key});
|
|
|
|
String _fmtDate(DateTime d) =>
|
|
'${d.month.toString().padLeft(2, '0')}/${d.day.toString().padLeft(2, '0')}/${d.year}';
|
|
|
|
String _countdown(Timestamp? ts, {required bool dateTbd}) {
|
|
if (dateTbd) return 'TBD';
|
|
if (ts == null) return '';
|
|
final t = ts.toDate();
|
|
final now = DateTime.now();
|
|
final endOfToday = DateTime(now.year, now.month, now.day, 23, 59, 59);
|
|
if (t.isBefore(endOfToday) && t.year == now.year && t.month == now.month && t.day == now.day) {
|
|
return 'Today';
|
|
}
|
|
if (t.isBefore(now)) return 'Out now';
|
|
final diff = t.difference(now);
|
|
if (diff.inDays >= 1) return 'in ${diff.inDays}d';
|
|
if (diff.inHours >= 1) return 'in ${diff.inHours}h';
|
|
return 'in ${diff.inMinutes}m';
|
|
}
|
|
|
|
int _sortUpcoming(DocumentSnapshot a, DocumentSnapshot b) {
|
|
final ad = a.data() as Map<String, dynamic>;
|
|
final bd = b.data() as Map<String, dynamic>;
|
|
final af = ad['featured'] == true;
|
|
final bf = bd['featured'] == true;
|
|
if (af != bf) return af ? -1 : 1;
|
|
final atbd = ad['dateTbd'] == true;
|
|
final btbd = bd['dateTbd'] == true;
|
|
if (atbd != btbd) return atbd ? 1 : -1;
|
|
final ta = (ad['targetDate'] as Timestamp?)?.millisecondsSinceEpoch ?? 0;
|
|
final tb = (bd['targetDate'] as Timestamp?)?.millisecondsSinceEpoch ?? 0;
|
|
return ta.compareTo(tb);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: Colors.black,
|
|
body: CustomScrollView(
|
|
slivers: [
|
|
SliverToBoxAdapter(
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(20, 56, 20, 12),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Icon(Icons.auto_awesome, size: 40, color: kOnsolGold),
|
|
const SizedBox(height: 12),
|
|
const Text(
|
|
'THE SUN IS RISING',
|
|
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, letterSpacing: 3),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'New series and chapter drops from ManaA artists.',
|
|
style: TextStyle(color: Colors.grey[400], letterSpacing: 0.5, height: 1.3),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
StreamBuilder<QuerySnapshot>(
|
|
stream: FirebaseFirestore.instance.collection('upcoming').limit(80).snapshots(),
|
|
builder: (context, snapshot) {
|
|
if (snapshot.hasError) {
|
|
return SliverFillRemaining(
|
|
child: Center(child: Text('Could not load schedule.\n${snapshot.error}', textAlign: TextAlign.center, style: const TextStyle(color: Colors.white54))),
|
|
);
|
|
}
|
|
if (!snapshot.hasData) {
|
|
return const SliverFillRemaining(child: Center(child: CircularProgressIndicator(color: kOnsolGold)));
|
|
}
|
|
final docs = snapshot.data!.docs.toList()..sort(_sortUpcoming);
|
|
if (docs.isEmpty) {
|
|
return SliverFillRemaining(
|
|
child: Center(
|
|
child: Text('Check back soon for announcements.', style: TextStyle(color: Colors.grey[500])),
|
|
),
|
|
);
|
|
}
|
|
return SliverPadding(
|
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 32),
|
|
sliver: SliverList(
|
|
delegate: SliverChildBuilderDelegate(
|
|
(context, i) {
|
|
final doc = docs[i];
|
|
final d = doc.data() as Map<String, dynamic>;
|
|
final kind = d['kind'] as String? ?? '';
|
|
final seriesTitle = d['seriesTitle'] ?? 'Untitled';
|
|
final artist = d['authorName'] ?? 'Artist';
|
|
final dateTbd = d['dateTbd'] as bool? ?? false;
|
|
final ts = d['targetDate'] as Timestamp?;
|
|
final dateStr = dateTbd ? 'Date TBD' : (ts != null ? _fmtDate(ts.toDate()) : '—');
|
|
final cd = _countdown(ts, dateTbd: dateTbd);
|
|
final ch = d['chapterNumber'];
|
|
final featured = d['featured'] == true;
|
|
final description = (d['description'] as String?)?.trim() ?? '';
|
|
final coverUrl = (d['teaserCoverUrl'] as String?)?.trim() ?? '';
|
|
|
|
final headline = kind == 'new_series'
|
|
? '$seriesTitle · $artist'
|
|
: 'New chapter · $seriesTitle${ch != null ? ' (Ch $ch)' : ''}';
|
|
|
|
final borderColor = featured ? kOnsolGold : kOnsolGold.withValues(alpha: 0.25);
|
|
final borderWidth = featured ? 2.5 : 1.0;
|
|
|
|
return Container(
|
|
margin: const EdgeInsets.only(bottom: 14),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(color: borderColor, width: borderWidth),
|
|
boxShadow: featured
|
|
? [
|
|
BoxShadow(
|
|
color: kOnsolGold.withValues(alpha: 0.18),
|
|
blurRadius: 16,
|
|
spreadRadius: 0,
|
|
),
|
|
]
|
|
: null,
|
|
),
|
|
child: Card(
|
|
color: featured ? const Color(0xFF1A1708) : Colors.grey[900],
|
|
margin: EdgeInsets.zero,
|
|
elevation: featured ? 4 : 0,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(13)),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(14),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
if (featured)
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: 8),
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.star_rounded, color: kOnsolGold, size: 18),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
'FEATURED DROP',
|
|
style: TextStyle(
|
|
color: kOnsolGold,
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w800,
|
|
letterSpacing: 1.4,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
if (coverUrl.isNotEmpty) ...[
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(10),
|
|
child: CachedNetworkImage(
|
|
imageUrl: coverUrl,
|
|
width: 92,
|
|
height: 130,
|
|
fit: BoxFit.cover,
|
|
alignment: Alignment.topCenter,
|
|
),
|
|
),
|
|
const SizedBox(width: 14),
|
|
],
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
headline,
|
|
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, height: 1.3, fontSize: 15),
|
|
),
|
|
if (kind != 'new_series') ...[
|
|
const SizedBox(height: 4),
|
|
Text(artist, style: TextStyle(color: Colors.grey[500], fontSize: 12)),
|
|
],
|
|
if (description.isNotEmpty) ...[
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
description,
|
|
maxLines: 4,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(color: Colors.grey[300], fontSize: 13, height: 1.35),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
Icon(Icons.calendar_today, size: 14, color: Colors.grey[500]),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
dateTbd ? 'Target: to be announced' : 'Target: $dateStr',
|
|
style: TextStyle(color: Colors.grey[400], fontSize: 12),
|
|
),
|
|
const Spacer(),
|
|
if (!dateTbd && cd.isNotEmpty)
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: kOnsolGold.withValues(alpha: 0.12),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Text(cd, style: const TextStyle(color: kOnsolGold, fontSize: 11, fontWeight: FontWeight.bold)),
|
|
)
|
|
else if (dateTbd)
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withValues(alpha: 0.08),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: const Text('SOON', style: TextStyle(color: Colors.white70, fontSize: 11, fontWeight: FontWeight.bold)),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
childCount: docs.length,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|