import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_storage/firebase_storage.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:onsolgo/core/constants.dart'; const String _kUpcomingCollection = 'upcoming'; const String _kindNewSeries = 'new_series'; const String _kindChapterDrop = 'chapter_drop'; /// Sentinel end date for Firestore ordering when "date TBD" (sorts after real dates). final DateTime _kTbdSortDate = DateTime(2099, 12, 31, 23, 59); /// Artists manage teasers shown on the Soon tab. class ArtistUpcomingScreen extends StatelessWidget { const ArtistUpcomingScreen({super.key}); String _fmtDate(DateTime d) => '${d.month.toString().padLeft(2, '0')}/${d.day.toString().padLeft(2, '0')}/${d.year}'; Future _openEditor( BuildContext context, { DocumentSnapshot? existing, }) async { final uid = FirebaseAuth.instance.currentUser?.uid; if (uid == null) return; final userSnap = await FirebaseFirestore.instance.collection('users').doc(uid).get(); final userName = (userSnap.data()?['username'] as String?)?.trim() ?? 'Artist'; final mangaSnap = await FirebaseFirestore.instance.collection('manga').where('authorId', isEqualTo: uid).get(); final mangaDocs = mangaSnap.docs; if (!context.mounted) return; String kind = existing?.get('kind') as String? ?? _kindNewSeries; final titleCtrl = TextEditingController(text: existing?.get('seriesTitle') as String? ?? ''); final descCtrl = TextEditingController(text: existing?.get('description') as String? ?? ''); final chCtrl = TextEditingController( text: existing != null && existing.get('chapterNumber') != null ? '${existing.get('chapterNumber')}' : '', ); String? mangaId = existing?.get('mangaId') as String?; final existingTs = existing?.get('targetDate') as Timestamp?; DateTime target = existingTs?.toDate() ?? DateTime.now().add(const Duration(days: 7)); if (existing != null && (existing.get('dateTbd') as bool? ?? false)) { target = DateTime.now().add(const Duration(days: 7)); } bool dateTbd = existing?.get('dateTbd') as bool? ?? false; bool featured = existing?.get('featured') as bool? ?? false; XFile? pickedCover; final existingCoverUrl = existing?.get('teaserCoverUrl') as String?; await showDialog( context: context, builder: (ctx) => StatefulBuilder( builder: (ctx, setS) => AlertDialog( backgroundColor: Colors.grey[900], title: Text(existing == null ? 'Announce release' : 'Edit announcement', style: const TextStyle(color: kOnsolGold, fontSize: 16)), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Text('Type', style: TextStyle(color: Colors.white70, fontSize: 12)), const SizedBox(height: 4), DropdownButton( isExpanded: true, dropdownColor: Colors.black87, value: kind, items: const [ DropdownMenuItem(value: _kindNewSeries, child: Text('New series (coming soon)', style: TextStyle(color: Colors.white))), DropdownMenuItem(value: _kindChapterDrop, child: Text('New chapter (existing series)', style: TextStyle(color: Colors.white))), ], onChanged: (v) => setS(() { kind = v ?? _kindNewSeries; if (kind == _kindNewSeries) mangaId = null; }), ), if (kind == _kindChapterDrop) ...[ const SizedBox(height: 12), const Text('Series', style: TextStyle(color: Colors.white70, fontSize: 12)), const SizedBox(height: 4), DropdownButton( isExpanded: true, dropdownColor: Colors.black87, value: mangaId != null && mangaDocs.any((d) => d.id == mangaId) ? mangaId : null, hint: const Text('Select series', style: TextStyle(color: Colors.white54)), items: mangaDocs .map((d) => DropdownMenuItem(value: d.id, child: Text(d['title'] ?? d.id, style: const TextStyle(color: Colors.white)))) .toList(), onChanged: (v) => setS(() { mangaId = v; try { final m = mangaDocs.firstWhere((d) => d.id == v); titleCtrl.text = m['title'] ?? ''; } catch (_) {} }), ), const SizedBox(height: 8), TextField( controller: chCtrl, keyboardType: TextInputType.number, style: const TextStyle(color: Colors.white), decoration: const InputDecoration( labelText: 'Chapter # (optional)', labelStyle: TextStyle(color: Colors.white70), ), ), ] else ...[ const SizedBox(height: 12), TextField( controller: titleCtrl, style: const TextStyle(color: Colors.white), decoration: const InputDecoration( labelText: 'Series title', labelStyle: TextStyle(color: Colors.white70), hintText: 'Working title', ), ), ], const SizedBox(height: 12), TextField( controller: descCtrl, maxLines: 4, style: const TextStyle(color: Colors.white), decoration: const InputDecoration( labelText: 'Description (Soon tab)', labelStyle: TextStyle(color: Colors.white70), hintText: 'Teaser, synopsis, or hook for readers', alignLabelWithHint: true, ), ), const SizedBox(height: 12), const Text('Teaser cover', style: TextStyle(color: Colors.white70, fontSize: 12)), const SizedBox(height: 6), Row( children: [ OutlinedButton.icon( onPressed: () async { final image = await ImagePicker().pickImage(source: ImageSource.gallery, imageQuality: 80); if (image != null) setS(() => pickedCover = image); }, icon: const Icon(Icons.add_photo_alternate_outlined, color: kOnsolGold, size: 20), label: const Text('Choose image', style: TextStyle(color: kOnsolGold, fontSize: 13)), ), ], ), if (pickedCover != null || (existingCoverUrl != null && existingCoverUrl.isNotEmpty)) ...[ const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular(8), child: AspectRatio( aspectRatio: 3 / 4, child: pickedCover != null ? FutureBuilder( future: pickedCover!.readAsBytes(), builder: (context, snap) { if (!snap.hasData) return const ColoredBox(color: Colors.white10); return Image.memory(snap.data!, fit: BoxFit.cover); }, ) : CachedNetworkImage(imageUrl: existingCoverUrl!, fit: BoxFit.cover), ), ), ], const SizedBox(height: 12), SwitchListTile( contentPadding: EdgeInsets.zero, title: const Text('Highlight on Soon tab', style: TextStyle(color: Colors.white70, fontSize: 13)), subtitle: const Text('Featured styling for this announcement', style: TextStyle(color: Colors.white38, fontSize: 11)), value: featured, activeThumbColor: kOnsolGold, onChanged: (v) => setS(() => featured = v), ), SwitchListTile( contentPadding: EdgeInsets.zero, title: const Text('Release date TBD', style: TextStyle(color: Colors.white70, fontSize: 13)), subtitle: const Text('No specific day yet — shows as “Date TBD”', style: TextStyle(color: Colors.white38, fontSize: 11)), value: dateTbd, activeThumbColor: kOnsolGold, onChanged: (v) => setS(() => dateTbd = v), ), if (!dateTbd) ListTile( contentPadding: EdgeInsets.zero, title: const Text('Release date', style: TextStyle(color: Colors.white70, fontSize: 12)), subtitle: Text(_fmtDate(target), style: const TextStyle(color: kOnsolGold, fontWeight: FontWeight.bold)), trailing: IconButton( icon: const Icon(Icons.calendar_month, color: kOnsolGold), onPressed: () async { final picked = await showDatePicker( context: ctx, initialDate: target, firstDate: DateTime.now().subtract(const Duration(days: 1)), lastDate: DateTime.now().add(const Duration(days: 365 * 3)), ); if (picked != null) setS(() => target = picked); }, ), ), ], ), ), actions: [ TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Cancel', style: TextStyle(color: Colors.grey))), ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: kOnsolGold, foregroundColor: Colors.black), onPressed: () async { final seriesTitle = titleCtrl.text.trim(); if (seriesTitle.isEmpty) return; if (kind == _kindChapterDrop && (mangaId == null || mangaId!.isEmpty)) return; num? chNum = num.tryParse(chCtrl.text.trim()); final targetTs = dateTbd ? Timestamp.fromDate(_kTbdSortDate) : Timestamp.fromDate(DateTime(target.year, target.month, target.day, 23, 59)); final payload = { 'kind': kind, 'authorId': uid, 'authorName': userName, 'seriesTitle': seriesTitle, 'mangaId': kind == _kindChapterDrop ? mangaId : null, 'chapterNumber': chNum, 'targetDate': targetTs, 'dateTbd': dateTbd, 'featured': featured, 'description': descCtrl.text.trim(), 'updatedAt': FieldValue.serverTimestamp(), }; if (pickedCover == null && existingCoverUrl != null && existingCoverUrl.isNotEmpty) { payload['teaserCoverUrl'] = existingCoverUrl; } try { if (existing == null) { payload['createdAt'] = FieldValue.serverTimestamp(); final docRef = FirebaseFirestore.instance.collection(_kUpcomingCollection).doc(); await docRef.set(payload); if (pickedCover != null) { final ref = FirebaseStorage.instance.ref().child('upcoming_covers').child('${docRef.id}.jpg'); await ref.putData(await pickedCover!.readAsBytes(), SettableMetadata(contentType: 'image/jpeg')); final url = await ref.getDownloadURL(); await docRef.update({'teaserCoverUrl': url}); } } else { await existing.reference.update(payload); if (pickedCover != null) { final ref = FirebaseStorage.instance.ref().child('upcoming_covers').child('${existing.id}.jpg'); await ref.putData(await pickedCover!.readAsBytes(), SettableMetadata(contentType: 'image/jpeg')); final url = await ref.getDownloadURL(); await existing.reference.update({'teaserCoverUrl': url}); } } if (ctx.mounted) Navigator.pop(ctx); } catch (e) { if (ctx.mounted) { ScaffoldMessenger.of(ctx).showSnackBar(SnackBar(content: Text('Save failed: $e'))); } } }, child: Text(existing == null ? 'Publish' : 'Save'), ), ], ), ), ); } @override Widget build(BuildContext context) { final uid = FirebaseAuth.instance.currentUser?.uid ?? ''; return Scaffold( backgroundColor: Colors.black, appBar: AppBar( backgroundColor: Colors.amber[900], title: const Text('UPCOMING RELEASES'), ), floatingActionButton: FloatingActionButton( backgroundColor: kOnsolGold, foregroundColor: Colors.black, onPressed: uid.isEmpty ? null : () => _openEditor(context), child: const Icon(Icons.add), ), body: uid.isEmpty ? const Center(child: Text('Sign in', style: TextStyle(color: Colors.white54))) : StreamBuilder( stream: FirebaseFirestore.instance.collection(_kUpcomingCollection).where('authorId', isEqualTo: uid).snapshots(), builder: (context, snapshot) { if (snapshot.hasError) { return Center(child: Text('Error: ${snapshot.error}', style: const TextStyle(color: Colors.redAccent))); } if (!snapshot.hasData) return const Center(child: CircularProgressIndicator(color: kOnsolGold)); final docs = snapshot.data!.docs.toList() ..sort((a, b) { final ad = a.data() as Map; final bd = b.data() as Map; 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); }); if (docs.isEmpty) { return Center( child: Padding( padding: const EdgeInsets.all(24), child: Text( 'No announcements yet.\nTap + to add a new series teaser or a chapter drop.', textAlign: TextAlign.center, style: TextStyle(color: Colors.grey[500], height: 1.4), ), ), ); } return ListView.builder( padding: const EdgeInsets.all(16), itemCount: docs.length, itemBuilder: (context, i) { final doc = docs[i]; final d = doc.data() as Map; final kind = d['kind'] as String? ?? ''; final title = d['seriesTitle'] ?? ''; final dateTbd = d['dateTbd'] as bool? ?? false; final ts = d['targetDate'] as Timestamp?; final dateStr = dateTbd ? 'Date TBD' : (ts != null ? _fmtDate(ts.toDate()) : '—'); final featured = d['featured'] == true; final coverUrl = d['teaserCoverUrl'] as String?; final subtitle = kind == _kindNewSeries ? 'New series · $dateStr' : 'New chapter · ${d['chapterNumber'] != null ? 'Ch ${d['chapterNumber']} · ' : ''}$dateStr'; return Card( color: Colors.grey[900], margin: const EdgeInsets.only(bottom: 10), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide( color: featured ? kOnsolGold : Colors.white12, width: featured ? 2 : 1, ), ), child: ListTile( isThreeLine: true, leading: coverUrl != null && coverUrl.isNotEmpty ? ClipRRect( borderRadius: BorderRadius.circular(6), child: CachedNetworkImage(imageUrl: coverUrl, width: 56, height: 80, fit: BoxFit.cover), ) : const Icon(Icons.image_outlined, color: Colors.white24), title: Row( children: [ Expanded(child: Text(title, style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.white))), if (featured) Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: kOnsolGold.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(4), ), child: const Text('FEATURED', style: TextStyle(color: kOnsolGold, fontSize: 9, fontWeight: FontWeight.bold)), ), ], ), subtitle: Text(subtitle, style: TextStyle(color: Colors.grey[400], fontSize: 12)), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( icon: const Icon(Icons.edit, color: kOnsolGold, size: 20), onPressed: () => _openEditor(context, existing: doc), ), IconButton( icon: const Icon(Icons.delete_outline, color: Colors.redAccent, size: 20), onPressed: () async { final ok = await showDialog( context: context, builder: (c) => AlertDialog( backgroundColor: Colors.grey[900], title: const Text('Remove?', style: TextStyle(color: Colors.white)), actions: [ TextButton(onPressed: () => Navigator.pop(c, false), child: const Text('Cancel')), TextButton(onPressed: () => Navigator.pop(c, true), child: const Text('Delete', style: TextStyle(color: Colors.red))), ], ), ); if (ok == true) await doc.reference.delete(); }, ), ], ), ), ); }, ); }, ), ); } }