Initial commit
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:onsolgo/core/constants.dart';
|
||||
import 'package:onsolgo/screens/library/series_detail.dart';
|
||||
|
||||
class LibraryGrid extends StatelessWidget {
|
||||
const LibraryGrid({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
// THE BRAND BANNER
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
color: Colors.black,
|
||||
padding: const EdgeInsets.only(top: 50, bottom: 10),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: kOnsolBanner,
|
||||
height: 100,
|
||||
fit: BoxFit.contain
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// THE MANAA GRID
|
||||
StreamBuilder<QuerySnapshot>(
|
||||
stream: FirebaseFirestore.instance.collection('manga').snapshots(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) return const SliverToBoxAdapter(child: Center(child: Text("Error Connection")));
|
||||
if (!snapshot.hasData) return const SliverToBoxAdapter(child: Center(child: CircularProgressIndicator()));
|
||||
|
||||
final docs = snapshot.data!.docs;
|
||||
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
sliver: SliverGrid(
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: 0.65,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 20
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => _MangaGridCard(manga: docs[index]),
|
||||
childCount: docs.length,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MangaGridCard extends StatelessWidget {
|
||||
final QueryDocumentSnapshot manga;
|
||||
const _MangaGridCard({required this.manga});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final mData = manga.data() as Map<String, dynamic>;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => SeriesDetail(manga: manga))
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: mData['coverUrl'] ?? '',
|
||||
fit: BoxFit.cover,
|
||||
width: double.infinity,
|
||||
alignment: Alignment.topCenter,
|
||||
// Fix for desaturation: Ensure no filters are applied
|
||||
placeholder: (context, url) => Container(color: Colors.grey[900]),
|
||||
),
|
||||
),
|
||||
// READ COUNT OVERLAY
|
||||
Positioned(
|
||||
bottom: 8, left: 8,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.7),
|
||||
borderRadius: BorderRadius.circular(4)
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.remove_red_eye, size: 12, color: Colors.white),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
"${safeInt(mData['reads'])}",
|
||||
style: const TextStyle(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
mData['title'].toString().toUpperCase(),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: Colors.white)
|
||||
),
|
||||
Text(
|
||||
mData['author'] ?? 'Artist',
|
||||
style: const TextStyle(fontSize: 11, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:google_mobile_ads/google_mobile_ads.dart';
|
||||
import 'package:onsolgo/core/constants.dart';
|
||||
import 'package:onsolgo/screens/reader/reader_view.dart';
|
||||
import 'package:onsolgo/widgets/energy_user_chip.dart';
|
||||
import 'package:onsolgo/screens/auth/tier_comparison_screen.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
class SeriesDetail extends StatefulWidget {
|
||||
final DocumentSnapshot manga;
|
||||
const SeriesDetail({super.key, required this.manga});
|
||||
@override
|
||||
State<SeriesDetail> createState() => _SeriesDetailState();
|
||||
}
|
||||
|
||||
class _SeriesDetailState extends State<SeriesDetail> {
|
||||
InterstitialAd? _interstitialAd;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (!kIsWeb) _loadInterstitial();
|
||||
}
|
||||
|
||||
void _loadInterstitial() {
|
||||
InterstitialAd.load(adUnitId: "ca-app-pub-3940256099942544/1033173712", request: const AdRequest(), adLoadCallback: InterstitialAdLoadCallback(onAdLoaded: (ad) => setState(() => _interstitialAd = ad), onAdFailedToLoad: (e) => debugPrint('$e')));
|
||||
}
|
||||
|
||||
Future<void> _handleAccess(DocumentSnapshot ch) async {
|
||||
final uid = FirebaseAuth.instance.currentUser?.uid; if (uid == null) return;
|
||||
final userDoc = await FirebaseFirestore.instance.collection('users').doc(uid).get();
|
||||
final uData = userDoc.data() ?? <String, dynamic>{};
|
||||
|
||||
// ELITE BYPASS: Rank 5 or Paid Tier
|
||||
if ((uData['tier'] ?? 'free') != 'free' || safeInt(uData['rankLevel']) == 5 || (uData['isVerified'] ?? false)) {
|
||||
_openReader(ch); return;
|
||||
}
|
||||
|
||||
int energy = safeInt(uData['energy'] ?? 2);
|
||||
if (energy > 0) {
|
||||
await userDoc.reference.update({'energy': energy - 1, 'lastEnergyRefill': FieldValue.serverTimestamp()});
|
||||
if (_interstitialAd != null && !kIsWeb) {
|
||||
_interstitialAd!.fullScreenContentCallback = FullScreenContentCallback(onAdDismissedFullScreenContent: (ad) { ad.dispose(); _openReader(ch); _loadInterstitial(); });
|
||||
_interstitialAd!.show(); _interstitialAd = null;
|
||||
} else { _openReader(ch); }
|
||||
} else { _showExhausted(); }
|
||||
}
|
||||
|
||||
void _openReader(DocumentSnapshot ch) { Navigator.push(context, MaterialPageRoute(builder: (c) => ReaderView(manga: widget.manga, chapter: ch))); }
|
||||
void _showExhausted() { showDialog(context: context, builder: (ctx) => AlertDialog(backgroundColor: Colors.grey[900], shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15), side: const BorderSide(color: Colors.red)), title: const Text("ENERGY DEPLETED"), content: const Text("Recharge in 24 hours or upgrade to OGO+."), actions: [ElevatedButton(onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (c) => const TierComparisonScreen())), child: const Text("UPGRADE"))])); }
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final md = widget.manga.data() as Map<String, dynamic>;
|
||||
final String cover = (md['coverUrl'] as String?)?.trim() ?? '';
|
||||
final String banner = (md['bannerUrl'] as String?)?.trim() ?? '';
|
||||
final bool hasBanner = banner.isNotEmpty;
|
||||
final String heroUrl = hasBanner ? banner : cover;
|
||||
final String uid = FirebaseAuth.instance.currentUser?.uid ?? "";
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black, extendBodyBehindAppBar: true,
|
||||
appBar: AppBar(backgroundColor: Colors.transparent, elevation: 0, actions: [IconButton(
|
||||
icon: const Icon(Icons.share_outlined),
|
||||
onPressed: () => SharePlus.instance.share(
|
||||
ShareParams(text: 'Check out ${md['title']} on ONSOL-GO!\n\n$kOnsolAppWebUrl'),
|
||||
),
|
||||
)]),
|
||||
body: SingleChildScrollView(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Stack(children: [
|
||||
CachedNetworkImage(
|
||||
imageUrl: heroUrl,
|
||||
width: double.infinity,
|
||||
height: hasBanner ? 300 : 380,
|
||||
fit: BoxFit.cover,
|
||||
alignment: Alignment.topCenter,
|
||||
placeholder: (context, url) => Container(
|
||||
height: hasBanner ? 300 : 380,
|
||||
color: Colors.grey[900],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: hasBanner ? 301 : 381,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Colors.transparent, Colors.black.withValues(alpha: 0.9), Colors.black],
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
if (hasBanner)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(md['title'] ?? '', style: const TextStyle(fontSize: 26, fontWeight: FontWeight.bold)),
|
||||
Text(md['author'] ?? '', style: const TextStyle(fontSize: 17, color: kOnsolGold)),
|
||||
],
|
||||
)
|
||||
else ...[
|
||||
Text(md['title'] ?? '', style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold)),
|
||||
Text(md['author'] ?? '', style: const TextStyle(fontSize: 18, color: kOnsolGold)),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
Align(alignment: Alignment.centerLeft, child: EnergyUserChip(uid: uid)),
|
||||
const SizedBox(height: 16),
|
||||
const Text("SYNOPSIS", style: TextStyle(fontSize: 10, color: Colors.grey, fontWeight: FontWeight.bold, letterSpacing: 2)),
|
||||
Text(md['synopsis'] ?? "No transmission recorded.", style: const TextStyle(color: Colors.white70, height: 1.4)),
|
||||
const SizedBox(height: 30),
|
||||
const Text("CHAPTERS", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
StreamBuilder<QuerySnapshot>(
|
||||
stream: widget.manga.reference.collection('chapters').orderBy('chapterNumber', descending: true).snapshots(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) return const CircularProgressIndicator();
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.only(top: 10), shrinkWrap: true, physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: snapshot.data!.docs.length,
|
||||
itemBuilder: (c, i) => _ChapterTile(ch: snapshot.data!.docs[i], manga: widget.manga, uid: uid, onTap: () => _handleAccess(snapshot.data!.docs[i])),
|
||||
);
|
||||
},
|
||||
)
|
||||
]),
|
||||
),
|
||||
])),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ChapterTile extends StatelessWidget {
|
||||
final DocumentSnapshot ch; final DocumentSnapshot manga; final String uid; final VoidCallback onTap;
|
||||
const _ChapterTile({required this.ch, required this.manga, required this.uid, required this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cd = ch.data() as Map<String, dynamic>;
|
||||
return StreamBuilder<DocumentSnapshot>(
|
||||
stream: FirebaseFirestore.instance.collection('users').doc(uid).collection('progress').doc(manga.id).snapshots(),
|
||||
builder: (context, prog) {
|
||||
double p = (prog.hasData && prog.data!.exists && prog.data!['lastChapter'] == cd['chapterNumber']) ? prog.data!['percent'] : 0.0;
|
||||
return Card(
|
||||
color: Colors.grey[900], margin: const EdgeInsets.only(bottom: 12),
|
||||
child: ListTile(
|
||||
onTap: onTap,
|
||||
leading: ClipRRect(borderRadius: BorderRadius.circular(4), child: CachedNetworkImage(imageUrl: cd['chapterCoverUrl'] ?? (manga.data() as Map)['coverUrl'], width: 50, height: 50, fit: BoxFit.cover)),
|
||||
title: Text("Ch ${cd['chapterNumber']}"),
|
||||
subtitle: LinearProgressIndicator(value: p, color: kOnsolGold, backgroundColor: Colors.white10, minHeight: 2),
|
||||
trailing: const Icon(Icons.bolt, color: Colors.orangeAccent, size: 18),
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:onsolgo/core/constants.dart';
|
||||
import 'package:onsolgo/screens/library/series_detail.dart';
|
||||
|
||||
class TrendingList extends StatelessWidget {
|
||||
const TrendingList({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("🔥 WHAT'S HOT", style: TextStyle(letterSpacing: 2, fontWeight: FontWeight.bold)),
|
||||
backgroundColor: Colors.black,
|
||||
centerTitle: true,
|
||||
),
|
||||
body: StreamBuilder<QuerySnapshot>(
|
||||
stream: FirebaseFirestore.instance.collection('manga').orderBy('reads', descending: true).snapshots(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) return const Center(child: CircularProgressIndicator());
|
||||
final docs = snapshot.data!.docs;
|
||||
return ListView.builder(
|
||||
itemCount: docs.length,
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemBuilder: (context, index) {
|
||||
final m = docs[index];
|
||||
Color medalColor = (index == 0) ? kOnsolGold : (index == 1) ? const Color(0xFFC0C0C0) : (index == 2) ? const Color(0xFFCD7F32) : Colors.transparent;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: medalColor != Colors.transparent ? Border.all(color: medalColor, width: 2) : null
|
||||
),
|
||||
child: Card(
|
||||
color: Colors.grey[900],
|
||||
margin: EdgeInsets.zero,
|
||||
child: ListTile(
|
||||
leading: ClipRRect(borderRadius: BorderRadius.circular(4), child: CachedNetworkImage(imageUrl: m['coverUrl'], width: 50, height: 70, fit: BoxFit.cover, alignment: Alignment.topCenter)),
|
||||
title: Text(m['title'] ?? '', style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
subtitle: Text("${m['reads'] ?? 0} Readers"),
|
||||
trailing: Text("#${index + 1}", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: medalColor != Colors.transparent ? medalColor : Colors.white)),
|
||||
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SeriesDetail(manga: m))),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user