first commit
This commit is contained in:
17
lib/core/config/admin_whitelist.dart
Normal file
17
lib/core/config/admin_whitelist.dart
Normal 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());
|
||||
}
|
||||
}
|
||||
22
lib/core/config/supabase_config.dart
Normal file
22
lib/core/config/supabase_config.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
14
lib/core/models/result.dart
Normal file
14
lib/core/models/result.dart
Normal 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]);
|
||||
}
|
||||
107
lib/core/router/app_router.dart
Normal file
107
lib/core/router/app_router.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
15
lib/core/router/app_routes.dart
Normal file
15
lib/core/router/app_routes.dart
Normal 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';
|
||||
}
|
||||
27
lib/core/services/storage_service.dart
Normal file
27
lib/core/services/storage_service.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
6
lib/core/supabase/supabase_providers.dart
Normal file
6
lib/core/supabase/supabase_providers.dart
Normal 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,
|
||||
);
|
||||
96
lib/core/theme/app_animations.dart
Normal file
96
lib/core/theme/app_animations.dart
Normal 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;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
35
lib/core/theme/app_colors.dart
Normal file
35
lib/core/theme/app_colors.dart
Normal 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
|
||||
}
|
||||
12
lib/core/theme/app_motion.dart
Normal file
12
lib/core/theme/app_motion.dart
Normal 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;
|
||||
}
|
||||
131
lib/core/theme/app_theme.dart
Normal file
131
lib/core/theme/app_theme.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
91
lib/core/theme/app_typography.dart
Normal file
91
lib/core/theme/app_typography.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
76
lib/core/widgets/riotz_button.dart
Normal file
76
lib/core/widgets/riotz_button.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
35
lib/core/widgets/riotz_card.dart
Normal file
35
lib/core/widgets/riotz_card.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
68
lib/core/widgets/riotz_scaffold.dart
Normal file
68
lib/core/widgets/riotz_scaffold.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user