first commit

This commit is contained in:
Lucas Saburido
2026-05-13 16:26:45 +01:00
commit cabf2025cd
252 changed files with 13524 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
class AdminWhitelist {
const AdminWhitelist._();
// Whitelist of admin emails.
// In a real production app, this should be handled via database roles (RBAC).
static const Set<String> emails = {
'admin@riotz.com',
'root@riotz.com',
'creator@riotz.com',
};
/// Checks if a user is an admin based on their email.
static bool isAdmin(String? email) {
if (email == null) return false;
return emails.contains(email.toLowerCase());
}
}

View File

@@ -0,0 +1,22 @@
import 'package:supabase_flutter/supabase_flutter.dart';
class SupabaseConfig {
const SupabaseConfig._();
static const _url = String.fromEnvironment('SUPABASE_URL');
static const _anonKey = String.fromEnvironment('SUPABASE_ANON_KEY');
static Future<void> initialize() async {
if (_url.isEmpty || _anonKey.isEmpty) {
throw StateError(
'Missing Supabase env values. Provide SUPABASE_URL and SUPABASE_ANON_KEY '
'using --dart-define.',
);
}
await Supabase.initialize(
url: _url,
anonKey: _anonKey,
);
}
}

View File

@@ -0,0 +1,14 @@
sealed class Result<T> {
const Result();
}
class Success<T> extends Result<T> {
final T data;
const Success(this.data);
}
class Failure<T> extends Result<T> {
final String message;
final dynamic error;
const Failure(this.message, [this.error]);
}

View File

@@ -0,0 +1,107 @@
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../../features/auth/presentation/screens/forgot_password_screen.dart';
import '../../features/auth/presentation/screens/login_screen.dart';
import '../../features/auth/presentation/screens/signup_screen.dart';
import '../../features/admin/presentation/screens/admin_screen.dart';
import '../../features/discover/presentation/pages/discover_page.dart';
import '../../features/feed/presentation/screens/feed_screen.dart';
import '../../features/feed/presentation/screens/upload_post_screen.dart';
import '../../features/music/presentation/pages/music_page.dart';
import '../../features/profile/presentation/pages/profile_page.dart';
import '../../features/splash/presentation/pages/splash_page.dart';
import '../../features/theme_preview/presentation/pages/riotz_theme_preview_page.dart';
import '../supabase/supabase_providers.dart';
import 'app_routes.dart';
final appRouterProvider = Provider<GoRouter>((ref) {
final client = ref.watch(supabaseProvider);
final authStateStream = client.auth.onAuthStateChange;
return GoRouter(
initialLocation: AppRoutes.splash,
refreshListenable: GoRouterRefreshStream(authStateStream),
routes: [
GoRoute(
path: AppRoutes.splash,
builder: (context, state) => const SplashPage(),
),
GoRoute(
path: AppRoutes.login,
builder: (context, state) => const LoginScreen(),
),
GoRoute(
path: AppRoutes.signup,
builder: (context, state) => const SignupScreen(),
),
GoRoute(
path: AppRoutes.forgotPassword,
builder: (context, state) => const ForgotPasswordScreen(),
),
GoRoute(
path: AppRoutes.home,
builder: (context, state) => const FeedScreen(),
),
GoRoute(
path: AppRoutes.uploadPost,
builder: (context, state) => const UploadPostScreen(),
),
GoRoute(
path: AppRoutes.profile,
builder: (context, state) => const ProfilePage(),
),
GoRoute(
path: AppRoutes.music,
builder: (context, state) => const MusicPage(),
),
GoRoute(
path: AppRoutes.discover,
builder: (context, state) => const DiscoverPage(),
),
GoRoute(
path: AppRoutes.admin,
builder: (context, state) => const AdminScreen(),
),
GoRoute(
path: AppRoutes.themePreview,
builder: (context, state) => const RiotzThemePreviewPage(),
),
],
redirect: (context, state) {
final isLoggedIn = client.auth.currentUser != null;
final isSplash = state.matchedLocation == AppRoutes.splash;
final isAuthRoute = state.matchedLocation == AppRoutes.login ||
state.matchedLocation == AppRoutes.signup ||
state.matchedLocation == AppRoutes.forgotPassword;
if (!isLoggedIn && !isSplash && !isAuthRoute) {
return AppRoutes.login;
}
if (isLoggedIn && (isSplash || isAuthRoute)) {
return AppRoutes.home;
}
return null;
},
);
});
class GoRouterRefreshStream extends ChangeNotifier {
GoRouterRefreshStream(Stream<AuthState> stream) {
_subscription = stream.asBroadcastStream().listen((_) => notifyListeners());
}
late final StreamSubscription<AuthState> _subscription;
@override
void dispose() {
_subscription.cancel();
super.dispose();
}
}

