**Major Features Added:** - **Inline Reply System**: Replace compose screen with inline reply boxes - **Thread Navigation**: Parent/child navigation with jump functionality - **Chain Flow UI**: Reply counts, expand/collapse animations, visual hierarchy - **Enhanced Animations**: Smooth transitions, hover effects, micro-interactions **Frontend Changes:** - **ThreadedCommentWidget**: Complete rewrite with animations and navigation - **ThreadNode Model**: Added parent references and descendant counting - **ThreadedConversationScreen**: Integrated navigation handlers - **PostDetailScreen**: Replaced with threaded conversation view - **ComposeScreen**: Added reply indicators and context - **PostActions**: Fixed visibility checks for chain buttons **Backend Changes:** - **API Route**: Added /posts/:id/thread endpoint - **Post Repository**: Include allow_chain and visibility fields in feed - **Thread Handler**: Support for fetching post chains **UI/UX Improvements:** - **Reply Context**: Clear indication when replying to specific posts - **Character Counting**: 500 character limit with live counter - **Visual Hierarchy**: Depth-based indentation and styling - **Smooth Animations**: SizeTransition, FadeTransition, hover states - **Chain Navigation**: Parent/child buttons with visual feedback **Technical Enhancements:** - **Animation Controllers**: Proper lifecycle management - **State Management**: Clean separation of concerns - **Navigation Callbacks**: Reusable navigation system - **Error Handling**: Graceful fallbacks and user feedback This creates a Reddit-style threaded conversation experience with smooth animations, inline replies, and intuitive navigation between posts in a chain.
316 lines
11 KiB
Dart
316 lines
11 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
|
|
enum AppThemeType {
|
|
basic,
|
|
pop,
|
|
}
|
|
|
|
class AppTheme {
|
|
// ============================================================================
|
|
// BASIC THEME PALETTE (Current/Original Theme)
|
|
// ============================================================================
|
|
|
|
// ANCHORS (The 60%)
|
|
static const Color basicNavyBlue = Color(0xFF000383);
|
|
static const Color basicNavyText = Color(0xFF000383);
|
|
|
|
// FLOW & STRUCTURE (The 30%)
|
|
static const Color basicEgyptianBlue = Color(0xFF0E38AE);
|
|
static const Color basicBrightNavy = Color(0xFF1974D1);
|
|
|
|
// ACCENTS (The 10%)
|
|
static const Color basicRoyalPurple = Color(0xFF7751A8);
|
|
static const Color basicKsuPurple = Color(0xFF512889);
|
|
|
|
// NEUTRALS & BACKGROUNDS
|
|
static const Color basicQueenPink = Color(0xFFE5C0DD);
|
|
static const Color basicQueenPinkLight = Color(0xFFF9F2F7);
|
|
static const Color basicWhite = Color(0xFFFFFFFF);
|
|
|
|
// ============================================================================
|
|
// POP THEME PALETTE - "Awakening" - High Contrast & Energy
|
|
// ============================================================================
|
|
|
|
// 60% - The Anchors (Deep & Grounded)
|
|
static const Color popNavyBlue = Color(0xFF000383);
|
|
static const Color popNavyText = Color(0xFF0D1050);
|
|
|
|
// 30% - Structure & Flow (Bright & Defined)
|
|
static const Color popEgyptianBlue = Color(0xFF0E38AE);
|
|
static const Color popBrightNavy = Color(0xFF1974D1);
|
|
|
|
// 10% - The Pop (Vibrant Accents)
|
|
static const Color popRoyalPurple = Color(0xFF7751A8);
|
|
static const Color popKsuPurple = Color(0xFF512889);
|
|
|
|
// Backgrounds - The "Clean" Space
|
|
static const Color popScaffoldBg = Color(0xFFF9F6F9);
|
|
static const Color popCardSurface = Colors.white;
|
|
|
|
// Interaction
|
|
static const Color popHighlight = Color(0xFFE5C0DD);
|
|
|
|
// ============================================================================
|
|
// CURRENT THEME COLORS (Dynamic based on selected theme)
|
|
// ============================================================================
|
|
|
|
static AppThemeType _currentThemeType = AppThemeType.basic;
|
|
|
|
static void setThemeType(AppThemeType type) {
|
|
_currentThemeType = type;
|
|
}
|
|
|
|
static bool get isPop => _currentThemeType == AppThemeType.pop;
|
|
|
|
// Dynamic color getters
|
|
static Color get navyBlue => isPop ? popNavyBlue : basicNavyBlue;
|
|
static Color get navyText => isPop ? popNavyText : basicNavyText;
|
|
static Color get egyptianBlue => isPop ? popEgyptianBlue : basicEgyptianBlue;
|
|
static Color get brightNavy => isPop ? popBrightNavy : basicBrightNavy;
|
|
static Color get royalPurple => isPop ? popRoyalPurple : basicRoyalPurple;
|
|
static Color get ksuPurple => isPop ? popKsuPurple : basicKsuPurple;
|
|
static Color get queenPink => isPop ? popHighlight : basicQueenPink;
|
|
static Color get queenPinkLight => scaffoldBg; // Alias for backward compatibility
|
|
static Color get scaffoldBg => isPop ? popScaffoldBg : basicQueenPinkLight;
|
|
static Color get cardSurface => isPop ? popCardSurface : basicWhite;
|
|
static const Color white = basicWhite;
|
|
|
|
// SEMANTIC
|
|
static const Color error = Color(0xFFD32F2F);
|
|
static const Color success = basicKsuPurple;
|
|
static const Color warning = Color(0xFFFBC02D);
|
|
static const Color info = Color(0xFF2196F3);
|
|
|
|
// Trust Tiers
|
|
static Color get tierEstablished => egyptianBlue;
|
|
static Color get tierTrusted => royalPurple;
|
|
static const Color tierNew = Colors.grey;
|
|
|
|
// ============================================================================
|
|
// DIMENSIONS
|
|
// ============================================================================
|
|
|
|
static const double spacingSm = 12.0;
|
|
static const double spacingMd = 16.0;
|
|
static const double spacingLg = 24.0;
|
|
static const double spacingXs = 4.0;
|
|
static const double spacing2xs = 2.0;
|
|
|
|
// Radii
|
|
static const double radiusSm = 4.0;
|
|
static const double radiusXs = 2.0;
|
|
static const double radiusMd = 8.0;
|
|
static const double radiusMdValue = 8.0;
|
|
static const double radiusFull = 36.0;
|
|
|
|
// Text Colors - COLOR HIERARCHY: Content neutral, UI branded
|
|
static Color get textPrimary => navyText; // Names, handles, UI
|
|
static Color get textSecondary => navyText; // Names, handles, UI
|
|
static Color get textTertiary => navyText; // Names, handles, UI
|
|
static const Color textDisabled = Colors.grey;
|
|
static const Color textOnAccent = white;
|
|
static Color get border => egyptianBlue;
|
|
|
|
// Post Content - Neutral for contrast with purple UI
|
|
static const Color postContent = Color(0xFF1A1A1A);
|
|
static const Color postContentLight = Color(0xFF4A4A4A);
|
|
|
|
// STRICT SEPARATION
|
|
static const double borderWidth = 1.5;
|
|
static const double dividerThickness = 2.0;
|
|
static const double flowLineWidth = 3.0;
|
|
|
|
// Post Specific Spacing
|
|
static const double spacingPostShort = 16.0;
|
|
static const double spacingPostMedium = 24.0;
|
|
static const double spacingPostLong = 32.0;
|
|
|
|
// ============================================================================
|
|
// TYPOGRAPHY (Literata + Navy Blue) - STRICT FLAT DESIGN
|
|
// ============================================================================
|
|
|
|
static TextTheme get textTheme => GoogleFonts.literataTextTheme().copyWith(
|
|
// Hero Body Text - Main post content (NEUTRAL for contrast with purple UI)
|
|
bodyLarge: GoogleFonts.literata(
|
|
fontSize: 17,
|
|
height: 1.5,
|
|
fontWeight: FontWeight.w400,
|
|
color: postContent,
|
|
),
|
|
// Standard Body - Secondary content (NEUTRAL)
|
|
bodyMedium: GoogleFonts.literata(
|
|
fontSize: 16,
|
|
height: 1.5,
|
|
fontWeight: FontWeight.w400,
|
|
color: postContentLight,
|
|
),
|
|
// Metadata / UI Elements (Sans-serif)
|
|
labelSmall: GoogleFonts.inter(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w600,
|
|
color: egyptianBlue,
|
|
letterSpacing: 0.2,
|
|
),
|
|
labelMedium: GoogleFonts.inter(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
color: brightNavy,
|
|
letterSpacing: 0,
|
|
),
|
|
// Author Name - Visual anchor, ExtraBold Navy Blue
|
|
labelLarge: GoogleFonts.inter(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w800,
|
|
color: navyBlue,
|
|
),
|
|
// Headlines
|
|
headlineSmall: GoogleFonts.literata(
|
|
fontSize: 22,
|
|
fontWeight: FontWeight.w700,
|
|
color: navyBlue,
|
|
letterSpacing: -0.5,
|
|
),
|
|
headlineMedium: GoogleFonts.literata(
|
|
fontSize: 26,
|
|
fontWeight: FontWeight.w700,
|
|
color: navyBlue,
|
|
letterSpacing: 0,
|
|
),
|
|
);
|
|
|
|
// Backward Compat Getters
|
|
static TextStyle get postBody => textTheme.bodyLarge!;
|
|
static TextStyle get postBodyShort => textTheme.bodyLarge!.copyWith(fontSize: 22);
|
|
static TextStyle get postBodyLong => textTheme.bodyLarge!.copyWith(fontSize: 18);
|
|
static TextStyle get postBodyReflective => textTheme.bodyLarge!.copyWith(
|
|
fontStyle: FontStyle.italic,
|
|
color: ksuPurple
|
|
);
|
|
|
|
// Text Style Getters
|
|
static TextStyle get bodyMedium => textTheme.bodyMedium!;
|
|
static TextStyle get bodyLarge => textTheme.bodyLarge!;
|
|
static TextStyle get headlineMedium => textTheme.headlineMedium!;
|
|
static TextStyle get headlineSmall => textTheme.headlineSmall!;
|
|
static TextStyle get labelMedium => textTheme.labelMedium!;
|
|
static TextStyle get labelSmall => textTheme.labelSmall!;
|
|
|
|
// ============================================================================
|
|
// THEME DATA
|
|
// ============================================================================
|
|
|
|
static ThemeData get lightTheme {
|
|
return ThemeData(
|
|
useMaterial3: true,
|
|
brightness: Brightness.light,
|
|
scaffoldBackgroundColor: scaffoldBg,
|
|
primaryColor: navyBlue,
|
|
|
|
// Color Scheme
|
|
colorScheme: ColorScheme.light(
|
|
primary: navyBlue,
|
|
secondary: brightNavy,
|
|
tertiary: royalPurple,
|
|
surface: cardSurface,
|
|
onSurface: navyText,
|
|
error: error,
|
|
),
|
|
|
|
// Text Theme
|
|
textTheme: textTheme,
|
|
fontFamily: GoogleFonts.literata().fontFamily,
|
|
|
|
// AppBar (High Contrast)
|
|
appBarTheme: AppBarTheme(
|
|
backgroundColor: cardSurface,
|
|
surfaceTintColor: Colors.transparent,
|
|
elevation: 0,
|
|
centerTitle: false,
|
|
iconTheme: IconThemeData(color: navyBlue),
|
|
titleTextStyle: textTheme.headlineSmall,
|
|
systemOverlayStyle: SystemUiOverlayStyle.dark,
|
|
shape: Border(
|
|
bottom: BorderSide(color: egyptianBlue, width: isPop ? 2 : borderWidth),
|
|
),
|
|
),
|
|
|
|
// Card Theme (Defined Edges)
|
|
cardTheme: CardThemeData(
|
|
color: cardSurface,
|
|
elevation: 0,
|
|
margin: EdgeInsets.zero,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
side: BorderSide(color: egyptianBlue, width: 1),
|
|
),
|
|
),
|
|
|
|
// Buttons
|
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: brightNavy,
|
|
foregroundColor: white,
|
|
elevation: 0,
|
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
textStyle: GoogleFonts.inter(fontWeight: FontWeight.bold),
|
|
),
|
|
),
|
|
|
|
textButtonTheme: TextButtonThemeData(
|
|
style: TextButton.styleFrom(
|
|
foregroundColor: egyptianBlue,
|
|
textStyle: GoogleFonts.inter(fontWeight: FontWeight.w600),
|
|
),
|
|
),
|
|
|
|
// Bottom Nav
|
|
bottomNavigationBarTheme: BottomNavigationBarThemeData(
|
|
backgroundColor: cardSurface,
|
|
selectedItemColor: royalPurple,
|
|
unselectedItemColor: const Color(0xFF9EA3B0),
|
|
type: BottomNavigationBarType.fixed,
|
|
showSelectedLabels: !isPop,
|
|
showUnselectedLabels: !isPop,
|
|
elevation: isPop ? 10 : 0,
|
|
),
|
|
|
|
// Floating Action Button
|
|
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
|
backgroundColor: brightNavy,
|
|
foregroundColor: Colors.white,
|
|
elevation: isPop ? 4 : 6,
|
|
shape: isPop ? const CircleBorder() : null,
|
|
),
|
|
|
|
// Divider (Hard & Visible)
|
|
dividerTheme: DividerThemeData(
|
|
color: queenPink,
|
|
thickness: 1,
|
|
space: 24,
|
|
),
|
|
|
|
// Input Fields
|
|
inputDecorationTheme: InputDecorationTheme(
|
|
filled: true,
|
|
fillColor: cardSurface,
|
|
contentPadding: const EdgeInsets.all(16),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
borderSide: BorderSide(color: egyptianBlue, width: 1),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
borderSide: BorderSide(color: egyptianBlue, width: 1),
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
borderSide: BorderSide(color: royalPurple, width: 2),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|