Initial Release: SAVEXSTATE Vault V1 - Cyber Orange Edition
This commit is contained in:
@@ -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 ]")),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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")),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 ]")),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 ]")),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
);
|
||||
|
||||
}
|
||||
@@ -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))}";
|
||||
}
|
||||
}
|
||||
@@ -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/"),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 ]")),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user