View File

@@ -0,0 +1,15 @@
class AppRoutes {
const AppRoutes._();
static const splash = '/';
static const login = '/auth/login';
static const signup = '/auth/signup';
static const forgotPassword = '/auth/forgot-password';
static const home = '/home';
static const uploadPost = '/upload-post';
static const profile = '/profile';
static const music = '/music';
static const discover = '/discover';
static const admin = '/admin';
static const themePreview = '/theme-preview';
}

View File

@@ -0,0 +1,27 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final storageServiceProvider = Provider<StorageService>((ref) {
return const StorageService(FlutterSecureStorage());
});
class StorageService {
const StorageService(this._storage);
final FlutterSecureStorage _storage;
Future<void> write(String key, String value) async {
await _storage.write(key: key, value: value);
}
Future<String?> read(String key) async {
return await _storage.read(key: key);
}
Future<void> delete(String key) async {
await _storage.delete(key: key);
}
Future<void> clearAll() async {
await _storage.deleteAll();
}
}

View File

@@ -0,0 +1,6 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
final supabaseProvider = Provider<SupabaseClient>(
(ref) => Supabase.instance.client,
);

View File

@@ -0,0 +1,96 @@
import 'package:flutter/material.dart';
class AppAnimations {
const AppAnimations._();
/// A standard fade transition
static Widget fade({
required Widget child,
Duration duration = const Duration(milliseconds: 300),
}) {
return AnimatedSwitcher(
duration: duration,
child: child,
);
}
/// A slide transition from the bottom (Brutalist style)
static Widget slideIn({
required Widget child,
Offset begin = const Offset(0, 0.1),
Duration duration = const Duration(milliseconds: 400),
}) {
return TweenAnimationBuilder<Offset>(
tween: Tween<Offset>(begin: begin, end: Offset.zero),
duration: duration,
curve: Curves.easeOutQuart,
builder: (context, offset, child) {
return FractionalTranslation(
translation: offset,
child: child,
);
},
child: child,
);
}
/// A simple "Glitch" effect using staggered offsets and opacities
/// This simulates an underground/grunge signal interference.
static Widget glitch({required Widget child}) {
return _GlitchWidget(child: child);
}
}
class _GlitchWidget extends StatefulWidget {
final Widget child;
const _GlitchWidget({required this.child});
@override
State<_GlitchWidget> createState() => _GlitchWidgetState();
}
class _GlitchWidgetState extends State<_GlitchWidget> with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
)..repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
final double glitchFactor = _controller.value;
// Only glitch occasionally
if (glitchFactor > 0.9) {
return Stack(
children: [
Transform.translate(
offset: const Offset(2, 0),
child: Opacity(opacity: 0.5, child: widget.child),
),
Transform.translate(
offset: const Offset(-2, 1),
child: Opacity(opacity: 0.5, child: widget.child),
),
widget.child,
],
);
}
return widget.child;
},
);
}
}

View File

@@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
class AppColors {
const AppColors._();
// Core Palette
static const black = Color(0xFF000000); // Pitch black
static const blackRaised = Color(0xFF1A1A1A); // Slightly raised black
static const blackSoft = Color(0xFF0F0F0F); // Soft black for backgrounds
static const darkGrey = Color(0xFF0A0A0A);
static const white = Color(0xFFFFFFFF);
static const offWhite = Color(0xFFEBEBEB); // For body text readability
// RIOTZ Red Tones (Aggressive & Premium)
static const neonRed = Color(0xFFFF0033); // Primary accent
static const bloodRed = Color(0xFF8B0000); // Secondary
static const deepRed = Color(0xFF4A0000); // Muted backgrounds
// Purple Accents
static const neonPurple = Color(0xFF9D00FF); // Neon purple accent
// Surfaces & Borders
static const surface = Color(0xFF0D0D0D); // Elevated surfaces
static const surfaceLight = Color(0xFF1A1A1A);
static const border = Color(0xFF262626); // Brutalist outlines
// Neutral / Muted
static const grey = Color(0xFF757575);
static const greyDark = Color(0xFF424242);
static const greyMuted = Color(0xFF212121);
// Semantic
static const error = Color(0xFFFF0033);
static const success = Color(0xFF00FF66); // Acid green for contrast
}

View File

