Initial Release: SAVEXSTATE Vault V1 - Cyber Orange Edition

This commit is contained in:
Zach Groth
2026-03-28 22:53:48 -05:00
commit d2863d2ce8
203 changed files with 8249 additions and 0 deletions
+115
View File
@@ -0,0 +1,115 @@
import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'dart:typed_data';
class AdminArtifactScreen extends StatefulWidget {
const AdminArtifactScreen({super.key});
@override
State<AdminArtifactScreen> createState() => _AdminArtifactScreenState();
}
class _AdminArtifactScreenState extends State<AdminArtifactScreen> {
final _titleController = TextEditingController();
final _descController = TextEditingController();
final _priceController = TextEditingController();
final _stockController = TextEditingController();
final _urlController = TextEditingController(); // FOR EXTERNAL STORE LINK
Uint8List? _fileBytes;
String? _fileName;
bool _isUploading = false;
Future<void> _pickImage() async {
final result = await FilePicker.platform.pickFiles(type: FileType.image, withData: true);
if (result != null) {
setState(() {
_fileBytes = result.files.first.bytes;
_fileName = result.files.first.name;
});
}
}
Future<void> _upload() async {
if (_fileBytes == null || _titleController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("MISSING_DATA")));
return;
}
setState(() => _isUploading = true);
try {
// 1. Upload to Storage
final path = 'artifacts/${DateTime.now().millisecondsSinceEpoch}_$_fileName';
final task = await FirebaseStorage.instance.ref().child(path).putData(_fileBytes!);
final url = await task.ref.getDownloadURL();
// 2. Save Document to Firestore
await FirebaseFirestore.instance.collection('artifacts').add({
'title': _titleController.text.trim(),
'description': _descController.text.trim(),
'price': _priceController.text.trim(),
'stock': _stockController.text.trim(),
'purchaseUrl': _urlController.text.trim(), // LINK TO STORE
'imageUrl': url,
'serial': "SS-${DateTime.now().millisecondsSinceEpoch.toString().substring(8)}",
'createdAt': FieldValue.serverTimestamp(),
});
if (!mounted) return;
Navigator.pop(context);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("SYSTEM_ERROR: $e")));
} finally {
setState(() => _isUploading = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("REGISTER_ARTIFACT")),
body: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
children: [
// Preview selected image
Container(
height: 200,
width: double.infinity,
decoration: BoxDecoration(border: Border.all(color: const Color(0xFF00FF00))),
child: _fileBytes == null
? const Center(child: Text("NO_IMAGE_LOADED"))
: Image.memory(_fileBytes!, fit: BoxFit.cover),
),
const SizedBox(height: 15),
OutlinedButton(onPressed: _pickImage, child: const Text("[ SELECT_ASSET_IMAGE ]")),
const SizedBox(height: 30),
TextField(controller: _titleController, decoration: const InputDecoration(labelText: "ASSET_NAME")),
const SizedBox(height: 15),
TextField(controller: _descController, maxLines: 3, decoration: const InputDecoration(labelText: "TECHNICAL_SPECS")),
const SizedBox(height: 15),
TextField(controller: _urlController, decoration: const InputDecoration(labelText: "PURCHASE_GATEWAY_URL")),
const SizedBox(height: 15),
Row(
children: [
Expanded(child: TextField(controller: _priceController, decoration: const InputDecoration(labelText: "VALUE_\$"))),
const SizedBox(width: 15),
Expanded(child: TextField(controller: _stockController, decoration: const InputDecoration(labelText: "UNITS_AVAIL"))),
],
),
const SizedBox(height: 50),
_isUploading
? const CircularProgressIndicator(color: Color(0xFF00FF00))
: ElevatedButton(onPressed: _upload, child: const Text("[ EXECUTE_REGISTRATION ]")),
],
),
),
);
}
}
+190
View File
@@ -0,0 +1,190 @@
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'dart:math';
import 'admin_post_screen.dart';
import 'admin_music_screen.dart';
import 'admin_users_screen.dart';
import 'admin_artifact_screen.dart';
import 'admin_event_screen.dart';
import 'admin_requests_view.dart'; // NEW IMPORT
class AdminDashboardView extends StatelessWidget {
const AdminDashboardView({super.key});
static const Color terminalGreen = Color(0xFFE87D25);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
title: const Text("SYSTEM_ADMIN_CORE"),
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, size: 16),
onPressed: () => Navigator.pop(context),
),
),
body: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("ROOT_ACCESS: ENABLED",
style: TextStyle(fontSize: 12, color: terminalGreen, letterSpacing: 2)),
const Text("SELECT_MODULE_TO_INITIALIZE:",
style: TextStyle(color: Colors.white24, fontSize: 10)),
const SizedBox(height: 30),
Expanded(
child: GridView.count(
crossAxisCount: 2,
crossAxisSpacing: 15,
mainAxisSpacing: 15,
children: [
_buildAdminTile(context, "BROADCAST_LOG", Icons.radar, Colors.red,
() => Navigator.push(context, MaterialPageRoute(builder: (context) => const AdminPostScreen()))),
_buildAdminTile(context, "ARCHIVE_SYNC", Icons.upload_file, Colors.blueGrey,
() => Navigator.push(context, MaterialPageRoute(builder: (context) => const AdminMusicScreen()))),
_buildAdminTile(context, "ARTIFACT_REG", Icons.inventory_2, Colors.amber,
() => Navigator.push(context, MaterialPageRoute(builder: (context) => const AdminArtifactScreen()))),
_buildAdminTile(context, "SESSION_INIT", Icons.location_on, Colors.cyan,
() => Navigator.push(context, MaterialPageRoute(builder: (context) => const AdminEventScreen()))),
_buildAdminTile(context, "KEY_GENERATOR", Icons.vpn_key, terminalGreen,
() => _showTierSelectionDialog(context)),
_buildAdminTile(context, "USER_DATABASE", Icons.dns, Colors.orange,
() => Navigator.push(context, MaterialPageRoute(builder: (context) => const AdminUsersScreen()))),
// --- NEW: INCOMING SIGNALS (REQUESTS) TILE ---
_buildAdminTile(context, "INCOMING_SIGNALS", Icons.satellite_alt, Colors.purpleAccent,
() => Navigator.push(context, MaterialPageRoute(builder: (context) => const AdminRequestsView()))),
],
),
),
],
),
),
);
}
Widget _buildAdminTile(BuildContext context, String label, IconData icon, Color color, VoidCallback onTap) {
return InkWell(
onTap: onTap,
child: Container(
decoration: BoxDecoration(
border: Border.all(color: color.withValues(alpha: 0.4)),
color: color.withValues(alpha: 0.05),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color, size: 32),
const SizedBox(height: 12),
Text(label,
style: TextStyle(color: color, fontSize: 9, fontWeight: FontWeight.bold, letterSpacing: 1),
textAlign: TextAlign.center
),
],
),
),
);
}
// --- TIERED KEY GENERATION LOGIC ---
void _showTierSelectionDialog(BuildContext context) {
int selectedTier = 1;
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setDialogState) => AlertDialog(
backgroundColor: Colors.black,
shape: const RoundedRectangleBorder(side: BorderSide(color: terminalGreen)),
title: const Text("GENERATE_ACCESS_KEY"),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("GRANT_LEVEL:", style: TextStyle(color: Colors.white24, fontSize: 10)),
DropdownButton<int>(
value: selectedTier,
isExpanded: true,
dropdownColor: Colors.black,
style: const TextStyle(color: terminalGreen, fontFamily: 'ShareTechMono'),
items: const [
DropdownMenuItem(value: 1, child: Text("LVL_01: OBSERVER")),
DropdownMenuItem(value: 2, child: Text("LVL_02: COLLECTOR")),
DropdownMenuItem(value: 3, child: Text("LVL_03: INVESTOR")),
],
onChanged: (val) => setDialogState(() => selectedTier = val!),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("[ CANCEL ]", style: TextStyle(color: Colors.white24))
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
_executeKeyGen(context, selectedTier);
},
child: const Text("[ EXECUTE ]"),
),
],
),
),
);
}
Future<void> _executeKeyGen(BuildContext context, int tier) async {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
String code = String.fromCharCodes(Iterable.generate(6, (_) => chars.codeUnitAt(Random().nextInt(chars.length))));
try {
await FirebaseFirestore.instance.collection('invites').doc(code).set({
'used': false,
'grantTier': tier,
'createdAt': FieldValue.serverTimestamp(),
});
if (!context.mounted) return;
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.black,
shape: const RoundedRectangleBorder(side: BorderSide(color: terminalGreen)),
title: Text("LVL_0${tier}_KEY_READY"),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text("ONE-TIME ACCESS KEY:", style: TextStyle(fontSize: 10, color: Colors.white24)),
const SizedBox(height: 15),
SelectableText(
code,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 32, color: terminalGreen, letterSpacing: 5, fontWeight: FontWeight.bold)
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("[ DONE ]", style: TextStyle(color: terminalGreen))
)
],
),
);
} catch (e) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(backgroundColor: Colors.red, content: Text("DATABASE_ERROR: $e")),
);
}
}
}
+67
View File
@@ -0,0 +1,67 @@
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
class AdminEventScreen extends StatefulWidget {
const AdminEventScreen({super.key});
@override
State<AdminEventScreen> createState() => _AdminEventScreenState();
}
class _AdminEventScreenState extends State<AdminEventScreen> {
final _title = TextEditingController();
final _venue = TextEditingController();
final _city = TextEditingController();
final _date = TextEditingController();
final _link = TextEditingController();
int _minTier = 1;
Future<void> _save() async {
await FirebaseFirestore.instance.collection('events').add({
'title': _title.text,
'venue': _venue.text,
'city': _city.text,
'date_string': _date.text,
'link': _link.text,
'minTier': _minTier,
'date': FieldValue.serverTimestamp(), // For sorting
});
Navigator.pop(context);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("REGISTER_SESSION")),
body: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
children: [
TextField(controller: _title, decoration: const InputDecoration(labelText: "EVENT_NAME")),
const SizedBox(height: 10),
TextField(controller: _venue, decoration: const InputDecoration(labelText: "VENUE_NAME")),
const SizedBox(height: 10),
TextField(controller: _city, decoration: const InputDecoration(labelText: "CITY_SECTOR")),
const SizedBox(height: 10),
TextField(controller: _date, decoration: const InputDecoration(labelText: "DATE (EX: OCT 24)")),
const SizedBox(height: 10),
TextField(controller: _link, decoration: const InputDecoration(labelText: "TICKET_GATEWAY_URL")),
const SizedBox(height: 20),
DropdownButton<int>(
value: _minTier,
isExpanded: true,
dropdownColor: Colors.black,
items: const [
DropdownMenuItem(value: 1, child: Text("LVL_01: PUBLIC")),
DropdownMenuItem(value: 2, child: Text("LVL_02: COLLECTORS")),
DropdownMenuItem(value: 3, child: Text("LVL_03: INVESTORS")),
],
onChanged: (v) => setState(() => _minTier = v!),
),
const SizedBox(height: 40),
ElevatedButton(onPressed: _save, child: const Text("[ INITIALIZE_SESSION ]")),
],
),
),
);
}
}
+136
View File
@@ -0,0 +1,136 @@
import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'dart:typed_data';
class AdminMusicScreen extends StatefulWidget {
const AdminMusicScreen({super.key});
@override
State<AdminMusicScreen> createState() => _AdminMusicScreenState();
}
class _AdminMusicScreenState extends State<AdminMusicScreen> {
final _titleController = TextEditingController();
PlatformFile? _audioFile;
PlatformFile? _imageFile;
bool _isUploading = false;
double _uploadProgress = 0;
static const Color terminalGreen = Color(0xFF00FF00);
Future<void> _pickAudio() async {
final result = await FilePicker.platform.pickFiles(type: FileType.audio, withData: true);
if (result != null) setState(() => _audioFile = result.files.first);
}
Future<void> _pickImage() async {
final result = await FilePicker.platform.pickFiles(type: FileType.image, withData: true);
if (result != null) setState(() => _imageFile = result.files.first);
}
Future<void> _uploadTrack() async {
if (_audioFile == null || _imageFile == null || _titleController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("SYSTEM_ERROR: DATA_REQUIRED")));
return;
}
setState(() { _isUploading = true; _uploadProgress = 0; });
try {
// 1. DYNAMIC CONTENT TYPE DETECTION
String audioContentType = 'audio/mpeg'; // Default MP3
if (_audioFile!.name.toLowerCase().endsWith('.wav')) audioContentType = 'audio/wav';
if (_audioFile!.name.toLowerCase().endsWith('.m4a')) audioContentType = 'audio/mp4';
// 2. UPLOAD AUDIO
final audioPath = 'music/${DateTime.now().millisecondsSinceEpoch}_${_audioFile!.name}';
final audioUploadTask = FirebaseStorage.instance.ref().child(audioPath).putData(
_audioFile!.bytes!,
SettableMetadata(contentType: audioContentType),
);
audioUploadTask.snapshotEvents.listen((event) {
setState(() => _uploadProgress = event.bytesTransferred / event.totalBytes);
});
final audioUrl = await (await audioUploadTask).ref.getDownloadURL();
// 3. UPLOAD IMAGE
final imagePath = 'covers/${DateTime.now().millisecondsSinceEpoch}_${_imageFile!.name}';
final imageUrl = await (await FirebaseStorage.instance.ref().child(imagePath).putData(
_imageFile!.bytes!,
SettableMetadata(contentType: 'image/jpeg'),
)).ref.getDownloadURL();
// 4. SAVE TO FIRESTORE
await FirebaseFirestore.instance.collection('tracks').add({
'title': _titleController.text.trim(),
'audioUrl': audioUrl,
'imageUrl': imageUrl,
'fileName': _audioFile!.name, // Storing this for the dynamic extension display
'uploadedAt': FieldValue.serverTimestamp(),
});
if (!mounted) return;
Navigator.pop(context);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("EXECUTION_ERROR: $e")));
} finally {
setState(() => _isUploading = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(title: const Text("REGISTER_NEW_ASSET"), centerTitle: true),
body: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
children: [
TextField(controller: _titleController, decoration: const InputDecoration(labelText: "ASSET_TITLE")),
const SizedBox(height: 25),
_buildFileSlot("AUDIO_SOURCE", _audioFile?.name, Icons.audiotrack, _pickAudio),
const SizedBox(height: 15),
_buildFileSlot("VISUAL_ASSET", _imageFile?.name, Icons.image, _pickImage),
const SizedBox(height: 40),
if (_isUploading) ...[
LinearProgressIndicator(value: _uploadProgress, color: terminalGreen, backgroundColor: Colors.white10),
const SizedBox(height: 10),
Text("${(_uploadProgress * 100).toStringAsFixed(0)}%_SYNCING", style: const TextStyle(fontSize: 10)),
] else
SizedBox(width: double.infinity, child: ElevatedButton(onPressed: _uploadTrack, child: const Text("[ RELEASE_TRACK ]"))),
],
),
),
);
}
Widget _buildFileSlot(String label, String? fileName, IconData icon, VoidCallback onTap) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: const TextStyle(color: Colors.white24, fontSize: 10)),
const SizedBox(height: 5),
InkWell(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(border: Border.all(color: fileName == null ? Colors.white10 : terminalGreen)),
child: Row(
children: [
Icon(icon, color: fileName == null ? Colors.grey : terminalGreen, size: 20),
const SizedBox(width: 15),
Expanded(child: Text(fileName ?? "SELECT_FILE...", style: TextStyle(color: fileName == null ? Colors.grey : Colors.white, fontSize: 14), overflow: TextOverflow.ellipsis)),
const Text("[ BROWSE ]", style: TextStyle(color: terminalGreen, fontSize: 10)),
],
),
),
),
],
);
}
}
+74
View File
@@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
class AdminPostScreen extends StatefulWidget {
const AdminPostScreen({super.key});
@override
State<AdminPostScreen> createState() => _AdminPostScreenState();
}
class _AdminPostScreenState extends State<AdminPostScreen> {
final _titleController = TextEditingController();
final _contentController = TextEditingController();
int _selectedMinTier = 1; // Default to Level 1 (Everyone)
bool _isUploading = false;
Future<void> _uploadPost() async {
if (_titleController.text.isEmpty || _contentController.text.isEmpty) return;
setState(() => _isUploading = true);
try {
await FirebaseFirestore.instance.collection('posts').add({
'title': _titleController.text.trim(),
'content': _contentController.text.trim(),
'minTier': _selectedMinTier, // STORE THE REQUIRED LEVEL
'timestamp': FieldValue.serverTimestamp(),
});
if (mounted) Navigator.pop(context);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("ERROR: $e")));
} finally {
setState(() => _isUploading = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(title: const Text("INITIALIZE_LOG")),
body: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
children: [
TextField(controller: _titleController, decoration: const InputDecoration(labelText: "LOG_TITLE")),
const SizedBox(height: 15),
TextField(controller: _contentController, maxLines: 5, decoration: const InputDecoration(labelText: "DATA_BODY")),
const SizedBox(height: 25),
// TIER SELECTOR
const Align(alignment: Alignment.centerLeft, child: Text("REQUIRED_ACCESS_LEVEL:", style: TextStyle(color: Colors.white24, fontSize: 10))),
DropdownButton<int>(
value: _selectedMinTier,
isExpanded: true,
dropdownColor: Colors.black,
style: const TextStyle(color: Color(0xFF00FF00), fontFamily: 'ShareTechMono'),
items: const [
DropdownMenuItem(value: 1, child: Text("LVL_01: OBSERVER (PUBLIC)")),
DropdownMenuItem(value: 2, child: Text("LVL_02: COLLECTOR")),
DropdownMenuItem(value: 3, child: Text("LVL_03: INVESTOR")),
],
onChanged: (val) => setState(() => _selectedMinTier = val!),
),
const Spacer(),
_isUploading
? const CircularProgressIndicator(color: Color(0xFF00FF00))
: ElevatedButton(onPressed: _uploadPost, child: const Text("[ BROADCAST_TO_VAULT ]")),
],
),
),
);
}
}
+74
View File
@@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
class AdminRequestsView extends StatelessWidget {
const AdminRequestsView({super.key});
static const Color terminalGreen = Color(0xFF00FF00);
// Function to delete a request after Sage has sent the key
Future<void> _purgeSignal(String docId) async {
await FirebaseFirestore.instance.collection('requests').doc(docId).delete();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
title: const Text("INCOMING_SIGNALS"),
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, size: 16),
onPressed: () => Navigator.pop(context),
),
),
body: StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance
.collection('requests')
.orderBy('timestamp', descending: true)
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return const Center(child: CircularProgressIndicator(color: terminalGreen));
final reqs = snapshot.data!.docs;
if (reqs.isEmpty) return const Center(child: Text("NO_SIGNALS_DETECTED", style: TextStyle(color: Colors.white10)));
return ListView.builder(
padding: const EdgeInsets.all(10),
itemCount: reqs.length,
itemBuilder: (context, index) {
final r = reqs[index].data() as Map<String, dynamic>;
final String docId = reqs[index].id;
// Handle different labels for PayPal vs Free Requests
String sender = r['origin'] == 'FREE_REQUEST'
? (r['name'] ?? "ANON")
: "PAYPAL_INVESTOR";
String contact = r['origin'] == 'FREE_REQUEST'
? (r['contact'] ?? "NO_CONTACT")
: (r['id'] ?? "NO_ID");
return Container(
margin: const EdgeInsets.symmetric(vertical: 5),
decoration: BoxDecoration(
border: Border.all(color: terminalGreen.withValues(alpha: 0.2)),
),
child: ListTile(
title: Text(contact, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14)),
subtitle: Text(
"SENDER: $sender\nTYPE: ${r['origin']}\nTIER_REQ: LVL_0${r['requestedTier'] ?? '?'}",
style: const TextStyle(fontSize: 10, color: Colors.grey),
),
trailing: IconButton(
icon: const Icon(Icons.close, color: Colors.red, size: 20),
onPressed: () => _purgeSignal(docId),
),
),
);
},
);
},
),
);
}
}
+78
View File
@@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
class AdminUsersScreen extends StatelessWidget {
const AdminUsersScreen({super.key});
static const Color terminalGreen = Color(0xFF00FF00);
// Function to upgrade/downgrade user tier
Future<void> _updateTier(String uid, int newTier) async {
await FirebaseFirestore.instance.collection('users').doc(uid).update({
'tier': newTier,
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
title: const Text("USER_DATABASE"),
centerTitle: true,
),
body: StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance.collection('users').snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return const Center(child: CircularProgressIndicator(color: terminalGreen));
final users = snapshot.data!.docs;
return ListView.builder(
itemCount: users.length,
padding: const EdgeInsets.all(10),
itemBuilder: (context, index) {
final userData = users[index].data() as Map<String, dynamic>;
final String uid = users[index].id;
final int currentTier = userData['tier'] ?? 1;
return Container(
margin: const EdgeInsets.symmetric(vertical: 5),
decoration: BoxDecoration(
border: Border.all(color: terminalGreen.withValues(alpha: 0.2)),
),
child: ListTile(
title: Text(userData['displayName'] ?? "UNKNOWN_ID", style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text("EMAIL: ${userData['email']}\nTIER: 0$currentTier",
style: const TextStyle(color: Colors.white38, fontSize: 10)),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
_tierBtn(uid, 1, "01"),
const SizedBox(width: 4),
_tierBtn(uid, 2, "02"),
const SizedBox(width: 4),
_tierBtn(uid, 3, "03"),
],
),
),
);
},
);
},
),
);
}
Widget _tierBtn(String uid, int tier, String label) {
return OutlinedButton(
style: OutlinedButton.styleFrom(
minimumSize: const Size(35, 35),
padding: EdgeInsets.zero,
side: const BorderSide(color: terminalGreen),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
),
onPressed: () => _updateTier(uid, tier),
child: Text(label, style: const TextStyle(fontSize: 10, color: terminalGreen)),
);
}
}
+193
View File
@@ -0,0 +1,193 @@
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:url_launcher/url_launcher.dart';
import 'admin_artifact_screen.dart';
class ArtifactsView extends StatelessWidget {
const ArtifactsView({super.key});
static const Color terminalGreen = Color(0xFFE87D25);
// --- ADMIN LOGIC: DELETE ARTIFACT ---
Future<void> _deleteArtifact(BuildContext context, String docId, String title) async {
bool confirm = await showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.black,
shape: const RoundedRectangleBorder(side: BorderSide(color: Colors.red)),
title: const Text("CONFIRM_ERASURE", style: TextStyle(color: Colors.red, fontSize: 14)),
content: Text("ARE YOU SURE YOU WANT TO REMOVE $title FROM THE VAULT?"),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text("[ CANCEL ]")),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text("[ DELETE ]", style: TextStyle(color: Colors.red))
),
],
),
) ?? false;
if (confirm) {
await FirebaseFirestore.instance.collection('artifacts').doc(docId).delete();
}
}
@override
Widget build(BuildContext context) {
final user = FirebaseAuth.instance.currentUser;
return StreamBuilder<DocumentSnapshot>(
stream: FirebaseFirestore.instance.collection('users').doc(user?.uid).snapshots(),
builder: (context, userSnapshot) {
bool isAdmin = false;
if (userSnapshot.hasData && userSnapshot.data!.exists) {
final data = userSnapshot.data!.data() as Map<String, dynamic>;
isAdmin = data['role'] == 'admin';
}
return Column(
children: [
if (isAdmin)
Padding(
padding: const EdgeInsets.all(12.0),
child: OutlinedButton(
style: OutlinedButton.styleFrom(
minimumSize: const Size(double.infinity, 45),
side: const BorderSide(color: terminalGreen),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
),
onPressed: () => Navigator.push(
context,
MaterialPageRoute(builder: (context) => const AdminArtifactScreen())
),
child: const Text("[ REGISTER_NEW_ARTIFACT ]"),
),
),
Expanded(
child: StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance
.collection('artifacts')
.orderBy('createdAt', descending: true)
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return const Center(child: CircularProgressIndicator(color: terminalGreen));
final docs = snapshot.data!.docs;
if (docs.isEmpty) return const Center(child: Text("INVENTORY_NULL / AWAITING_DROP"));
return ListView.builder(
itemCount: docs.length,
padding: const EdgeInsets.only(bottom: 20),
itemBuilder: (context, index) {
final String docId = docs[index].id;
final item = docs[index].data() as Map<String, dynamic>;
return _buildArtifactCard(context, item, docId, isAdmin);
},
);
},
),
),
],
);
},
);
}
Widget _buildArtifactCard(BuildContext context, Map data, String docId, bool isAdmin) {
final int stock = int.tryParse(data['stock'].toString()) ?? 0;
final bool isSoldOut = stock <= 0;
final String title = data['title'].toString().toUpperCase();
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
border: Border.all(color: terminalGreen.withValues(alpha: 0.2)),
color: Colors.black,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// IMAGE SECTION
Stack(
children: [
AspectRatio(
aspectRatio: 1,
child: Image.network(
data['imageUrl'],
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => const Icon(Icons.broken_image, color: terminalGreen),
),
),
// DELETE BUTTON (ONLY FOR ADMIN)
if (isAdmin)
Positioned(
top: 10,
right: 10,
child: IconButton(
style: IconButton.styleFrom(backgroundColor: Colors.black54),
icon: const Icon(Icons.delete_forever, color: Colors.red),
onPressed: () => _deleteArtifact(context, docId, title),
),
),
],
),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
title,
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold, letterSpacing: 1.2, height: 1.3),
softWrap: true,
),
),
const SizedBox(width: 12),
Text("\$${data['price']}", style: const TextStyle(color: terminalGreen, fontSize: 18, fontWeight: FontWeight.bold)),
],
),
const SizedBox(height: 15),
Text("SERIAL: ${data['serial']}", style: const TextStyle(color: Colors.white24, fontSize: 10)),
Text(
isSoldOut ? "STATUS: DEPLETED" : "UNITS_AVAIL: $stock",
style: TextStyle(color: isSoldOut ? Colors.red : terminalGreen, fontSize: 10),
),
const SizedBox(height: 15),
Text(data['description'] ?? "", style: const TextStyle(color: Colors.white70, fontSize: 13, height: 1.4)),
const SizedBox(height: 25),
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: isSoldOut ? Colors.grey[900] : Colors.black,
side: BorderSide(color: isSoldOut ? Colors.white12 : terminalGreen),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
),
onPressed: isSoldOut ? null : () async {
final Uri url = Uri.parse(data['purchaseUrl'] ?? "https://savexstate.com");
if (!await launchUrl(url, mode: LaunchMode.externalApplication)) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("ERROR: LINK_FAILURE")));
}
},
child: Text(
isSoldOut ? "[ ASSET_DEPLETED ]" : "[ ACQUIRE_ASSET ]",
style: TextStyle(color: isSoldOut ? Colors.grey : terminalGreen),
),
),
),
],
),
),
],
),
);
}
}
+23
View File
@@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'login_screen.dart'; // Import your login screen
import 'home_screen.dart'; // Import your home screen
class AuthWrapper extends StatelessWidget {
const AuthWrapper({super.key});
@override
Widget build(BuildContext context) {
return StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
// If snapshot has user data, they are logged in
if (snapshot.hasData) {
return const HomeScreen();
}
// Otherwise, show login
return const LoginScreen();
},
);
}
}
+261
View File
@@ -0,0 +1,261 @@
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
class CommunityView extends StatelessWidget {
const CommunityView({super.key});
@override
Widget build(BuildContext context) {
final user = FirebaseAuth.instance.currentUser;
// We check the user's role at the top level of the Community tab
return StreamBuilder<DocumentSnapshot>(
stream: FirebaseFirestore.instance.collection('users').doc(user?.uid).snapshots(),
builder: (context, userSnapshot) {
bool isAdmin = false;
if (userSnapshot.hasData && userSnapshot.data!.exists) {
final userData = userSnapshot.data!.data() as Map<String, dynamic>;
isAdmin = userData['role'] == 'admin';
}
return DefaultTabController(
length: 2,
child: Column(
children: [
const TabBar(
indicatorColor: Colors.blueAccent,
labelColor: Colors.blueAccent,
unselectedLabelColor: Colors.grey,
tabs: [
Tab(text: "Fan Lounge"),
Tab(text: "Q&A"),
],
),
Expanded(
child: TabBarView(
children: [
_ChatRoom(),
_QnAFlow(isAdmin: isAdmin), // Pass admin status here
],
),
),
],
),
);
},
);
}
}
// --- PART 1: THE CHATROOM ---
class _ChatRoom extends StatefulWidget {
@override
State<_ChatRoom> createState() => _ChatRoomState();
}
class _ChatRoomState extends State<_ChatRoom> {
final _msgController = TextEditingController();
void _sendMessage() async {
if (_msgController.text.trim().isEmpty) return;
final user = FirebaseAuth.instance.currentUser;
// Before sending, we try to get the most recent name from Firestore
final userDoc = await FirebaseFirestore.instance.collection('users').doc(user?.uid).get();
final String name = userDoc.exists ? (userDoc.data()?['displayName'] ?? "Fan") : "Anonymous Fan";
await FirebaseFirestore.instance.collection('chat').add({
'text': _msgController.text.trim(),
'senderId': user?.uid,
'senderName': name,
'timestamp': FieldValue.serverTimestamp(),
});
_msgController.clear();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance.collection('chat').orderBy('timestamp', descending: true).snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return const Center(child: CircularProgressIndicator());
final msgs = snapshot.data!.docs;
return ListView.builder(
reverse: true, // Newest messages at bottom
padding: const EdgeInsets.all(10),
itemCount: msgs.length,
itemBuilder: (context, index) {
final data = msgs[index].data() as Map<String, dynamic>;
final isMe = data['senderId'] == FirebaseAuth.instance.currentUser?.uid;
return Align(
alignment: isMe ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.symmetric(vertical: 4),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: isMe ? Colors.blueAccent : Colors.grey[850],
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: isMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [
if (!isMe) Text(data['senderName'] ?? "Fan", style: const TextStyle(color: Colors.blueAccent, fontSize: 10, fontWeight: FontWeight.bold)),
Text(data['text'] ?? "", style: const TextStyle(color: Colors.white, fontSize: 15)),
],
),
),
);
},
);
},
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
color: Colors.black,
child: Row(
children: [
Expanded(
child: TextField(
controller: _msgController,
decoration: const InputDecoration(hintText: "Say something...", border: InputBorder.none)
)
),
IconButton(onPressed: _sendMessage, icon: const Icon(Icons.send, color: Colors.blueAccent)),
],
),
),
],
);
}
}
// --- PART 2: THE Q&A ---
class _QnAFlow extends StatelessWidget {
final bool isAdmin;
_QnAFlow({required this.isAdmin});
final _questionController = TextEditingController();
void _submitQuestion() async {
if (_questionController.text.isEmpty) return;
final user = FirebaseAuth.instance.currentUser;
final userDoc = await FirebaseFirestore.instance.collection('users').doc(user?.uid).get();
final String name = userDoc.exists ? (userDoc.data()?['displayName'] ?? "Fan") : "Fan";
await FirebaseFirestore.instance.collection('qna').add({
'question': _questionController.text.trim(),
'answer': null,
'senderName': name,
'timestamp': FieldValue.serverTimestamp(),
});
_questionController.clear();
}
// ONLY Admin sees this dialog
void _showAnswerDialog(BuildContext context, String qId, String questionText) {
final ansController = TextEditingController();
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.grey[900],
title: Text("Answer Question", style: TextStyle(color: Colors.blueAccent[100], fontSize: 16)),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('"$questionText"', style: const TextStyle(fontStyle: FontStyle.italic, color: Colors.white70)),
const SizedBox(height: 15),
TextField(
controller: ansController,
maxLines: 3,
decoration: const InputDecoration(hintText: "Type your answer...", border: OutlineInputBorder()),
),
],
),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text("Cancel")),
ElevatedButton(
onPressed: () async {
await FirebaseFirestore.instance.collection('qna').doc(qId).update({
'answer': ansController.text.trim(),
});
Navigator.pop(context);
},
child: const Text("Post Answer"),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: _questionController,
decoration: InputDecoration(
hintText: "Ask Sage a question...",
filled: true,
fillColor: Colors.grey[900],
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none),
suffixIcon: IconButton(onPressed: _submitQuestion, icon: const Icon(Icons.help_outline, color: Colors.blueAccent)),
),
),
),
Expanded(
child: StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance.collection('qna').orderBy('timestamp', descending: true).snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return const Center(child: CircularProgressIndicator());
final questions = snapshot.data!.docs;
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 10),
itemCount: questions.length,
itemBuilder: (context, index) {
final data = questions[index].data() as Map<String, dynamic>;
final String qId = questions[index].id;
final hasAnswer = data['answer'] != null;
return Card(
color: hasAnswer ? Colors.blueGrey[900] : Colors.grey[850],
margin: const EdgeInsets.symmetric(vertical: 6),
child: ListTile(
title: Text(data['question'] ?? "", style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
hasAnswer ? "SAGE: ${data['answer']}" : "Waiting for answer...",
style: TextStyle(
color: hasAnswer ? Colors.greenAccent : Colors.grey,
fontWeight: hasAnswer ? FontWeight.bold : FontWeight.normal
),
),
),
trailing: isAdmin ? const Icon(Icons.edit, color: Colors.blueAccent, size: 18) : null,
onTap: () {
if (isAdmin) {
_showAnswerDialog(context, qId, data['question']);
}
},
),
);
},
);
},
),
),
],
);
}
}
+112
View File
@@ -0,0 +1,112 @@
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:url_launcher/url_launcher.dart';
class EventsView extends StatefulWidget {
const EventsView({super.key});
@override
State<EventsView> createState() => _EventsViewState();
}
class _EventsViewState extends State<EventsView> {
static const Color terminalGreen = Color(0xFFE87D25);
int _userTier = 1;
@override
void initState() {
super.initState();
_fetchTier();
}
Future<void> _fetchTier() async {
final user = FirebaseAuth.instance.currentUser;
if (user != null) {
final doc = await FirebaseFirestore.instance.collection('users').doc(user.uid).get();
if (mounted) setState(() => _userTier = doc.data()?['tier'] ?? 1);
}
}
@override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance.collection('events').orderBy('date', descending: false).snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return const Center(child: CircularProgressIndicator(color: terminalGreen));
final events = snapshot.data!.docs;
if (events.isEmpty) return const Center(child: Text("NO_SCHEDULED_SESSIONS", style: TextStyle(color: Colors.white24)));
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: events.length,
itemBuilder: (context, index) {
final e = events[index].data() as Map<String, dynamic>;
final int minTier = e['minTier'] ?? 1;
// LOCK LOGIC
if (_userTier < minTier) return _buildLockedEvent(minTier);
return Container(
margin: const EdgeInsets.only(bottom: 15),
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
border: Border.all(color: terminalGreen.withValues(alpha: 0.3)),
color: Colors.black,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("DATE: ${e['date_string'] ?? 'TBA'}", style: const TextStyle(color: terminalGreen, fontSize: 10)),
const SizedBox(height: 5),
Text(e['title'].toString().toUpperCase(), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, letterSpacing: 2)),
Text("LOCATION: ${e['venue']} | ${e['city']}", style: const TextStyle(color: Colors.white70, fontSize: 12)),
const SizedBox(height: 15),
SizedBox(
width: double.infinity,
child: OutlinedButton(
style: OutlinedButton.styleFrom(
side: const BorderSide(color: terminalGreen),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
),
onPressed: () async {
final Uri url = Uri.parse(e['link'] ?? "");
if (!await launchUrl(url)) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("LINK_ERROR")));
}
},
child: const Text("[ ACCESS_TICKETS ]"),
),
),
],
),
);
},
);
},
);
}
Widget _buildLockedEvent(int tier) {
return Container(
margin: const EdgeInsets.only(bottom: 15),
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
border: Border.all(color: Colors.white10),
color: Colors.white.withValues(alpha: 0.02),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("SESSION_ID: [ENCRYPTED]", style: TextStyle(color: Colors.white24, fontSize: 10)),
const SizedBox(height: 5),
const Text("REDACTED_EVENT_DATA", style: TextStyle(color: Colors.white12, fontSize: 16, letterSpacing: 1)),
const SizedBox(height: 10),
// FIXED VARIABLE INTERPOLATION HERE:
Text("[ LVL_0${tier}_ACCESS_REQUIRED ]",
style: const TextStyle(color: Colors.redAccent, fontSize: 10, fontWeight: FontWeight.bold)),
],
),
);
}
}
+136
View File
@@ -0,0 +1,136 @@
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:intl/intl.dart';
class FeedView extends StatefulWidget {
const FeedView({super.key});
@override
State<FeedView> createState() => _FeedViewState();
}
class _FeedViewState extends State<FeedView> {
static const Color terminalGreen = Color(0xFFE87D25);
int _userTier = 1;
@override
void initState() {
super.initState();
_fetchUserTier();
}
Future<void> _fetchUserTier() async {
final user = FirebaseAuth.instance.currentUser;
if (user != null) {
final doc = await FirebaseFirestore.instance.collection('users').doc(user.uid).get();
if (mounted) setState(() => _userTier = doc.data()?['tier'] ?? 1);
}
}
@override
Widget build(BuildContext context) {
return Column(
children: [
SizedBox(
height: 220,
child: PageView(
children: [
_buildPromoCard('assets/images/promo1.jpg', "SIGNAL: HIGH", "SECTOR: CHI_064"),
_buildPromoCard('assets/images/promo2.jpg', "STATUS: ENCRYPTED", "TIER_ACCESS: ENABLED"),
],
),
),
Expanded(
child: StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance.collection('posts').orderBy('timestamp', descending: true).snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return const Center(child: CircularProgressIndicator(color: terminalGreen));
final docs = snapshot.data!.docs;
return ListView.builder(
padding: const EdgeInsets.only(top: 10, bottom: 20),
itemCount: docs.length,
itemBuilder: (context, index) {
final post = docs[index].data() as Map<String, dynamic>;
final int postMinTier = post['minTier'] ?? 1;
// LOCK LOGIC
if (_userTier < postMinTier) {
return _buildRedactedLog(postMinTier);
}
return _buildLogEntry(context, post, docs[index].id, (post['timestamp'] as Timestamp?)?.toDate() ?? DateTime.now());
},
);
},
),
),
],
);
}
// THE LOCKED VIEW
Widget _buildRedactedLog(int requiredTier) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
border: Border.all(color: Colors.red.withValues(alpha: 0.3)),
color: Colors.red.withValues(alpha: 0.05),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("LOG_STATUS: [CLASSIFIED_DATA]", style: TextStyle(color: Colors.red, fontSize: 9)),
const SizedBox(height: 8),
const Text("X X X X X X X X X X X X X X X X", style: TextStyle(color: Colors.white10, letterSpacing: 2)),
const SizedBox(height: 5),
const Text("REDACTED REDACTED REDACTED REDACTED REDACTED", style: TextStyle(color: Colors.white10)),
const SizedBox(height: 10),
Text("[ ACCESS_LEVEL_0${requiredTier}_REQUIRED ]", style: const TextStyle(color: Colors.red, fontSize: 10, fontWeight: FontWeight.bold)),
],
),
);
}
Widget _buildLogEntry(BuildContext context, Map data, String id, DateTime date) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(border: Border.all(color: terminalGreen.withValues(alpha: 0.2)), color: Colors.black),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("LOG_ID: ${id.substring(0, 8)} | ${DateFormat('yy.MM.dd').format(date)}", style: const TextStyle(fontSize: 9, color: Colors.white24)),
const SizedBox(height: 8),
Text(data['title'].toString().toUpperCase(), style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 1.5)),
const SizedBox(height: 8),
Text(data['content'], style: const TextStyle(color: Colors.white70, fontSize: 13, height: 1.4)),
],
),
);
}
Widget _buildPromoCard(String path, String t1, String t2) {
return Container(
margin: const EdgeInsets.all(10),
decoration: BoxDecoration(
border: Border.all(color: terminalGreen.withValues(alpha: 0.4)),
image: DecorationImage(image: AssetImage(path), fit: BoxFit.cover, colorFilter: ColorFilter.mode(Colors.green.withValues(alpha: 0.15), BlendMode.srcATop)),
),
child: Container(
color: Colors.black.withValues(alpha: 0.3),
padding: const EdgeInsets.all(12),
alignment: Alignment.bottomLeft,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(t1, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14, letterSpacing: 1)),
Text(t2, style: TextStyle(color: terminalGreen.withValues(alpha: 0.7), fontSize: 10)),
],
),
),
);
}
}
+89
View File
@@ -0,0 +1,89 @@
// File generated by FlutterFire CLI.
// ignore_for_file: type=lint
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
return web;
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
return ios;
case TargetPlatform.macOS:
return macos;
case TargetPlatform.windows:
return windows;
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions web = FirebaseOptions(
apiKey: 'AIzaSyAhNNOuOtA_x6PprjiF6ch4lmUMMO6crKk',
appId: '1:401871647549:web:9a403344032a2520bd0b54',
messagingSenderId: '401871647549',
projectId: 'sage-the-64th-wonder',
authDomain: 'sage-the-64th-wonder.firebaseapp.com',
storageBucket: 'sage-the-64th-wonder.firebasestorage.app',
measurementId: 'G-EBH1H7K6DM',
);
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyB0NUOvlrxw4FLs04AxP5tEExBzvBw7Yc0',
appId: '1:401871647549:android:bc0a553f0358981ebd0b54',
messagingSenderId: '401871647549',
projectId: 'sage-the-64th-wonder',
storageBucket: 'sage-the-64th-wonder.firebasestorage.app',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyCcmGY2iognTHrf61mge9sPMdXj0hcS5DU',
appId: '1:401871647549:ios:158bd8714840c262bd0b54',
messagingSenderId: '401871647549',
projectId: 'sage-the-64th-wonder',
storageBucket: 'sage-the-64th-wonder.firebasestorage.app',
iosBundleId: 'com.stnebulathe64thwonder.the64thWonder',
);
static const FirebaseOptions macos = FirebaseOptions(
apiKey: 'AIzaSyCcmGY2iognTHrf61mge9sPMdXj0hcS5DU',
appId: '1:401871647549:ios:158bd8714840c262bd0b54',
messagingSenderId: '401871647549',
projectId: 'sage-the-64th-wonder',
storageBucket: 'sage-the-64th-wonder.firebasestorage.app',
iosBundleId: 'com.stnebulathe64thwonder.the64thWonder',
);
static const FirebaseOptions windows = FirebaseOptions(
apiKey: 'AIzaSyAhNNOuOtA_x6PprjiF6ch4lmUMMO6crKk',
appId: '1:401871647549:web:2a782aff43bcbd21bd0b54',
messagingSenderId: '401871647549',
projectId: 'sage-the-64th-wonder',
authDomain: 'sage-the-64th-wonder.firebaseapp.com',
storageBucket: 'sage-the-64th-wonder.firebasestorage.app',
measurementId: 'G-N620ZDBGSS',
);
}
+108
View File
@@ -0,0 +1,108 @@
import 'package:flutter/material.dart';
import 'player_service.dart';
import 'package:intl/intl.dart';
class FullPlayerView extends StatelessWidget {
const FullPlayerView({super.key});
static const Color terminalGreen = Color(0xFFE87D25);
@override
Widget build(BuildContext context) {
final service = PlayerService();
final track = service.currentTrack.value!;
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
title: const Text("AUDIO_DIAGNOSTICS"),
leading: IconButton(icon: const Icon(Icons.keyboard_arrow_down), onPressed: () => Navigator.pop(context)),
),
body: Column(
children: [
const SizedBox(height: 20),
// MASSIVE ALBUM ART WITH SYSTEM BORDER
Container(
margin: const EdgeInsets.all(30),
decoration: BoxDecoration(
border: Border.all(color: terminalGreen, width: 2),
boxShadow: [BoxShadow(color: terminalGreen.withValues(alpha: 0.1), blurRadius: 20)],
),
child: AspectRatio(
aspectRatio: 1,
child: Image.network(track['imageUrl'], fit: BoxFit.cover),
),
),
Text(track['title'].toString().toUpperCase(),
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 4)),
const Text("SOURCE: SAGE_ARCHIVE_V.064", style: TextStyle(color: Colors.white24, fontSize: 10)),
const Spacer(),
// PROGRESS BAR
StreamBuilder<Duration>(
stream: service.player.positionStream,
builder: (context, snapshot) {
final pos = snapshot.data ?? Duration.zero;
final total = service.player.duration ?? Duration.zero;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 40),
child: Column(
children: [
LinearProgressIndicator(
value: total.inMilliseconds > 0 ? pos.inMilliseconds / total.inMilliseconds : 0,
backgroundColor: Colors.white10,
color: terminalGreen,
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(_formatDuration(pos), style: const TextStyle(fontSize: 10, color: terminalGreen)),
Text(_formatDuration(total), style: const TextStyle(fontSize: 10, color: terminalGreen)),
],
)
],
),
);
}
),
// CONTROLS
Padding(
padding: const EdgeInsets.symmetric(vertical: 40),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(icon: const Icon(Icons.replay_10, color: terminalGreen), onPressed: () => service.player.seek(service.player.position - const Duration(seconds: 10))),
const SizedBox(width: 30),
StreamBuilder<bool>(
stream: service.player.playingStream,
builder: (context, snapshot) {
final isPlaying = snapshot.data ?? false;
return GestureDetector(
onTap: () => service.toggle(),
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(shape: BoxShape.circle, border: Border.all(color: terminalGreen)),
child: Icon(isPlaying ? Icons.pause : Icons.play_arrow, color: terminalGreen, size: 40),
),
);
}
),
const SizedBox(width: 30),
IconButton(icon: const Icon(Icons.forward_30, color: terminalGreen), onPressed: () => service.player.seek(service.player.position + const Duration(seconds: 30))),
],
),
),
const SizedBox(height: 20),
],
),
);
}
String _formatDuration(Duration d) {
String twoDigits(int n) => n.toString().padLeft(2, "0");
return "${twoDigits(d.inMinutes)}:${twoDigits(d.inSeconds.remainder(60))}";
}
}
+157
View File
@@ -0,0 +1,157 @@
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:google_fonts/google_fonts.dart';
import 'feed_view.dart';
import 'music_vault_view.dart';
import 'artifacts_view.dart';
import 'events_view.dart';
import 'community_view.dart';
import 'admin_dashboard_view.dart';
import 'player_service.dart';
import 'full_player_view.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
int _selectedIndex = 0;
// COLOR CONSTANTS
static const Color cyberOrange = Color(0xFFE87D25);
static const Color dimmedOrange = Color(0xFF4A280B); // Deep "Burnt" Orange for unselected items
final List<Widget> _pages = [
const FeedView(),
const MusicVaultView(),
const ArtifactsView(),
const EventsView(),
const CommunityView(),
];
@override
Widget build(BuildContext context) {
final user = FirebaseAuth.instance.currentUser;
return StreamBuilder<DocumentSnapshot>(
stream: FirebaseFirestore.instance.collection('users').doc(user?.uid).snapshots(),
builder: (context, snapshot) {
bool isAdmin = false;
if (snapshot.hasData && snapshot.data!.exists) {
final userData = snapshot.data!.data() as Map<String, dynamic>;
isAdmin = userData['role'] == 'admin';
}
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.black,
title: Image.asset(
'assets/images/logo.png',
height: 32,
color: cyberOrange, // UPDATED
filterQuality: FilterQuality.high,
),
centerTitle: true,
leading: const Icon(Icons.lock_outline, color: cyberOrange, size: 16), // UPDATED
actions: [
if (isAdmin)
IconButton(
icon: const Icon(Icons.settings_input_component, color: Colors.redAccent, size: 20),
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => AdminDashboardView())),
),
IconButton(
onPressed: () => FirebaseAuth.instance.signOut(),
icon: const Icon(Icons.power_settings_new, color: cyberOrange, size: 20), // UPDATED
)
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1.0),
child: Container(color: cyberOrange.withValues(alpha: 0.2), height: 1.0), // UPDATED
),
),
body: Column(
children: [
Expanded(child: _pages[_selectedIndex]),
// MINI PLAYER
ValueListenableBuilder<Map<String, dynamic>?>(
valueListenable: PlayerService().currentTrack,
builder: (context, track, child) {
if (track == null) return const SizedBox.shrink();
return GestureDetector(
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => const FullPlayerView())),
child: Container(
height: 65,
decoration: BoxDecoration(
color: const Color(0xFF0A0A0A),
border: Border(top: BorderSide(color: cyberOrange.withValues(alpha: 0.5), width: 0.5)),
),
padding: const EdgeInsets.symmetric(horizontal: 15),
child: Row(
children: [
Container(
width: 45, height: 45,
decoration: BoxDecoration(border: Border.all(color: cyberOrange, width: 1)),
child: Image.network(track['imageUrl'], fit: BoxFit.cover),
),
const SizedBox(width: 15),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(track['title'].toString().toUpperCase(), style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold, letterSpacing: 1, color: cyberOrange)),
const Text("STATUS: STREAMING_ACTIVE", style: TextStyle(fontSize: 8, color: cyberOrange)),
],
),
),
StreamBuilder<bool>(
stream: PlayerService().player.playingStream,
builder: (context, snap) {
final isPlaying = snap.data ?? false;
return IconButton(
icon: Icon(isPlaying ? Icons.pause : Icons.play_arrow, color: cyberOrange),
onPressed: () => PlayerService().toggle(),
);
}
),
],
),
),
);
},
),
],
),
bottomNavigationBar: Container(
decoration: BoxDecoration(
border: Border(top: BorderSide(color: cyberOrange.withValues(alpha: 0.3))),
),
child: BottomNavigationBar(
currentIndex: _selectedIndex,
onTap: (index) => setState(() => _selectedIndex = index),
backgroundColor: Colors.black,
selectedItemColor: cyberOrange, // UPDATED
unselectedItemColor: dimmedOrange, // UPDATED: Burnt Orange
type: BottomNavigationBarType.fixed,
selectedLabelStyle: GoogleFonts.shareTechMono(fontSize: 9),
unselectedLabelStyle: GoogleFonts.shareTechMono(fontSize: 9),
items: const [
BottomNavigationBarItem(icon: Icon(Icons.radar, size: 20), label: "/BROADCAST/"),
BottomNavigationBarItem(icon: Icon(Icons.folder_zip_outlined, size: 20), label: "/ARCHIVE/"),
BottomNavigationBarItem(icon: Icon(Icons.inventory_2_outlined, size: 20), label: "/ARTIFACTS/"),
BottomNavigationBarItem(icon: Icon(Icons.location_on_outlined, size: 20), label: "/SESSIONS/"),
BottomNavigationBarItem(icon: Icon(Icons.terminal, size: 20), label: "/COMMS/"),
],
),
),
);
},
);
}
}
+328
View File
@@ -0,0 +1,328 @@
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:url_launcher/url_launcher.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _nameController = TextEditingController();
final _codeController = TextEditingController();
bool _isLoginMode = true;
bool _isUnlocked = false;
bool _showInfo = false;
bool _isLoading = false;
int _tierToGrant = 1;
static const Color terminalGreen = Color(0xFFE87D25);
// --- LOGIC: REQUEST LVL 1 (FREE) ---
Future<void> _requestFreeAccess() async {
final nameController = TextEditingController();
final contactController = TextEditingController();
bool isSending = false; // Internal state for the dialog
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setDialogState) => AlertDialog(
backgroundColor: Colors.black,
shape: const RoundedRectangleBorder(side: BorderSide(color: terminalGreen)),
title: const Text("FREE_ACCESS_REQUEST", style: TextStyle(fontSize: 14)),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text("ENTER_INFO_FOR_KEY_DELIVERY:", style: TextStyle(fontSize: 10, color: Colors.white24)),
const SizedBox(height: 15),
TextField(controller: nameController, decoration: const InputDecoration(labelText: "ALIAS / NAME")),
const SizedBox(height: 10),
TextField(controller: contactController, decoration: const InputDecoration(labelText: "EMAIL_OR_CONTACT")),
],
),
actions: [
TextButton(
onPressed: isSending ? null : () => Navigator.pop(context),
child: const Text("[ CANCEL ]", style: TextStyle(color: Colors.white24))
),
isSending
? const Padding(
padding: EdgeInsets.only(right: 20),
child: CircularProgressIndicator(color: terminalGreen, strokeWidth: 2),
)
: ElevatedButton(
onPressed: () async {
if (contactController.text.trim().isEmpty) return;
setDialogState(() => isSending = true);
try {
await FirebaseFirestore.instance.collection('requests').add({
'name': nameController.text.trim(),
'contact': contactController.text.trim(),
'timestamp': FieldValue.serverTimestamp(),
'status': 'pending',
'requestedTier': 1,
'origin': 'FREE_REQUEST'
});
if (!mounted) return;
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("SIGNAL_SENT: SAGE_WILL_TRANSMIT_KEY"), backgroundColor: terminalGreen)
);
} catch (e) {
setDialogState(() => isSending = false);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("UPLINK_ERROR: $e"), backgroundColor: Colors.red)
);
}
}
},
child: const Text("[ SEND_REQUEST ]")
)
],
),
),
);
}
// --- LOGIC: VERIFY INVITE KEY ---
Future<void> _verifyInviteCode() async {
if (_codeController.text.isEmpty) return;
setState(() => _isLoading = true);
try {
final codeDoc = await FirebaseFirestore.instance.collection('invites').doc(_codeController.text.trim().toUpperCase()).get();
if (!mounted) return;
if (codeDoc.exists && codeDoc.data()?['used'] == false) {
setState(() {
_isUnlocked = true;
_tierToGrant = codeDoc.data()?['grantTier'] ?? 1;
});
} else {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("INVALID_OR_EXPIRED_KEY"), backgroundColor: Colors.red));
}
} catch (e) {
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("SYNC_ERROR: $e")));
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
// --- LOGIC: LOGIN / REGISTER ---
Future<void> _submit() async {
if (_emailController.text.isEmpty || _passwordController.text.isEmpty) return;
setState(() => _isLoading = true);
try {
if (_isLoginMode) {
await FirebaseAuth.instance.signInWithEmailAndPassword(email: _emailController.text.trim(), password: _passwordController.text.trim());
} else {
UserCredential userCredential = await FirebaseAuth.instance.createUserWithEmailAndPassword(email: _emailController.text.trim(), password: _passwordController.text.trim());
await FirebaseFirestore.instance.collection('invites').doc(_codeController.text.trim().toUpperCase()).update({'used': true, 'usedBy': userCredential.user!.uid});
await FirebaseFirestore.instance.collection('users').doc(userCredential.user!.uid).set({
'email': _emailController.text.trim(),
'displayName': _nameController.text.trim(),
'role': 'fan',
'tier': _tierToGrant,
'createdAt': FieldValue.serverTimestamp(),
});
}
} catch (e) {
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()), backgroundColor: Colors.red));
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
void _showClaimDialog() {
final claimController = TextEditingController();
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.black,
shape: const RoundedRectangleBorder(side: BorderSide(color: terminalGreen)),
title: const Text("CLAIM_ACCESS_KEY", style: TextStyle(fontSize: 14)),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text("ENTER_PAYPAL_EMAIL_OR_TRANS_ID:", style: TextStyle(fontSize: 10, color: Colors.white24)),
const SizedBox(height: 15),
TextField(controller: claimController, style: const TextStyle(color: Colors.white), decoration: const InputDecoration(hintText: "PAYPAL_RECEIPT_ID")),
],
),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text("[ CANCEL ]")),
ElevatedButton(
onPressed: () async {
if (claimController.text.isEmpty) return;
await FirebaseFirestore.instance.collection('requests').add({
'id': claimController.text.trim(),
'timestamp': FieldValue.serverTimestamp(),
'status': 'pending',
'origin': 'PAYPAL_CLAIM'
});
if (!mounted) return;
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("SIGNAL_SENT: SAGE_WILL_VERIFY")));
},
child: const Text("[ SUBMIT ]")
)
],
)
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(40.0),
child: Column(
children: [
Image.asset('assets/images/logo.png', height: 100, color: terminalGreen),
const SizedBox(height: 30),
if (_showInfo) _buildTierInfo()
else if (_isLoginMode) _buildLoginFields()
else if (!_isUnlocked) _buildCodeVerifyFields()
else _buildRegistrationFields(),
const SizedBox(height: 30),
TextButton(
onPressed: () => setState(() => _showInfo = !_showInfo),
child: Text(_showInfo ? "< RETURN_TO_LOGIN" : "> VIEW_ACCESS_MATRIX",
style: const TextStyle(color: Colors.white24, fontSize: 10, letterSpacing: 2)),
),
],
),
),
),
);
}
Widget _buildTierInfo() {
return Column(
children: [
const Text("--- ACCESS_LEVEL_MATRIX ---", style: TextStyle(fontWeight: FontWeight.bold, letterSpacing: 2)),
const SizedBox(height: 25),
_tierRow("01_OBSERVER", "FREE", "30s Previews / Read-Only Comms"),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: OutlinedButton(
style: OutlinedButton.styleFrom(side: const BorderSide(color: terminalGreen), shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero)),
onPressed: _requestFreeAccess,
child: const Text("[ REQUEST_LVL_01 ]", style: TextStyle(fontSize: 10, color: terminalGreen)),
),
),
const SizedBox(height: 20),
_tierRow("02_COLLECTOR", "\$25/MO", "Full Archive / Active Comms"),
_paypalBtn("ACQUIRE_LVL_02", "https://www.paypal.com/ncp/payment/YOUR_L2_LINK"),
const SizedBox(height: 20),
_tierRow("03_INVESTOR", "\$100/YR", "Extractions / Private Logs / Priority"),
_paypalBtn("ACQUIRE_LVL_03", "https://www.paypal.com/ncp/payment/YOUR_L3_LINK"),
const SizedBox(height: 30),
const Text("PAID_BUT_NO_KEY?", style: TextStyle(color: Colors.white24, fontSize: 9)),
TextButton(onPressed: _showClaimDialog, child: const Text("> LOG_PAYMENT_SIGNAL", style: TextStyle(color: terminalGreen, fontSize: 10))),
],
);
}
Widget _tierRow(String name, String price, String perks) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(name, style: const TextStyle(color: terminalGreen, fontWeight: FontWeight.bold, fontSize: 12)),
Text(price, style: const TextStyle(color: Colors.white, fontSize: 12)),
],
),
const SizedBox(height: 4),
Text(perks, style: const TextStyle(color: Colors.white38, fontSize: 9)),
],
);
}
Widget _paypalBtn(String label, String url) {
return SizedBox(
width: double.infinity,
child: OutlinedButton(
style: OutlinedButton.styleFrom(side: const BorderSide(color: terminalGreen), shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero)),
onPressed: () async {
final Uri uri = Uri.parse(url);
if (await canLaunchUrl(uri)) await launchUrl(uri);
},
child: Text("[ $label ]", style: const TextStyle(fontSize: 10, color: terminalGreen)),
),
);
}
Widget _buildLoginFields() {
return Column(
children: [
const Text("[ SYSTEM_LOGIN_REQUIRED ]", style: TextStyle(letterSpacing: 2)),
const SizedBox(height: 25),
TextField(controller: _emailController, decoration: const InputDecoration(labelText: "USER_ID")),
const SizedBox(height: 10),
TextField(controller: _passwordController, obscureText: true, decoration: const InputDecoration(labelText: "ENCRYPT_PASS")),
const SizedBox(height: 30),
_isLoading ? const CircularProgressIndicator(color: terminalGreen) : ElevatedButton(onPressed: _submit, child: const Text("[ ENTER ]")),
TextButton(
onPressed: () => setState(() => _isLoginMode = false),
child: const Text("> INITIALIZE_NEW_KEY", style: TextStyle(color: Colors.white24, fontSize: 10))
),
],
);
}
Widget _buildCodeVerifyFields() {
return Column(
children: [
const Text("[ KEY_VALIDATION ]", style: TextStyle(letterSpacing: 2)),
const SizedBox(height: 25),
TextField(
controller: _codeController,
textAlign: TextAlign.center,
style: const TextStyle(letterSpacing: 8, color: Colors.white, fontWeight: FontWeight.bold),
decoration: const InputDecoration(hintText: "ENTER_KEY", hintStyle: TextStyle(color: Colors.white10))
),
const SizedBox(height: 30),
_isLoading ? const CircularProgressIndicator(color: terminalGreen) : ElevatedButton(onPressed: _verifyInviteCode, child: const Text("[ VALIDATE ]")),
TextButton(onPressed: () => setState(() => _isLoginMode = true), child: const Text("> BACK", style: TextStyle(color: Colors.white24, fontSize: 10))),
],
);
}
Widget _buildRegistrationFields() {
return Column(
children: [
Text("[ LVL_0${_tierToGrant}_ACCESS_GRANTED ]", style: const TextStyle(color: terminalGreen, letterSpacing: 2)),
const SizedBox(height: 25),
TextField(controller: _nameController, decoration: const InputDecoration(labelText: "ALIAS / NAME")),
const SizedBox(height: 10),
TextField(controller: _emailController, decoration: const InputDecoration(labelText: "EMAIL_ADDR")),
const SizedBox(height: 10),
TextField(controller: _passwordController, obscureText: true, decoration: const InputDecoration(labelText: "CREATE_PASS")),
const SizedBox(height: 30),
_isLoading ? const CircularProgressIndicator(color: terminalGreen) : ElevatedButton(onPressed: _submit, child: const Text("[ INITIALIZE_PROFILE ]")),
],
);
}
}
+85
View File
@@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:google_fonts/google_fonts.dart';
import 'firebase_options.dart';
import 'terminal_boot_screen.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
runApp(const SageApp());
}
class SageApp extends StatelessWidget {
const SageApp({super.key});
@override
Widget build(BuildContext context) {
const Color cyberOrange = Color(0xFFE87D25);
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'SAVEXSTATE™',
theme: ThemeData.dark().copyWith(
scaffoldBackgroundColor: const Color(0xFF000000),
// FONT: Retro Tech Mono
textTheme: ThemeData.dark().textTheme.apply(
fontFamily: 'ShareTechMono', // Matches the family name in pubspec
bodyColor: cyberOrange,
displayColor: cyberOrange,
),
primaryColor: cyberOrange,
appBarTheme: AppBarTheme(
backgroundColor: Colors.black,
elevation: 0,
centerTitle: true,
titleTextStyle: GoogleFonts.shareTechMono(
color: cyberOrange,
fontSize: 20,
fontWeight: FontWeight.bold,
),
iconTheme: const IconThemeData(color: cyberOrange),
),
// INPUTS: Sharp edges [TERMINAL_STYLE]
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: Colors.black,
labelStyle: const TextStyle(color: cyberOrange),
hintStyle: TextStyle(color: cyberOrange.withValues(alpha: 0.5)),
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: cyberOrange, width: 1),
borderRadius: BorderRadius.zero,
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: cyberOrange, width: 2),
borderRadius: BorderRadius.zero,
),
),
// BUTTONS: Brackets [ EXECUTE ]
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: cyberOrange,
side: const BorderSide(color: cyberOrange, width: 1),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 25),
textStyle: GoogleFonts.shareTechMono(fontWeight: FontWeight.bold, letterSpacing: 2),
),
),
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
backgroundColor: Colors.black,
selectedItemColor: cyberOrange,
unselectedItemColor: Color(0xFF004400),
type: BottomNavigationBarType.fixed,
),
),
home: const TerminalBootScreen(),
);
}
}
+85
View File
@@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'player_service.dart'; // REQUIRED
class MusicVaultView extends StatefulWidget {
const MusicVaultView({super.key});
@override
State<MusicVaultView> createState() => _MusicVaultViewState();
}
class _MusicVaultViewState extends State<MusicVaultView> {
static const Color terminalGreen = Color(0xFFE87D25);
int _userTier = 1;
@override
void initState() {
super.initState();
_fetchUserTier();
}
Future<void> _fetchUserTier() async {
final user = FirebaseAuth.instance.currentUser;
if (user != null) {
final doc = await FirebaseFirestore.instance.collection('users').doc(user.uid).get();
if (mounted) setState(() => _userTier = doc.data()?['tier'] ?? 1);
}
}
@override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance.collection('tracks').orderBy('uploadedAt', descending: true).snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return const Center(child: CircularProgressIndicator(color: terminalGreen));
final tracks = snapshot.data!.docs;
if (tracks.isEmpty) return const Center(child: Text("ARCHIVE_NULL / AWAITING_DROP"));
return ListView.builder(
itemCount: tracks.length,
padding: const EdgeInsets.only(top: 10, bottom: 20),
itemBuilder: (context, index) {
final t = tracks[index].data() as Map<String, dynamic>;
final String docId = tracks[index].id;
// Check if this specific track is the one playing
return ValueListenableBuilder(
valueListenable: PlayerService().currentTrack,
builder: (context, activeTrack, child) {
final bool isCurrent = activeTrack != null && activeTrack['audioUrl'] == t['audioUrl'];
return Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: isCurrent ? terminalGreen.withValues(alpha: 0.05) : Colors.transparent,
border: Border(left: BorderSide(color: isCurrent ? terminalGreen : Colors.white12, width: 2)),
),
child: ListTile(
leading: Container(
width: 45, height: 45,
decoration: BoxDecoration(border: Border.all(color: isCurrent ? terminalGreen : Colors.white10)),
child: Image.network(
"${t['imageUrl']}${t['imageUrl'].contains('?') ? '&' : '?'}t=${DateTime.now().millisecondsSinceEpoch}",
fit: BoxFit.cover,
errorBuilder: (context, e, s) => const Icon(Icons.audiotrack, color: terminalGreen, size: 20),
),
),
title: Text(
"${t['title'].toString().toUpperCase()}.${(t['fileName'] ?? 'MP3').split('.').last.toUpperCase()}",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: isCurrent ? terminalGreen : Colors.white),
),
subtitle: Text(isCurrent ? "SYSTEM_STATUS: RUNNING" : "SYSTEM_STATUS: ENCRYPTED", style: const TextStyle(fontSize: 9, color: Colors.white24)),
trailing: Icon(isCurrent ? Icons.graphic_eq : Icons.play_arrow_outlined, color: terminalGreen, size: 18),
onTap: () => PlayerService().play(t, _userTier),
),
);
},
);
},
);
},
);
}
}
+49
View File
@@ -0,0 +1,49 @@
import 'package:just_audio/just_audio.dart';
import 'package:flutter/material.dart';
import 'dart:async';
class PlayerService {
static final PlayerService _instance = PlayerService._internal();
factory PlayerService() => _instance;
PlayerService._internal();
final AudioPlayer player = AudioPlayer();
// Track state that other screens can listen to
ValueNotifier<Map<String, dynamic>?> currentTrack = ValueNotifier(null);
// Subscription to handle the 30-second snip-lock
StreamSubscription? _tierSubscription;
void play(Map<String, dynamic> track, int userTier) async {
try {
// 1. Clean up previous track and listeners
await player.stop();
await _tierSubscription?.cancel();
currentTrack.value = track;
// 2. Load and Play
await player.setUrl(track['audioUrl']);
player.play();
// 3. Set up the Snip-Lock for the NEW track
_tierSubscription = player.positionStream.listen((position) {
if (userTier < 2 && position.inSeconds >= 30 && player.playing) {
player.pause();
player.seek(Duration.zero);
}
});
} catch (e) {
print("PLAYER_SYSTEM_ERROR: $e");
}
}
void toggle() {
if (player.playing) {
player.pause();
} else {
player.play();
}
}
}
+58
View File
@@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
import 'dart:async';
import 'auth_wrapper.dart';
class TerminalBootScreen extends StatefulWidget {
const TerminalBootScreen({super.key});
@override
State<TerminalBootScreen> createState() => _TerminalBootScreenState();
}
class _TerminalBootScreenState extends State<TerminalBootScreen> {
final List<String> _lines = [];
final List<String> _allMessages = [
"> INITIALIZING SAVEXSTATE™ OS...",
"> CONNECTING TO SAGE_NET...",
"> LOADING ENCRYPTION_KEYS...",
"> SCANNING_BIOMETRICS...",
"> ACCESS_GRANTED.",
"> WELCOME TO THE VAULT.",
];
@override
void initState() {
super.initState();
_startBootSequence();
}
void _startBootSequence() async {
for (String msg in _allMessages) {
await Future.delayed(const Duration(milliseconds: 500));
if (mounted) setState(() => _lines.add(msg));
}
await Future.delayed(const Duration(seconds: 1));
if (mounted) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => const AuthWrapper()));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Padding(
padding: const EdgeInsets.all(30.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Center(child: Image.asset('assets/images/logo.png', height: 120, color: const Color(0xFFE87D25))),
const SizedBox(height: 50),
..._lines.map((line) => Text(line, style: const TextStyle(color: Color(0xFFE87D25), fontSize: 13))),
],
),
),
);
}
}