Initial commit
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:onsolgo/core/constants.dart';
|
||||
|
||||
class AchievementsView extends StatelessWidget {
|
||||
const AchievementsView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final String uid = FirebaseAuth.instance.currentUser?.uid ?? "";
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.black,
|
||||
title: const Text("CITIZEN ARCHIVE",
|
||||
style: TextStyle(letterSpacing: 3, fontSize: 12, fontWeight: FontWeight.bold)),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: StreamBuilder<QuerySnapshot>(
|
||||
stream: FirebaseFirestore.instance.collection('users').doc(uid).collection('achievements').snapshots(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) return const Center(child: CircularProgressIndicator(color: kOnsolGold));
|
||||
final unlocked = snapshot.data!.docs.map((d) => d.id).toList();
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
children: [
|
||||
_buildCategory("ONBOARDING", "ACCOUNT", unlocked),
|
||||
_buildCategory("READING PROGRESSION", "READING", unlocked),
|
||||
_buildCategory("COMMUNITY & ENGAGEMENT", "COMMUNITY", unlocked),
|
||||
_buildCategory("THE VAULT", "MARKET", unlocked),
|
||||
_buildCategory("SUPPORT & INVESTMENT", "INVEST", unlocked),
|
||||
_buildCategory("CONSISTENCY", "CONSISTENCY", unlocked),
|
||||
_buildCategory("DISCOVERY", "DISCOVERY", unlocked),
|
||||
_buildCategory("RARE / HIDDEN", "RARE", unlocked),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCategory(String title, String cat, List<String> unlocked) {
|
||||
final items = kAllAchievements.where((a) => a.category == cat).toList();
|
||||
if (items.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||
child: Text(title,
|
||||
style: const TextStyle(color: kOnsolGold, fontWeight: FontWeight.bold, fontSize: 11, letterSpacing: 1.5))
|
||||
),
|
||||
GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: 2.1,
|
||||
crossAxisSpacing: 10,
|
||||
mainAxisSpacing: 10
|
||||
),
|
||||
itemCount: items.length,
|
||||
itemBuilder: (c, i) {
|
||||
final ach = items[i];
|
||||
bool done = unlocked.contains(ach.id);
|
||||
|
||||
// NO CONST HERE: Colors are calculated at runtime
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: done ? kOnsolGold.withValues(alpha: 0.1) : Colors.grey[900],
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: done ? kOnsolGold : Colors.transparent, width: 0.5)
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(ach.icon, color: done ? kOnsolGold : Colors.grey, size: 22),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(ach.title,
|
||||
style: TextStyle(fontSize: 9, fontWeight: FontWeight.bold, color: done ? Colors.white : Colors.grey)),
|
||||
Text(ach.desc,
|
||||
style: const TextStyle(fontSize: 7, color: Colors.white38), maxLines: 2),
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter/material.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:onsolgo/screens/library/series_detail.dart';
|
||||
|
||||
class CollectionView extends StatelessWidget {
|
||||
const CollectionView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final uid = FirebaseAuth.instance.currentUser?.uid;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
appBar: AppBar(title: const Text("YOUR VAULT"), backgroundColor: Colors.black),
|
||||
body: StreamBuilder<QuerySnapshot>(
|
||||
stream: FirebaseFirestore.instance.collection('users').doc(uid).collection('user_collection').snapshots(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) return const Center(child: CircularProgressIndicator());
|
||||
final items = snapshot.data!.docs;
|
||||
if (items.isEmpty) return const Center(child: Text("Favorite a series to see it here.", style: TextStyle(color: Colors.grey)));
|
||||
|
||||
return GridView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2, childAspectRatio: 0.65, crossAxisSpacing: 16, mainAxisSpacing: 16),
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final doc = items[index];
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
DocumentSnapshot fullManga = await FirebaseFirestore.instance.collection('manga').doc(doc.id).get();
|
||||
if (context.mounted) Navigator.push(context, MaterialPageRoute(builder: (c) => SeriesDetail(manga: fullManga)));
|
||||
},
|
||||
child: Column(children: [
|
||||
Expanded(child: ClipRRect(borderRadius: BorderRadius.circular(10), child: CachedNetworkImage(imageUrl: doc['coverUrl'], fit: BoxFit.cover))),
|
||||
const SizedBox(height: 8),
|
||||
Text(doc['title'].toString().toUpperCase(), style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12)),
|
||||
]),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:firebase_storage/firebase_storage.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:onsolgo/core/constants.dart';
|
||||
import 'package:onsolgo/screens/admin/admin_dashboard.dart';
|
||||
import 'package:onsolgo/screens/profile/collection_view.dart';
|
||||
import 'package:onsolgo/screens/profile/achievements_view.dart';
|
||||
import 'package:onsolgo/screens/artist/artist_hub.dart';
|
||||
|
||||
class ProfileView extends StatefulWidget {
|
||||
const ProfileView({super.key});
|
||||
@override
|
||||
State<ProfileView> createState() => _ProfileViewState();
|
||||
}
|
||||
|
||||
class _ProfileViewState extends State<ProfileView> {
|
||||
bool _isUploading = false;
|
||||
Uint8List? _localBytes;
|
||||
final ImagePicker _picker = ImagePicker();
|
||||
|
||||
Future<void> _pickAndUpload(String uid) async {
|
||||
try {
|
||||
final XFile? image = await _picker.pickImage(source: ImageSource.gallery, imageQuality: 40, maxWidth: 500);
|
||||
if (image == null) return;
|
||||
final bytes = await image.readAsBytes();
|
||||
setState(() { _localBytes = bytes; _isUploading = true; });
|
||||
|
||||
final ref = FirebaseStorage.instance.ref().child('avatars').child('$uid.jpg');
|
||||
await ref.putData(bytes, SettableMetadata(contentType: 'image/jpeg'));
|
||||
|
||||
final downloadUrl = await ref.getDownloadURL();
|
||||
final sep = downloadUrl.contains('?') ? '&' : '?';
|
||||
final hashedUrl = '$downloadUrl${sep}t=${DateTime.now().millisecondsSinceEpoch}';
|
||||
await FirebaseFirestore.instance.collection('users').doc(uid).update({'pfpUrl': hashedUrl});
|
||||
} finally { if (mounted) setState(() => _isUploading = false); }
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final String uid = FirebaseAuth.instance.currentUser?.uid ?? "";
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
appBar: AppBar(backgroundColor: Colors.black, elevation: 0, title: const Text("PROFILE")),
|
||||
body: SafeArea(
|
||||
child: StreamBuilder<DocumentSnapshot>(
|
||||
stream: FirebaseFirestore.instance.collection('users').doc(uid).snapshots(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData || !snapshot.data!.exists) return const Center(child: CircularProgressIndicator());
|
||||
|
||||
var data = snapshot.data!.data() as Map<String, dynamic>;
|
||||
|
||||
// --- WEB-SAFE DATA PARSING ---
|
||||
int rank = safeInt(data['rankLevel']);
|
||||
final int energy = safeInt(data['energy'] ?? 2);
|
||||
String role = data['role']?.toString().toLowerCase() ?? "reader";
|
||||
String tier = data['tier']?.toString().toLowerCase() ?? "free";
|
||||
String? pfp = data['pfpUrl'];
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 30),
|
||||
// AVATAR
|
||||
Center(
|
||||
child: Stack(alignment: Alignment.center, children: [
|
||||
Container(width: 120, height: 120, decoration: BoxDecoration(shape: BoxShape.circle, border: Border.all(color: getRankColor(rank), width: 3)),
|
||||
child: ClipOval(
|
||||
child: _localBytes != null
|
||||
? Image.memory(_localBytes!, fit: BoxFit.cover)
|
||||
: (pfp != null && pfp.isNotEmpty
|
||||
? CachedNetworkImage(
|
||||
imageUrl: pfp,
|
||||
key: ValueKey(pfp),
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (context, url) => const Center(child: SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2))),
|
||||
errorWidget: (context, url, error) => const Icon(Icons.person, size: 60),
|
||||
)
|
||||
: const Icon(Icons.person, size: 60)),
|
||||
),
|
||||
),
|
||||
Positioned.fill(child: Material(color: Colors.transparent, child: InkWell(borderRadius: BorderRadius.circular(100), onTap: () => _pickAndUpload(uid)))),
|
||||
if (_isUploading)
|
||||
const Positioned.fill(
|
||||
child: Center(child: SizedBox(width: 40, height: 40, child: CircularProgressIndicator(strokeWidth: 2, color: kOnsolGold))),
|
||||
),
|
||||
]),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(data['username']?.toString().toUpperCase() ?? "CITIZEN", style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
|
||||
|
||||
// --- ENERGY BAR ---
|
||||
const SizedBox(height: 8),
|
||||
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
const Icon(Icons.local_fire_department, color: Colors.orange, size: 18),
|
||||
Text("${data['streak'] ?? 0} DAY STREAK", style: const TextStyle(color: Colors.orange, fontWeight: FontWeight.bold, fontSize: 12)),
|
||||
const SizedBox(width: 15),
|
||||
const Icon(Icons.bolt, color: Colors.orangeAccent, size: 18),
|
||||
Text((tier != 'free' || rank == 5) ? "ENERGY: MAX" : "ENERGY: $energy/2",
|
||||
style: const TextStyle(color: Colors.orangeAccent, fontWeight: FontWeight.bold, fontSize: 12)),
|
||||
]),
|
||||
|
||||
Text(getRankName(rank), style: TextStyle(color: getRankColor(rank), letterSpacing: 2, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 40),
|
||||
|
||||
_MenuTile(icon: Icons.inventory_2_outlined, label: "VIEW VAULT", onTap: () => Navigator.push(context, MaterialPageRoute(builder: (c) => const CollectionView()))),
|
||||
_MenuTile(icon: Icons.emoji_events_outlined, label: "CITIZEN ARCHIVE", onTap: () => Navigator.push(context, MaterialPageRoute(builder: (c) => const AchievementsView()))),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
|
||||
// --- HUB BUTTONS (Stricter checks for Web) ---
|
||||
if (rank == 5 || role == 'artist')
|
||||
_HubBtn(label: "ARTIST HUB", color: Colors.amber[900]!, icon: Icons.palette, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (c) => const ArtistHub()))),
|
||||
|
||||
const SizedBox(height: 10),
|
||||
if (role == 'admin')
|
||||
_HubBtn(label: "COMMAND CENTER", color: Colors.red[900]!, icon: Icons.admin_panel_settings, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (c) => const AdminDashboard()))),
|
||||
|
||||
const SizedBox(height: 30),
|
||||
TextButton(onPressed: () => FirebaseAuth.instance.signOut(), child: const Text("TERMINATE SESSION", style: TextStyle(color: Colors.grey))),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HubBtn extends StatelessWidget {
|
||||
final String label; final Color color; final IconData icon; final VoidCallback onTap;
|
||||
const _HubBtn({required this.label, required this.color, required this.icon, required this.onTap});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(onTap: onTap, tileColor: color.withValues(alpha: 0.15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), leading: Icon(icon, color: color), title: Text(label, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12)));
|
||||
}
|
||||
}
|
||||
|
||||
class _MenuTile extends StatelessWidget {
|
||||
final IconData icon; final String label; final VoidCallback onTap;
|
||||
const _MenuTile({required this.icon, required this.label, required this.onTap});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(leading: Icon(icon, color: Colors.white, size: 22), title: Text(label, style: const TextStyle(fontSize: 13)), trailing: const Icon(Icons.chevron_right, color: Colors.grey, size: 18), onTap: onTap);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user