@@ -0,0 +1,12 @@
import 'package:flutter/material.dart';
class AppMotion {
const AppMotion._();
static const fast = Duration(milliseconds: 120);
static const normal = Duration(milliseconds: 220);
static const slow = Duration(milliseconds: 360);
static const standardCurve = Curves.easeOutCubic;
static const emphasizedCurve = Curves.easeOutQuart;
}

View File

@@ -0,0 +1,131 @@
import 'package:flutter/material.dart';
import 'app_colors.dart';
import 'app_typography.dart';
import 'app_motion.dart';
class AppTheme {
const AppTheme._();
static ThemeData get dark {
final textTheme = AppTypography.darkTextTheme();
return ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
scaffoldBackgroundColor: AppColors.black,
colorScheme: const ColorScheme.dark(
primary: AppColors.neonRed,
secondary: AppColors.bloodRed,
surface: AppColors.surface,
onPrimary: AppColors.white,
onSecondary: AppColors.white,
onSurface: AppColors.white,
error: AppColors.error,
outline: AppColors.border,
),
textTheme: textTheme,
// App Bar Theme - Centralized & Brutalist
appBarTheme: AppBarTheme(
backgroundColor: AppColors.black,
elevation: 0,
centerTitle: true,
iconTheme: const IconThemeData(color: AppColors.white, size: 20),
titleTextStyle: textTheme.headlineMedium?.copyWith(
color: AppColors.white,
),
),
// Card Theme - Sharp Edges, Subtle Borders
cardTheme: CardThemeData(
color: AppColors.surface,
elevation: 0,
margin: EdgeInsets.zero,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.zero,
side: BorderSide(color: AppColors.border, width: 1),
),
),
// Input Decoration - Industrial / Terminal style
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: AppColors.surface,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 18),
hintStyle: textTheme.bodySmall?.copyWith(color: AppColors.greyDark),
labelStyle: textTheme.bodyMedium?.copyWith(color: AppColors.grey),
border: const OutlineInputBorder(
borderRadius: BorderRadius.zero,
borderSide: BorderSide(color: AppColors.border),
),
enabledBorder: const OutlineInputBorder(
borderRadius: BorderRadius.zero,
borderSide: BorderSide(color: AppColors.border),
),
focusedBorder: const OutlineInputBorder(
borderRadius: BorderRadius.zero,
borderSide: BorderSide(color: AppColors.neonRed, width: 1.5),
),
errorBorder: const OutlineInputBorder(
borderRadius: BorderRadius.zero,
borderSide: BorderSide(color: AppColors.error),
),
),
// Button Themes
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
backgroundColor: AppColors.neonRed,
foregroundColor: AppColors.white,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
textStyle: textTheme.labelLarge,
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.white,
side: const BorderSide(color: AppColors.white, width: 1.5),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
textStyle: textTheme.labelLarge,
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24),
),
),
// Navigation Bar - Custom Riotz Feel
navigationBarTheme: NavigationBarThemeData(
backgroundColor: AppColors.black,
indicatorColor: AppColors.neonRed.withOpacity(0.1),
labelTextStyle: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return textTheme.bodySmall?.copyWith(color: AppColors.neonRed, fontWeight: FontWeight.bold);
}
return textTheme.bodySmall?.copyWith(color: AppColors.grey);
}),
iconTheme: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return const IconThemeData(color: AppColors.neonRed, size: 24);
}
return const IconThemeData(color: AppColors.grey, size: 24);
}),
),
// Dialog Theme
dialogTheme: const DialogThemeData(
backgroundColor: AppColors.surface,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.zero,
side: BorderSide(color: AppColors.border, width: 1),
),
),
dividerTheme: const DividerThemeData(
color: AppColors.border,
thickness: 1,
space: 1,
),
);
}
}

View File

@@ -0,0 +1,91 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'app_colors.dart';
class AppTypography {
const AppTypography._();
// Primary heading font: Aggressive, industrial, and bold
static String get headingFont => GoogleFonts.bebasNeue().fontFamily!;
// Body font: Monospace or clean Sans for that "terminal/underground" feel
static String get bodyFont => GoogleFonts.inter().fontFamily!;
static String get monoFont => GoogleFonts.jetBrainsMono().fontFamily!;
static TextTheme darkTextTheme() {
return TextTheme(
displayLarge: TextStyle(
fontFamily: headingFont,
fontSize: 72,
fontWeight: FontWeight.w900,
color: AppColors.white,
letterSpacing: -1.0,
height: 0.9,
),
displayMedium: TextStyle(
fontFamily: headingFont,
fontSize: 48,
fontWeight: FontWeight.bold,
color: AppColors.white,
letterSpacing: 0.5,
height: 1.0,
),
headlineLarge: TextStyle(
fontFamily: headingFont,
fontSize: 32,
fontWeight: FontWeight.bold,
color: AppColors.neonRed,
letterSpacing: 1.5,
),
headlineMedium: TextStyle(
fontFamily: headingFont,
fontSize: 24,
fontWeight: FontWeight.bold,
color: AppColors.white,
letterSpacing: 1.2,
),
titleLarge: TextStyle(
fontFamily: bodyFont,
fontSize: 20,
fontWeight: FontWeight.w900,
color: AppColors.white,
letterSpacing: -0.5,
),
titleMedium: TextStyle(
fontFamily: bodyFont,
fontSize: 16,
fontWeight: FontWeight.w700,
color: AppColors.white,
),
bodyLarge: TextStyle(
fontFamily: bodyFont,
fontSize: 16,
fontWeight: FontWeight.normal,
color: AppColors.offWhite,
height: 1.5,
),
bodyMedium: TextStyle(
fontFamily: bodyFont,
fontSize: 14,
fontWeight: FontWeight.normal,
color: AppColors.offWhite,
height: 1.4,
),
bodySmall: TextStyle(
fontFamily: monoFont,
fontSize: 12,
fontWeight: FontWeight.w400,
color: AppColors.grey,
letterSpacing: 0.5,
),
labelLarge: TextStyle(
fontFamily: headingFont,
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.white,
letterSpacing: 2.0,
),
);
}
}

View File

@@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
enum RiotzButtonStyle { primary, secondary, outline }
class RiotzButton extends StatelessWidget {
const RiotzButton({
required this.label,
required this.onPressed,
this.style = RiotzButtonStyle.primary,
this.isLoading = false,
this.fullWidth = true,
super.key,
});
final String label;
final VoidCallback? onPressed;
final RiotzButtonStyle style;
final bool isLoading;
final bool fullWidth;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
Widget content = isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2, color: AppColors.white),
)
: Text(label.toUpperCase(), style: theme.textTheme.labelLarge);
if (fullWidth) {
content = Center(child: content);
}
return InkWell(
onTap: isLoading ? null : onPressed,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24),
decoration: BoxDecoration(
color: _getBgColor(),
border: Border.all(
color: _getBorderColor(),
width: 2,
),
),
child: content,
),
);
}
Color _getBgColor() {
switch (style) {
case RiotzButtonStyle.primary:
return AppColors.neonRed;
case RiotzButtonStyle.secondary:
return AppColors.bloodRed;
case RiotzButtonStyle.outline:
return Colors.transparent;
}
}
Color _getBorderColor() {
switch (style) {
case RiotzButtonStyle.primary:
return AppColors.neonRed;
case RiotzButtonStyle.secondary:
return AppColors.bloodRed;
case RiotzButtonStyle.outline:
return AppColors.white;
}
}
}

View File

@@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
class RiotzCard extends StatelessWidget {
const RiotzCard({
required this.child,
this.padding,
this.onTap,
this.isAccent = false,
super.key,
});
final Widget child;
final EdgeInsetsGeometry? padding;
final VoidCallback? onTap;
final bool isAccent;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Container(
padding: padding ?? const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.surface,
border: Border.all(
color: isAccent ? AppColors.neonRed : AppColors.border,
width: isAccent ? 2 : 1,
),
),
child: child,
),
);
}
}

View File

@@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
class RiotzScaffold extends StatelessWidget {
const RiotzScaffold({
required this.body,
this.appBar,
this.bottomNavigationBar,
this.floatingActionButton,
this.useSafeArea = true,
super.key,
});
final Widget body;
final PreferredSizeWidget? appBar;
final Widget? bottomNavigationBar;
final Widget? floatingActionButton;
final bool useSafeArea;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: appBar,
body: Stack(
children: [
// Base Layer: Deep Black
Container(color: AppColors.black),
// Aesthetic Layer: Subtle Brutalist Gradient
Positioned.fill(
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
AppColors.black,
AppColors.deepRed.withOpacity(0.05),
AppColors.black,
],
),
),
),
),
// Texture Layer: Grunge Overlay
// Note: Add 'assets/textures/noise.png' to pubspec and uncomment below
/*
Positioned.fill(
child: Opacity(
opacity: 0.03,
child: Image.asset(
'assets/textures/noise.png',
repeat: ImageRepeat.repeat,
),
),
),
*/
// Content Layer
useSafeArea ? SafeArea(child: body) : body,
],
),
bottomNavigationBar: bottomNavigationBar,
floatingActionButton: floatingActionButton,
);
}
}