feat: implement 7-skin theme system with persistent selection
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+16
-1
@@ -119,5 +119,20 @@
|
||||
"common.retry": "Zkusit znovu",
|
||||
"language.title": "Jazyk",
|
||||
"language.en": "Angličtina",
|
||||
"language.cs": "Čeština"
|
||||
"language.cs": "Čeština",
|
||||
"skin.title": "Vzhled aplikace",
|
||||
"skin.defaultSkin": "Výchozí",
|
||||
"skin.defaultSkin_desc": "Teplý pergamen",
|
||||
"skin.accessibility": "Přístupný",
|
||||
"skin.accessibility_desc": "Vysoký kontrast",
|
||||
"skin.senior": "Senior",
|
||||
"skin.senior_desc": "Velký text",
|
||||
"skin.modern": "Moderní",
|
||||
"skin.modern_desc": "Čistý a minimální",
|
||||
"skin.kids": "Dětský",
|
||||
"skin.kids_desc": "Barevný a hravý",
|
||||
"skin.epic": "Epický",
|
||||
"skin.epic_desc": "Tmavý a výrazný",
|
||||
"skin.future": "Budoucnost",
|
||||
"skin.future_desc": "Neonové vibrace"
|
||||
}
|
||||
|
||||
+16
-1
@@ -119,5 +119,20 @@
|
||||
"common.retry": "Retry",
|
||||
"language.title": "Language",
|
||||
"language.en": "English",
|
||||
"language.cs": "Czech"
|
||||
"language.cs": "Czech",
|
||||
"skin.title": "App skin",
|
||||
"skin.defaultSkin": "Default",
|
||||
"skin.defaultSkin_desc": "Warm parchment",
|
||||
"skin.accessibility": "Accessible",
|
||||
"skin.accessibility_desc": "High contrast",
|
||||
"skin.senior": "Senior",
|
||||
"skin.senior_desc": "Large text",
|
||||
"skin.modern": "Modern",
|
||||
"skin.modern_desc": "Clean & minimal",
|
||||
"skin.kids": "Kids",
|
||||
"skin.kids_desc": "Colorful & fun",
|
||||
"skin.epic": "Epic",
|
||||
"skin.epic_desc": "Dark & bold",
|
||||
"skin.future": "Future",
|
||||
"skin.future_desc": "Neon vibes"
|
||||
}
|
||||
|
||||
@@ -1,30 +1,31 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'kittie_colors.dart';
|
||||
import 'kittie_typography.dart';
|
||||
import 'skin_colors.dart';
|
||||
import 'skin_type.dart';
|
||||
import 'skin_typography.dart';
|
||||
import '../constants/app_constants.dart';
|
||||
|
||||
/// Material 3 theme for KittieMobile.
|
||||
abstract final class KittieTheme {
|
||||
static ThemeData get light {
|
||||
final colorScheme = ColorScheme.light(
|
||||
surface: KittieColors.cream,
|
||||
onSurface: KittieColors.brownDark,
|
||||
primary: KittieColors.brown,
|
||||
onPrimary: KittieColors.cream,
|
||||
secondary: KittieColors.amber,
|
||||
onSecondary: KittieColors.brownDark,
|
||||
tertiary: KittieColors.sageGreen,
|
||||
onTertiary: KittieColors.cream,
|
||||
error: KittieColors.red,
|
||||
onError: KittieColors.cream,
|
||||
);
|
||||
/// Legacy light theme — uses the default parchment colors.
|
||||
static ThemeData get light => fromSkin(
|
||||
DefaultSkinColors(),
|
||||
SkinTypography(
|
||||
colors: DefaultSkinColors(),
|
||||
skinType: SkinType.defaultSkin,
|
||||
),
|
||||
);
|
||||
|
||||
/// Builds a full [ThemeData] from [SkinColors] and [SkinTypography].
|
||||
static ThemeData fromSkin(SkinColors colors, SkinTypography typography) {
|
||||
final cs = colors.toColorScheme();
|
||||
final typo = typography.textTheme;
|
||||
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: colorScheme,
|
||||
textTheme: KittieTypography.textTheme,
|
||||
scaffoldBackgroundColor: KittieColors.cream,
|
||||
colorScheme: cs,
|
||||
textTheme: typo,
|
||||
scaffoldBackgroundColor: cs.surface,
|
||||
|
||||
// AppBar
|
||||
appBarTheme: AppBarTheme(
|
||||
@@ -32,35 +33,33 @@ abstract final class KittieTheme {
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 0,
|
||||
centerTitle: false,
|
||||
titleTextStyle: KittieTypography.h2.copyWith(
|
||||
color: KittieColors.brownDark,
|
||||
),
|
||||
iconTheme: const IconThemeData(color: KittieColors.brownDark),
|
||||
titleTextStyle: typo.headlineMedium,
|
||||
iconTheme: IconThemeData(color: cs.onSurface),
|
||||
),
|
||||
|
||||
// Card
|
||||
cardTheme: CardThemeData(
|
||||
color: KittieColors.cream,
|
||||
color: cs.surface,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppConstants.cardBorderRadius),
|
||||
side: const BorderSide(color: KittieColors.creamBorder),
|
||||
side: BorderSide(color: cs.onSurface.withValues(alpha: 0.12)),
|
||||
),
|
||||
),
|
||||
|
||||
// Bottom Navigation Bar
|
||||
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
|
||||
backgroundColor: KittieColors.cream,
|
||||
selectedItemColor: KittieColors.brown,
|
||||
unselectedItemColor: KittieColors.brownLight,
|
||||
bottomNavigationBarTheme: BottomNavigationBarThemeData(
|
||||
backgroundColor: cs.surface,
|
||||
selectedItemColor: cs.primary,
|
||||
unselectedItemColor: cs.onSurface.withValues(alpha: 0.5),
|
||||
elevation: 0,
|
||||
type: BottomNavigationBarType.fixed,
|
||||
),
|
||||
|
||||
// FAB
|
||||
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
||||
backgroundColor: KittieColors.brown,
|
||||
foregroundColor: KittieColors.cream,
|
||||
backgroundColor: cs.primary,
|
||||
foregroundColor: cs.onPrimary,
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppConstants.cardBorderRadius),
|
||||
@@ -68,9 +67,9 @@ abstract final class KittieTheme {
|
||||
),
|
||||
|
||||
// Bottom Sheet
|
||||
bottomSheetTheme: const BottomSheetThemeData(
|
||||
backgroundColor: KittieColors.cream,
|
||||
shape: RoundedRectangleBorder(
|
||||
bottomSheetTheme: BottomSheetThemeData(
|
||||
backgroundColor: cs.surface,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(AppConstants.bottomSheetBorderRadius),
|
||||
),
|
||||
@@ -80,14 +79,14 @@ abstract final class KittieTheme {
|
||||
// Input Decoration
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: KittieColors.creamDark,
|
||||
fillColor: colors.creamDark,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: KittieColors.brown, width: 1.5),
|
||||
borderSide: BorderSide(color: cs.primary, width: 1.5),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
@@ -98,13 +97,13 @@ abstract final class KittieTheme {
|
||||
// Elevated Button
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: KittieColors.brown,
|
||||
foregroundColor: KittieColors.cream,
|
||||
backgroundColor: cs.primary,
|
||||
foregroundColor: cs.onPrimary,
|
||||
minimumSize: const Size(0, 48),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
textStyle: KittieTypography.labelLarge,
|
||||
textStyle: typo.labelLarge,
|
||||
elevation: 0,
|
||||
),
|
||||
),
|
||||
@@ -112,8 +111,8 @@ abstract final class KittieTheme {
|
||||
// Text Button
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: KittieColors.brown,
|
||||
textStyle: KittieTypography.labelLarge,
|
||||
foregroundColor: cs.primary,
|
||||
textStyle: typo.labelLarge,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,511 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Abstract base class for all skin color palettes.
|
||||
///
|
||||
/// Each skin provides concrete color values used throughout the app.
|
||||
/// Dark skins (Epic, Future) MUST override [toColorScheme] with
|
||||
/// [ColorScheme.dark] to prevent white-on-white text bugs.
|
||||
abstract class SkinColors {
|
||||
// ── Surface / cream ────────────────────────────────────────────────────────
|
||||
Color get cream;
|
||||
Color get creamDark;
|
||||
Color get creamBorder;
|
||||
|
||||
// ── Brown / primary ────────────────────────────────────────────────────────
|
||||
Color get brown;
|
||||
Color get brownLight;
|
||||
Color get brownDark;
|
||||
Color get brownMid;
|
||||
|
||||
// ── Sage green / tertiary ──────────────────────────────────────────────────
|
||||
Color get sageGreen;
|
||||
Color get greenLight;
|
||||
Color get greenDark;
|
||||
|
||||
// ── Amber / secondary ──────────────────────────────────────────────────────
|
||||
Color get amber;
|
||||
|
||||
// ── Purple / accent ────────────────────────────────────────────────────────
|
||||
Color get purple;
|
||||
Color get purpleLight;
|
||||
|
||||
// ── Red / error ────────────────────────────────────────────────────────────
|
||||
Color get red;
|
||||
Color get redLight;
|
||||
|
||||
// ── Orange / warning ───────────────────────────────────────────────────────
|
||||
Color get orange;
|
||||
Color get orangeLight;
|
||||
|
||||
// ── Energy level colors ────────────────────────────────────────────────────
|
||||
Color get energyLow;
|
||||
Color get energyMedium;
|
||||
Color get energyHigh;
|
||||
|
||||
/// Builds the [ColorScheme] for this skin.
|
||||
///
|
||||
/// Light skins use [ColorScheme.light]; dark skins (epic, future)
|
||||
/// MUST override with [ColorScheme.dark].
|
||||
ColorScheme toColorScheme() => ColorScheme.light(
|
||||
surface: cream,
|
||||
onSurface: brownDark,
|
||||
primary: brown,
|
||||
onPrimary: cream,
|
||||
secondary: amber,
|
||||
onSecondary: brownDark,
|
||||
tertiary: sageGreen,
|
||||
onTertiary: cream,
|
||||
error: red,
|
||||
onError: cream,
|
||||
);
|
||||
}
|
||||
|
||||
// ── Default (Warm Parchment) ─────────────────────────────────────────────────
|
||||
|
||||
class DefaultSkinColors extends SkinColors {
|
||||
@override
|
||||
Color get cream => const Color(0xFFF5F2EC);
|
||||
@override
|
||||
Color get creamDark => const Color(0xFFEDE8DF);
|
||||
@override
|
||||
Color get creamBorder => const Color(0xFFE0DBD0);
|
||||
|
||||
@override
|
||||
Color get brown => const Color(0xFF6B5A48);
|
||||
@override
|
||||
Color get brownLight => const Color(0xFF8C8070);
|
||||
@override
|
||||
Color get brownDark => const Color(0xFF2C2820);
|
||||
@override
|
||||
Color get brownMid => const Color(0xFF3D3830);
|
||||
|
||||
@override
|
||||
Color get sageGreen => const Color(0xFF7BAE82);
|
||||
@override
|
||||
Color get greenLight => const Color(0xFFEEF4EF);
|
||||
@override
|
||||
Color get greenDark => const Color(0xFF3A6040);
|
||||
|
||||
@override
|
||||
Color get amber => const Color(0xFFC4A882);
|
||||
|
||||
@override
|
||||
Color get purple => const Color(0xFF9070A0);
|
||||
@override
|
||||
Color get purpleLight => const Color(0xFFF0E8F8);
|
||||
|
||||
@override
|
||||
Color get red => const Color(0xFFC45858);
|
||||
@override
|
||||
Color get redLight => const Color(0xFFFAF0F0);
|
||||
|
||||
@override
|
||||
Color get orange => const Color(0xFFC4943A);
|
||||
@override
|
||||
Color get orangeLight => const Color(0xFFFBF3E8);
|
||||
|
||||
@override
|
||||
Color get energyLow => sageGreen;
|
||||
@override
|
||||
Color get energyMedium => orange;
|
||||
@override
|
||||
Color get energyHigh => red;
|
||||
}
|
||||
|
||||
// ── Accessibility (WCAG AAA High-Contrast) ───────────────────────────────────
|
||||
|
||||
class AccessibilitySkinColors extends SkinColors {
|
||||
@override
|
||||
Color get cream => const Color(0xFFFFFFFF);
|
||||
@override
|
||||
Color get creamDark => const Color(0xFFF0F0F0);
|
||||
@override
|
||||
Color get creamBorder => const Color(0xFFCCCCCC);
|
||||
|
||||
@override
|
||||
Color get brown => const Color(0xFF0055CC);
|
||||
@override
|
||||
Color get brownLight => const Color(0xFF444444);
|
||||
@override
|
||||
Color get brownDark => const Color(0xFF1A1A1A);
|
||||
@override
|
||||
Color get brownMid => const Color(0xFF333333);
|
||||
|
||||
@override
|
||||
Color get sageGreen => const Color(0xFF006600);
|
||||
@override
|
||||
Color get greenLight => const Color(0xFFE0F0E0);
|
||||
@override
|
||||
Color get greenDark => const Color(0xFF004400);
|
||||
|
||||
@override
|
||||
Color get amber => const Color(0xFF996600);
|
||||
|
||||
@override
|
||||
Color get purple => const Color(0xFF660099);
|
||||
@override
|
||||
Color get purpleLight => const Color(0xFFEEDDFF);
|
||||
|
||||
@override
|
||||
Color get red => const Color(0xFFCC0000);
|
||||
@override
|
||||
Color get redLight => const Color(0xFFFFEEEE);
|
||||
|
||||
@override
|
||||
Color get orange => const Color(0xFFCC5500);
|
||||
@override
|
||||
Color get orangeLight => const Color(0xFFFFEEDD);
|
||||
|
||||
@override
|
||||
Color get energyLow => const Color(0xFF006600);
|
||||
@override
|
||||
Color get energyMedium => const Color(0xFFCC5500);
|
||||
@override
|
||||
Color get energyHigh => const Color(0xFFCC0000);
|
||||
|
||||
@override
|
||||
ColorScheme toColorScheme() => const ColorScheme.light(
|
||||
surface: Color(0xFFFFFFFF),
|
||||
onSurface: Color(0xFF1A1A1A),
|
||||
primary: Color(0xFF0055CC),
|
||||
onPrimary: Color(0xFFFFFFFF),
|
||||
secondary: Color(0xFF996600),
|
||||
onSecondary: Color(0xFFFFFFFF),
|
||||
tertiary: Color(0xFF006600),
|
||||
onTertiary: Color(0xFFFFFFFF),
|
||||
error: Color(0xFFCC0000),
|
||||
onError: Color(0xFFFFFFFF),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Senior (off-white, navy, gold, 1.25x) ────────────────────────────────────
|
||||
|
||||
class SeniorSkinColors extends SkinColors {
|
||||
@override
|
||||
Color get cream => const Color(0xFFFAFAFA);
|
||||
@override
|
||||
Color get creamDark => const Color(0xFFF0EFE8);
|
||||
@override
|
||||
Color get creamBorder => const Color(0xFFDDDDD5);
|
||||
|
||||
@override
|
||||
Color get brown => const Color(0xFF2C4A7C);
|
||||
@override
|
||||
Color get brownLight => const Color(0xFF6B7A99);
|
||||
@override
|
||||
Color get brownDark => const Color(0xFF1C1C1C);
|
||||
@override
|
||||
Color get brownMid => const Color(0xFF3A3A3A);
|
||||
|
||||
@override
|
||||
Color get sageGreen => const Color(0xFF3A7D44);
|
||||
@override
|
||||
Color get greenLight => const Color(0xFFE8F5E9);
|
||||
@override
|
||||
Color get greenDark => const Color(0xFF1B5E20);
|
||||
|
||||
@override
|
||||
Color get amber => const Color(0xFFB8942C);
|
||||
|
||||
@override
|
||||
Color get purple => const Color(0xFF6A3D9A);
|
||||
@override
|
||||
Color get purpleLight => const Color(0xFFF0E6FF);
|
||||
|
||||
@override
|
||||
Color get red => const Color(0xFFB71C1C);
|
||||
@override
|
||||
Color get redLight => const Color(0xFFFFEBEE);
|
||||
|
||||
@override
|
||||
Color get orange => const Color(0xFFE65100);
|
||||
@override
|
||||
Color get orangeLight => const Color(0xFFFFF3E0);
|
||||
|
||||
@override
|
||||
Color get energyLow => sageGreen;
|
||||
@override
|
||||
Color get energyMedium => amber;
|
||||
@override
|
||||
Color get energyHigh => red;
|
||||
|
||||
@override
|
||||
ColorScheme toColorScheme() => ColorScheme.light(
|
||||
surface: cream,
|
||||
onSurface: brownDark,
|
||||
primary: brown,
|
||||
onPrimary: Colors.white,
|
||||
secondary: amber,
|
||||
onSecondary: brownDark,
|
||||
tertiary: sageGreen,
|
||||
onTertiary: Colors.white,
|
||||
error: red,
|
||||
onError: Colors.white,
|
||||
);
|
||||
}
|
||||
|
||||
// ── Modern (cool gray, teal) ──────────────────────────────────────────────────
|
||||
|
||||
class ModernSkinColors extends SkinColors {
|
||||
@override
|
||||
Color get cream => const Color(0xFFF8F9FA);
|
||||
@override
|
||||
Color get creamDark => const Color(0xFFECEFF1);
|
||||
@override
|
||||
Color get creamBorder => const Color(0xFFCFD8DC);
|
||||
|
||||
@override
|
||||
Color get brown => const Color(0xFF0D9488);
|
||||
@override
|
||||
Color get brownLight => const Color(0xFF64748B);
|
||||
@override
|
||||
Color get brownDark => const Color(0xFF212529);
|
||||
@override
|
||||
Color get brownMid => const Color(0xFF374151);
|
||||
|
||||
@override
|
||||
Color get sageGreen => const Color(0xFF10B981);
|
||||
@override
|
||||
Color get greenLight => const Color(0xFFD1FAE5);
|
||||
@override
|
||||
Color get greenDark => const Color(0xFF065F46);
|
||||
|
||||
@override
|
||||
Color get amber => const Color(0xFFF59E0B);
|
||||
|
||||
@override
|
||||
Color get purple => const Color(0xFF8B5CF6);
|
||||
@override
|
||||
Color get purpleLight => const Color(0xFFEDE9FE);
|
||||
|
||||
@override
|
||||
Color get red => const Color(0xFFEF4444);
|
||||
@override
|
||||
Color get redLight => const Color(0xFFFEE2E2);
|
||||
|
||||
@override
|
||||
Color get orange => const Color(0xFFF97316);
|
||||
@override
|
||||
Color get orangeLight => const Color(0xFFFFEDD5);
|
||||
|
||||
@override
|
||||
Color get energyLow => sageGreen;
|
||||
@override
|
||||
Color get energyMedium => amber;
|
||||
@override
|
||||
Color get energyHigh => red;
|
||||
|
||||
@override
|
||||
ColorScheme toColorScheme() => ColorScheme.light(
|
||||
surface: cream,
|
||||
onSurface: brownDark,
|
||||
primary: brown,
|
||||
onPrimary: Colors.white,
|
||||
secondary: amber,
|
||||
onSecondary: brownDark,
|
||||
tertiary: sageGreen,
|
||||
onTertiary: Colors.white,
|
||||
error: red,
|
||||
onError: Colors.white,
|
||||
);
|
||||
}
|
||||
|
||||
// ── Kids (lavender, pink, sunny yellow, 1.1x) ────────────────────────────────
|
||||
|
||||
class KidsSkinColors extends SkinColors {
|
||||
@override
|
||||
Color get cream => const Color(0xFFF0E6FF);
|
||||
@override
|
||||
Color get creamDark => const Color(0xFFE3D5F5);
|
||||
@override
|
||||
Color get creamBorder => const Color(0xFFD1B8F0);
|
||||
|
||||
@override
|
||||
Color get brown => const Color(0xFFE91E8F);
|
||||
@override
|
||||
Color get brownLight => const Color(0xFF9966CC);
|
||||
@override
|
||||
Color get brownDark => const Color(0xFF3D1F6E);
|
||||
@override
|
||||
Color get brownMid => const Color(0xFF5D3091);
|
||||
|
||||
@override
|
||||
Color get sageGreen => const Color(0xFF4CAF50);
|
||||
@override
|
||||
Color get greenLight => const Color(0xFFE8F5E9);
|
||||
@override
|
||||
Color get greenDark => const Color(0xFF1B5E20);
|
||||
|
||||
@override
|
||||
Color get amber => const Color(0xFFFFD93D);
|
||||
|
||||
@override
|
||||
Color get purple => const Color(0xFF9C27B0);
|
||||
@override
|
||||
Color get purpleLight => const Color(0xFFF3E5F5);
|
||||
|
||||
@override
|
||||
Color get red => const Color(0xFFF44336);
|
||||
@override
|
||||
Color get redLight => const Color(0xFFFFEBEE);
|
||||
|
||||
@override
|
||||
Color get orange => const Color(0xFFFF9800);
|
||||
@override
|
||||
Color get orangeLight => const Color(0xFFFFF3E0);
|
||||
|
||||
@override
|
||||
Color get energyLow => sageGreen;
|
||||
@override
|
||||
Color get energyMedium => amber;
|
||||
@override
|
||||
Color get energyHigh => red;
|
||||
|
||||
@override
|
||||
ColorScheme toColorScheme() => ColorScheme.light(
|
||||
surface: cream,
|
||||
onSurface: brownDark,
|
||||
primary: brown,
|
||||
onPrimary: Colors.white,
|
||||
secondary: amber,
|
||||
onSecondary: brownDark,
|
||||
tertiary: sageGreen,
|
||||
onTertiary: Colors.white,
|
||||
error: red,
|
||||
onError: Colors.white,
|
||||
);
|
||||
}
|
||||
|
||||
// ── Epic (dark purple, gold) ──────────────────────────────────────────────────
|
||||
|
||||
class EpicSkinColors extends SkinColors {
|
||||
@override
|
||||
Color get cream => const Color(0xFF1A1425);
|
||||
@override
|
||||
Color get creamDark => const Color(0xFF241C35);
|
||||
@override
|
||||
Color get creamBorder => const Color(0xFF3D3050);
|
||||
|
||||
@override
|
||||
Color get brown => const Color(0xFF7C3AED);
|
||||
@override
|
||||
Color get brownLight => const Color(0xFFAA88DD);
|
||||
@override
|
||||
Color get brownDark => const Color(0xFFE8E0F0);
|
||||
@override
|
||||
Color get brownMid => const Color(0xFFCCBBEE);
|
||||
|
||||
@override
|
||||
Color get sageGreen => const Color(0xFF10B981);
|
||||
@override
|
||||
Color get greenLight => const Color(0xFF1A2E28);
|
||||
@override
|
||||
Color get greenDark => const Color(0xFF6EE7B7);
|
||||
|
||||
@override
|
||||
Color get amber => const Color(0xFFF59E0B);
|
||||
|
||||
@override
|
||||
Color get purple => const Color(0xFFA855F7);
|
||||
@override
|
||||
Color get purpleLight => const Color(0xFF2D1B4E);
|
||||
|
||||
@override
|
||||
Color get red => const Color(0xFFF87171);
|
||||
@override
|
||||
Color get redLight => const Color(0xFF2D1414);
|
||||
|
||||
@override
|
||||
Color get orange => const Color(0xFFFB923C);
|
||||
@override
|
||||
Color get orangeLight => const Color(0xFF2D1F0A);
|
||||
|
||||
@override
|
||||
Color get energyLow => sageGreen;
|
||||
@override
|
||||
Color get energyMedium => amber;
|
||||
@override
|
||||
Color get energyHigh => red;
|
||||
|
||||
/// Epic is a dark skin — MUST use [ColorScheme.dark].
|
||||
@override
|
||||
ColorScheme toColorScheme() => const ColorScheme.dark(
|
||||
surface: Color(0xFF1A1425),
|
||||
onSurface: Color(0xFFE8E0F0),
|
||||
primary: Color(0xFF7C3AED),
|
||||
onPrimary: Color(0xFFFFFFFF),
|
||||
secondary: Color(0xFFF59E0B),
|
||||
onSecondary: Color(0xFF1A1425),
|
||||
tertiary: Color(0xFF10B981),
|
||||
onTertiary: Color(0xFF1A1425),
|
||||
error: Color(0xFFF87171),
|
||||
onError: Color(0xFF1A1425),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Future (neon cyan/blue, very dark) ───────────────────────────────────────
|
||||
|
||||
class FutureSkinColors extends SkinColors {
|
||||
@override
|
||||
Color get cream => const Color(0xFF0A0E17);
|
||||
@override
|
||||
Color get creamDark => const Color(0xFF111827);
|
||||
@override
|
||||
Color get creamBorder => const Color(0xFF1F2D3D);
|
||||
|
||||
@override
|
||||
Color get brown => const Color(0xFF2979FF);
|
||||
@override
|
||||
Color get brownLight => const Color(0xFF5599FF);
|
||||
@override
|
||||
Color get brownDark => const Color(0xFF00E5FF);
|
||||
@override
|
||||
Color get brownMid => const Color(0xFF99CCFF);
|
||||
|
||||
@override
|
||||
Color get sageGreen => const Color(0xFF00E5FF);
|
||||
@override
|
||||
Color get greenLight => const Color(0xFF0A1A1F);
|
||||
@override
|
||||
Color get greenDark => const Color(0xFF00BCD4);
|
||||
|
||||
@override
|
||||
Color get amber => const Color(0xFFFF6D00);
|
||||
|
||||
@override
|
||||
Color get purple => const Color(0xFFFF1744);
|
||||
@override
|
||||
Color get purpleLight => const Color(0xFF1A0A10);
|
||||
|
||||
@override
|
||||
Color get red => const Color(0xFFFF1744);
|
||||
@override
|
||||
Color get redLight => const Color(0xFF1A0A10);
|
||||
|
||||
@override
|
||||
Color get orange => const Color(0xFFFF6D00);
|
||||
@override
|
||||
Color get orangeLight => const Color(0xFF1A100A);
|
||||
|
||||
@override
|
||||
Color get energyLow => sageGreen;
|
||||
@override
|
||||
Color get energyMedium => amber;
|
||||
@override
|
||||
Color get energyHigh => red;
|
||||
|
||||
/// Future is a dark skin — MUST use [ColorScheme.dark].
|
||||
@override
|
||||
ColorScheme toColorScheme() => const ColorScheme.dark(
|
||||
surface: Color(0xFF0A0E17),
|
||||
onSurface: Color(0xFF00E5FF),
|
||||
primary: Color(0xFF2979FF),
|
||||
onPrimary: Color(0xFF0A0E17),
|
||||
secondary: Color(0xFFFF6D00),
|
||||
onSecondary: Color(0xFF0A0E17),
|
||||
tertiary: Color(0xFF00E5FF),
|
||||
onTertiary: Color(0xFF0A0E17),
|
||||
error: Color(0xFFFF1744),
|
||||
onError: Color(0xFF0A0E17),
|
||||
);
|
||||
}
|
||||
@@ -1,33 +1,44 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'app_skin.dart';
|
||||
import 'skin_colors.dart';
|
||||
import 'skin_type.dart';
|
||||
import 'skin_typography.dart';
|
||||
|
||||
const _kSkinKey = 'app_skin';
|
||||
|
||||
/// Provides the currently active [AppSkin].
|
||||
///
|
||||
/// Initialised to [AppSkin.defaultSkin]; call [loadSaved] at startup to
|
||||
/// restore the user's persisted choice.
|
||||
final skinProvider = NotifierProvider<SkinNotifier, AppSkin>(SkinNotifier.new);
|
||||
/// Module-level initial skin — set before [runApp] via [setInitialSkin].
|
||||
SkinType _initialSkin = SkinType.defaultSkin;
|
||||
|
||||
class SkinNotifier extends Notifier<AppSkin> {
|
||||
/// Sets the initial skin from persisted preferences before [runApp].
|
||||
void setInitialSkin(SkinType skin) => _initialSkin = skin;
|
||||
|
||||
/// Provides the currently active [SkinType].
|
||||
///
|
||||
/// Initialised to the value set by [setInitialSkin] (or [SkinType.defaultSkin]
|
||||
/// if not called). Use [SkinNotifier.setSkin] to switch and persist.
|
||||
final skinProvider = NotifierProvider<SkinNotifier, SkinType>(SkinNotifier.new);
|
||||
|
||||
class SkinNotifier extends Notifier<SkinType> {
|
||||
@override
|
||||
AppSkin build() => AppSkin.defaultSkin;
|
||||
SkinType build() => _initialSkin;
|
||||
|
||||
/// Switches to [skin] and persists the choice.
|
||||
Future<void> setSkin(AppSkin skin) async {
|
||||
Future<void> setSkin(SkinType skin) async {
|
||||
state = skin;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_kSkinKey, skin.key);
|
||||
}
|
||||
|
||||
/// Restores the previously persisted skin from SharedPreferences.
|
||||
Future<void> loadSaved() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final key = prefs.getString(_kSkinKey);
|
||||
if (key != null) {
|
||||
state = AppSkin.fromKey(key);
|
||||
}
|
||||
await prefs.setString(_kSkinKey, skin.storageKey);
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides the [SkinColors] for the currently active skin.
|
||||
final skinColorsProvider = Provider<SkinColors>(
|
||||
(ref) => ref.watch(skinProvider).colors,
|
||||
);
|
||||
|
||||
/// Provides the [SkinTypography] for the currently active skin.
|
||||
final skinTypographyProvider = Provider<SkinTypography>((ref) {
|
||||
final skinType = ref.watch(skinProvider);
|
||||
final colors = ref.watch(skinColorsProvider);
|
||||
return SkinTypography(colors: colors, skinType: skinType);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import 'skin_colors.dart';
|
||||
|
||||
/// All available app skins.
|
||||
enum SkinType {
|
||||
defaultSkin,
|
||||
accessibility,
|
||||
senior,
|
||||
modern,
|
||||
kids,
|
||||
epic,
|
||||
future;
|
||||
}
|
||||
|
||||
extension SkinTypeExtension on SkinType {
|
||||
/// i18n key for the skin name.
|
||||
String get nameKey => 'skin.$name';
|
||||
|
||||
/// i18n key for the skin description.
|
||||
String get descriptionKey => 'skin.${name}_desc';
|
||||
|
||||
/// Stable storage key for SharedPreferences persistence.
|
||||
String get storageKey => name;
|
||||
|
||||
/// Parses a storage key back to [SkinType], defaulting to [SkinType.defaultSkin].
|
||||
static SkinType fromStorageKey(String key) => SkinType.values.firstWhere(
|
||||
(s) => s.storageKey == key,
|
||||
orElse: () => SkinType.defaultSkin,
|
||||
);
|
||||
|
||||
/// Returns the [SkinColors] instance for this skin.
|
||||
SkinColors get colors => switch (this) {
|
||||
SkinType.defaultSkin => DefaultSkinColors(),
|
||||
SkinType.accessibility => AccessibilitySkinColors(),
|
||||
SkinType.senior => SeniorSkinColors(),
|
||||
SkinType.modern => ModernSkinColors(),
|
||||
SkinType.kids => KidsSkinColors(),
|
||||
SkinType.epic => EpicSkinColors(),
|
||||
SkinType.future => FutureSkinColors(),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
import 'skin_colors.dart';
|
||||
import 'skin_type.dart';
|
||||
|
||||
/// Typography system parameterized by [SkinColors] and [SkinType].
|
||||
///
|
||||
/// Font family:
|
||||
/// - accessibility, senior → Atkinson Hyperlegible
|
||||
/// - all others → Lexend
|
||||
///
|
||||
/// Scale factors:
|
||||
/// - senior: 1.25×, kids: 1.1×, accessibility: 1.15×, others: 1.0×
|
||||
class SkinTypography {
|
||||
SkinTypography({required this.colors, required this.skinType});
|
||||
|
||||
final SkinColors colors;
|
||||
final SkinType skinType;
|
||||
|
||||
double get _scale => switch (skinType) {
|
||||
SkinType.accessibility => 1.15,
|
||||
SkinType.senior => 1.25,
|
||||
SkinType.kids => 1.1,
|
||||
_ => 1.0,
|
||||
};
|
||||
|
||||
TextStyle _s(double size, FontWeight w) {
|
||||
final scaled = size * _scale;
|
||||
return switch (skinType) {
|
||||
SkinType.accessibility || SkinType.senior =>
|
||||
GoogleFonts.atkinsonHyperlegible(
|
||||
fontSize: scaled,
|
||||
fontWeight: w,
|
||||
color: colors.brownDark,
|
||||
),
|
||||
_ => GoogleFonts.lexend(
|
||||
fontSize: scaled,
|
||||
fontWeight: w,
|
||||
color: colors.brownDark,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// ── Headings ────────────────────────────────────────────────────────────────
|
||||
TextStyle get h1 => _s(28, FontWeight.w700);
|
||||
TextStyle get h2 => _s(22, FontWeight.w600);
|
||||
TextStyle get h3 => _s(18, FontWeight.w600);
|
||||
|
||||
// ── Body ────────────────────────────────────────────────────────────────────
|
||||
TextStyle get bodyLarge => _s(16, FontWeight.w400);
|
||||
TextStyle get bodyMedium => _s(14, FontWeight.w400);
|
||||
TextStyle get bodySmall => _s(13, FontWeight.w400);
|
||||
|
||||
// ── Labels ──────────────────────────────────────────────────────────────────
|
||||
TextStyle get labelLarge => _s(14, FontWeight.w500);
|
||||
TextStyle get labelMedium => _s(12, FontWeight.w500);
|
||||
TextStyle get labelSmall => _s(11, FontWeight.w500);
|
||||
|
||||
/// Full [TextTheme] for Material 3.
|
||||
TextTheme get textTheme => TextTheme(
|
||||
headlineLarge: h1,
|
||||
headlineMedium: h2,
|
||||
headlineSmall: h3,
|
||||
bodyLarge: bodyLarge,
|
||||
bodyMedium: bodyMedium,
|
||||
bodySmall: bodySmall,
|
||||
labelLarge: labelLarge,
|
||||
labelMedium: labelMedium,
|
||||
labelSmall: labelSmall,
|
||||
titleLarge: h2,
|
||||
titleMedium: h3,
|
||||
titleSmall: labelLarge,
|
||||
);
|
||||
}
|
||||
@@ -6,8 +6,7 @@ import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../../../core/i18n/i18n_provider.dart';
|
||||
import '../../../core/i18n/translation_service.dart';
|
||||
import '../../../core/theme/kittie_colors.dart';
|
||||
import '../../../core/theme/kittie_typography.dart';
|
||||
import '../../../core/theme/skin_provider.dart';
|
||||
import '../../../domain/entities/pet_state_entity.dart';
|
||||
import '../../../domain/entities/pet_type.dart';
|
||||
import '../../../shared/widgets/language_picker.dart';
|
||||
@@ -57,7 +56,8 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
|
||||
const uuid = Uuid();
|
||||
final userId = uuid.v4();
|
||||
final deviceId = '${defaultTargetPlatform.name.toLowerCase()}-${uuid.v4()}';
|
||||
final deviceId =
|
||||
'${defaultTargetPlatform.name.toLowerCase()}-${uuid.v4()}';
|
||||
|
||||
await repo.createUser(
|
||||
id: userId,
|
||||
@@ -85,33 +85,17 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final step = ref.watch(onboardingStepProvider);
|
||||
final skinColors = ref.watch(skinColorsProvider);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: KittieColors.brownDark,
|
||||
backgroundColor: skinColors.brownDark,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// Progress dots
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 24, bottom: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(3, (i) {
|
||||
final isActive = i == step;
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||
width: isActive ? 24 : 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: isActive
|
||||
? KittieColors.amber
|
||||
: KittieColors.brownLight,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
child: _ProgressDots(step: step, skinColors: skinColors),
|
||||
),
|
||||
|
||||
// Pages
|
||||
@@ -143,6 +127,32 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
class _ProgressDots extends StatelessWidget {
|
||||
const _ProgressDots({required this.step, required this.skinColors});
|
||||
final int step;
|
||||
final dynamic skinColors;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(3, (i) {
|
||||
final isActive = i == step;
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||
width: isActive ? 24 : 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: isActive ? skinColors.amber : skinColors.brownLight,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Step 1: Welcome ────────────────────────────────────────────────────────
|
||||
|
||||
class _WelcomeStep extends ConsumerWidget {
|
||||
@@ -152,6 +162,8 @@ class _WelcomeStep extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final t = ref.watch(tProvider);
|
||||
final skinColors = ref.watch(skinColorsProvider);
|
||||
final skinTypo = ref.watch(skinTypographyProvider);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
@@ -164,14 +176,13 @@ class _WelcomeStep extends ConsumerWidget {
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
t('onboarding.welcome_title'),
|
||||
style: KittieTypography.h1.copyWith(color: KittieColors.cream),
|
||||
style: skinTypo.h1.copyWith(color: skinColors.cream),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
t('onboarding.welcome_subtitle'),
|
||||
style:
|
||||
KittieTypography.bodyLarge.copyWith(color: KittieColors.amber),
|
||||
style: skinTypo.bodyLarge.copyWith(color: skinColors.amber),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
@@ -180,8 +191,8 @@ class _WelcomeStep extends ConsumerWidget {
|
||||
child: ElevatedButton(
|
||||
onPressed: onNext,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: KittieColors.amber,
|
||||
foregroundColor: KittieColors.brownDark,
|
||||
backgroundColor: skinColors.amber,
|
||||
foregroundColor: skinColors.brownDark,
|
||||
),
|
||||
child: Text(t('onboarding.start')),
|
||||
),
|
||||
@@ -215,6 +226,8 @@ class _PetSelectionStep extends ConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final selected = ref.watch(selectedPetTypeProvider);
|
||||
final t = ref.watch(tProvider);
|
||||
final skinColors = ref.watch(skinColorsProvider);
|
||||
final skinTypo = ref.watch(skinTypographyProvider);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
@@ -223,7 +236,7 @@ class _PetSelectionStep extends ConsumerWidget {
|
||||
children: [
|
||||
Text(
|
||||
t('onboarding.choose_pet'),
|
||||
style: KittieTypography.h2.copyWith(color: KittieColors.cream),
|
||||
style: skinTypo.h2.copyWith(color: skinColors.cream),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
@@ -242,11 +255,12 @@ class _PetSelectionStep extends ConsumerWidget {
|
||||
height: 80,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: KittieColors.brownMid,
|
||||
color: skinColors.brownMid,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color:
|
||||
isSelected ? KittieColors.amber : Colors.transparent,
|
||||
color: isSelected
|
||||
? skinColors.amber
|
||||
: Colors.transparent,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
@@ -257,8 +271,9 @@ class _PetSelectionStep extends ConsumerWidget {
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
t(pet.labelKey),
|
||||
style: KittieTypography.labelSmall
|
||||
.copyWith(color: KittieColors.cream),
|
||||
style: skinTypo.labelSmall.copyWith(
|
||||
color: skinColors.cream,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -272,20 +287,20 @@ class _PetSelectionStep extends ConsumerWidget {
|
||||
// Name input
|
||||
Text(
|
||||
t('onboarding.pet_name_label'),
|
||||
style:
|
||||
KittieTypography.bodyLarge.copyWith(color: KittieColors.cream),
|
||||
style: skinTypo.bodyLarge.copyWith(color: skinColors.cream),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: nameController,
|
||||
style: KittieTypography.bodyLarge
|
||||
.copyWith(color: KittieColors.brownDark),
|
||||
style:
|
||||
skinTypo.bodyLarge.copyWith(color: skinColors.brownDark),
|
||||
textAlign: TextAlign.center,
|
||||
decoration: InputDecoration(
|
||||
fillColor: KittieColors.creamDark,
|
||||
fillColor: skinColors.creamDark,
|
||||
hintText: t('onboarding.pet_name_hint'),
|
||||
hintStyle: KittieTypography.bodyLarge
|
||||
.copyWith(color: KittieColors.brownLight),
|
||||
hintStyle: skinTypo.bodyLarge.copyWith(
|
||||
color: skinColors.brownLight,
|
||||
),
|
||||
),
|
||||
onChanged: (value) {
|
||||
ref.read(petNameProvider.notifier).update(value);
|
||||
@@ -301,16 +316,17 @@ class _PetSelectionStep extends ConsumerWidget {
|
||||
onPressed: onBack,
|
||||
child: Text(
|
||||
t('common.back'),
|
||||
style: KittieTypography.labelLarge
|
||||
.copyWith(color: KittieColors.brownLight),
|
||||
style: skinTypo.labelLarge.copyWith(
|
||||
color: skinColors.brownLight,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
ElevatedButton(
|
||||
onPressed: onNext,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: KittieColors.amber,
|
||||
foregroundColor: KittieColors.brownDark,
|
||||
backgroundColor: skinColors.amber,
|
||||
foregroundColor: skinColors.brownDark,
|
||||
),
|
||||
child: Text(t('onboarding.continue')),
|
||||
),
|
||||
@@ -334,6 +350,8 @@ class _ReadyStep extends ConsumerWidget {
|
||||
final petName = ref.watch(petNameProvider);
|
||||
final petType = ref.watch(selectedPetTypeProvider);
|
||||
final t = ref.watch(tProvider);
|
||||
final skinColors = ref.watch(skinColorsProvider);
|
||||
final skinTypo = ref.watch(skinTypographyProvider);
|
||||
|
||||
final emoji = switch (petType) {
|
||||
PetType.dog => '🐶',
|
||||
@@ -346,23 +364,22 @@ class _ReadyStep extends ConsumerWidget {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icon(
|
||||
Icons.check_circle_rounded,
|
||||
color: KittieColors.sageGreen,
|
||||
color: skinColors.sageGreen,
|
||||
size: 80,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
t('onboarding.ready_title'),
|
||||
style: KittieTypography.h1.copyWith(color: KittieColors.cream),
|
||||
style: skinTypo.h1.copyWith(color: skinColors.cream),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
t('onboarding.ready_subtitle',
|
||||
params: {'emoji': emoji, 'petName': petName}),
|
||||
style:
|
||||
KittieTypography.bodyLarge.copyWith(color: KittieColors.amber),
|
||||
style: skinTypo.bodyLarge.copyWith(color: skinColors.amber),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
@@ -372,16 +389,17 @@ class _ReadyStep extends ConsumerWidget {
|
||||
onPressed: onBack,
|
||||
child: Text(
|
||||
t('common.back'),
|
||||
style: KittieTypography.labelLarge
|
||||
.copyWith(color: KittieColors.brownLight),
|
||||
style: skinTypo.labelLarge.copyWith(
|
||||
color: skinColors.brownLight,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
ElevatedButton(
|
||||
onPressed: onComplete,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: KittieColors.sageGreen,
|
||||
foregroundColor: KittieColors.cream,
|
||||
backgroundColor: skinColors.sageGreen,
|
||||
foregroundColor: skinColors.cream,
|
||||
),
|
||||
child: Text(t('onboarding.start_using')),
|
||||
),
|
||||
|
||||
@@ -4,8 +4,7 @@ import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../../../core/constants/app_constants.dart';
|
||||
import '../../../core/i18n/i18n_provider.dart';
|
||||
import '../../../core/theme/kittie_colors.dart';
|
||||
import '../../../core/theme/kittie_typography.dart';
|
||||
import '../../../core/theme/skin_provider.dart';
|
||||
import '../../../domain/entities/task_entity.dart';
|
||||
import '../providers/add_task_providers.dart';
|
||||
import '../providers/pet_providers.dart';
|
||||
@@ -81,8 +80,8 @@ class _AddTaskSheetState extends ConsumerState<_AddTaskSheet> {
|
||||
final int minute = time?.minute ?? 0;
|
||||
return switch (day) {
|
||||
TaskDay.today => DateTime(now.year, now.month, now.day, hour, minute),
|
||||
TaskDay.tomorrow => DateTime(
|
||||
now.year, now.month, now.day + 1, hour, minute),
|
||||
TaskDay.tomorrow =>
|
||||
DateTime(now.year, now.month, now.day + 1, hour, minute),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
@@ -114,11 +113,12 @@ class _AddTaskSheetState extends ConsumerState<_AddTaskSheet> {
|
||||
Widget build(BuildContext context) {
|
||||
final step = ref.watch(addTaskStepProvider);
|
||||
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
|
||||
final skinColors = ref.watch(skinColorsProvider);
|
||||
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: KittieColors.cream,
|
||||
borderRadius: BorderRadius.vertical(
|
||||
decoration: BoxDecoration(
|
||||
color: skinColors.cream,
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(AppConstants.bottomSheetBorderRadius),
|
||||
),
|
||||
),
|
||||
@@ -136,34 +136,14 @@ class _AddTaskSheetState extends ConsumerState<_AddTaskSheet> {
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: KittieColors.creamBorder,
|
||||
color: skinColors.creamBorder,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Step dots
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(4, (i) {
|
||||
final isActive = i == step;
|
||||
final isPast = i < step;
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 3),
|
||||
width: isActive ? 20 : 6,
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
color: isActive
|
||||
? KittieColors.brown
|
||||
: isPast
|
||||
? KittieColors.sageGreen
|
||||
: KittieColors.creamBorder,
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
_StepDots(step: step),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Step content
|
||||
@@ -188,8 +168,7 @@ class _AddTaskSheetState extends ConsumerState<_AddTaskSheet> {
|
||||
),
|
||||
1 => _Step2When(
|
||||
key: const ValueKey(1),
|
||||
onNext: () =>
|
||||
ref.read(addTaskStepProvider.notifier).next(),
|
||||
onNext: () => ref.read(addTaskStepProvider.notifier).next(),
|
||||
onBack: () =>
|
||||
ref.read(addTaskStepProvider.notifier).previous(),
|
||||
),
|
||||
@@ -213,6 +192,38 @@ class _AddTaskSheetState extends ConsumerState<_AddTaskSheet> {
|
||||
}
|
||||
}
|
||||
|
||||
class _StepDots extends ConsumerWidget {
|
||||
const _StepDots({required this.step});
|
||||
final int step;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final skinColors = ref.watch(skinColorsProvider);
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(4, (i) {
|
||||
final isActive = i == step;
|
||||
final isPast = i < step;
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 3),
|
||||
width: isActive ? 20 : 6,
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
color: isActive
|
||||
? skinColors.brown
|
||||
: isPast
|
||||
? skinColors.sageGreen
|
||||
: skinColors.creamBorder,
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Step 1: Title & Note ───────────────────────────────────────────────────
|
||||
|
||||
class _Step1Title extends ConsumerWidget {
|
||||
@@ -230,28 +241,28 @@ class _Step1Title extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final t = ref.watch(tProvider);
|
||||
final skinColors = ref.watch(skinColorsProvider);
|
||||
final skinTypo = ref.watch(skinTypographyProvider);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(t('tasks.new_task'), style: KittieTypography.h2),
|
||||
Text(t('tasks.new_task'), style: skinTypo.h2),
|
||||
const SizedBox(height: 20),
|
||||
Text(t('tasks.title_label'), style: KittieTypography.labelMedium),
|
||||
Text(t('tasks.title_label'), style: skinTypo.labelMedium),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: titleController,
|
||||
autofocus: true,
|
||||
style: KittieTypography.bodyLarge
|
||||
.copyWith(color: KittieColors.brownDark),
|
||||
style: skinTypo.bodyLarge.copyWith(color: skinColors.brownDark),
|
||||
decoration: InputDecoration(hintText: t('tasks.title_hint')),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(t('tasks.note_label'), style: KittieTypography.labelMedium),
|
||||
Text(t('tasks.note_label'), style: skinTypo.labelMedium),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: noteController,
|
||||
style: KittieTypography.bodyMedium
|
||||
.copyWith(color: KittieColors.brownDark),
|
||||
style: skinTypo.bodyMedium.copyWith(color: skinColors.brownDark),
|
||||
maxLines: 3,
|
||||
decoration: InputDecoration(hintText: t('tasks.note_hint')),
|
||||
),
|
||||
@@ -294,11 +305,13 @@ class _Step2When extends ConsumerWidget {
|
||||
final selectedDay = ref.watch(selectedTaskDayProvider);
|
||||
final selectedTime = ref.watch(selectedTaskTimeProvider);
|
||||
final t = ref.watch(tProvider);
|
||||
final skinColors = ref.watch(skinColorsProvider);
|
||||
final skinTypo = ref.watch(skinTypographyProvider);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(t('tasks.when_title'), style: KittieTypography.h2),
|
||||
Text(t('tasks.when_title'), style: skinTypo.h2),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Day chips
|
||||
@@ -313,11 +326,10 @@ class _Step2When extends ConsumerWidget {
|
||||
onSelected: (_) {
|
||||
ref.read(selectedTaskDayProvider.notifier).select(d.day);
|
||||
},
|
||||
selectedColor: KittieColors.amber,
|
||||
backgroundColor: KittieColors.creamDark,
|
||||
labelStyle: KittieTypography.labelLarge.copyWith(
|
||||
color:
|
||||
isSelected ? KittieColors.brownDark : KittieColors.brown,
|
||||
selectedColor: skinColors.amber,
|
||||
backgroundColor: skinColors.creamDark,
|
||||
labelStyle: skinTypo.labelLarge.copyWith(
|
||||
color: isSelected ? skinColors.brownDark : skinColors.brown,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
@@ -331,7 +343,7 @@ class _Step2When extends ConsumerWidget {
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Time presets
|
||||
Text(t('tasks.time_label'), style: KittieTypography.labelMedium),
|
||||
Text(t('tasks.time_label'), style: skinTypo.labelMedium),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
@@ -345,11 +357,10 @@ class _Step2When extends ConsumerWidget {
|
||||
onSelected: (_) {
|
||||
ref.read(selectedTaskTimeProvider.notifier).select(time);
|
||||
},
|
||||
selectedColor: KittieColors.amber,
|
||||
backgroundColor: KittieColors.creamDark,
|
||||
labelStyle: KittieTypography.labelLarge.copyWith(
|
||||
color:
|
||||
isSelected ? KittieColors.brownDark : KittieColors.brown,
|
||||
selectedColor: skinColors.amber,
|
||||
backgroundColor: skinColors.creamDark,
|
||||
labelStyle: skinTypo.labelLarge.copyWith(
|
||||
color: isSelected ? skinColors.brownDark : skinColors.brown,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
@@ -391,16 +402,33 @@ class _Step3Energy extends ConsumerWidget {
|
||||
final VoidCallback onSave;
|
||||
final VoidCallback onBack;
|
||||
|
||||
static const _energyOptions = [
|
||||
(level: 1, labelKey: 'tasks.energy_low', emoji: '🍃', color: KittieColors.sageGreen),
|
||||
(level: 2, labelKey: 'tasks.energy_medium', emoji: '⚡', color: KittieColors.orange),
|
||||
(level: 3, labelKey: 'tasks.energy_high', emoji: '🔥', color: KittieColors.red),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final selected = ref.watch(selectedEnergyProvider);
|
||||
final t = ref.watch(tProvider);
|
||||
final skinColors = ref.watch(skinColorsProvider);
|
||||
final skinTypo = ref.watch(skinTypographyProvider);
|
||||
|
||||
final energyOptions = [
|
||||
(
|
||||
level: 1,
|
||||
labelKey: 'tasks.energy_low',
|
||||
emoji: '🍃',
|
||||
color: skinColors.sageGreen
|
||||
),
|
||||
(
|
||||
level: 2,
|
||||
labelKey: 'tasks.energy_medium',
|
||||
emoji: '⚡',
|
||||
color: skinColors.orange
|
||||
),
|
||||
(
|
||||
level: 3,
|
||||
labelKey: 'tasks.energy_high',
|
||||
emoji: '🔥',
|
||||
color: skinColors.red
|
||||
),
|
||||
];
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -409,7 +437,7 @@ class _Step3Energy extends ConsumerWidget {
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: KittieColors.creamDark,
|
||||
color: skinColors.creamDark,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Row(
|
||||
@@ -419,7 +447,7 @@ class _Step3Energy extends ConsumerWidget {
|
||||
Expanded(
|
||||
child: Text(
|
||||
t('tasks.energy_question'),
|
||||
style: KittieTypography.bodyLarge,
|
||||
style: skinTypo.bodyLarge,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -429,7 +457,7 @@ class _Step3Energy extends ConsumerWidget {
|
||||
|
||||
// Energy picker
|
||||
Row(
|
||||
children: _energyOptions.map((opt) {
|
||||
children: energyOptions.map((opt) {
|
||||
final isSelected = selected == opt.level;
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
@@ -443,7 +471,7 @@ class _Step3Energy extends ConsumerWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? opt.color.withValues(alpha: 0.15)
|
||||
: KittieColors.creamDark,
|
||||
: skinColors.creamDark,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: isSelected ? opt.color : Colors.transparent,
|
||||
@@ -456,8 +484,8 @@ class _Step3Energy extends ConsumerWidget {
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
t(opt.labelKey),
|
||||
style: KittieTypography.labelLarge.copyWith(
|
||||
color: isSelected ? opt.color : KittieColors.brown,
|
||||
style: skinTypo.labelLarge.copyWith(
|
||||
color: isSelected ? opt.color : skinColors.brown,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -479,9 +507,9 @@ class _Step3Energy extends ConsumerWidget {
|
||||
ElevatedButton(
|
||||
onPressed: selected != null ? onSave : null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: KittieColors.sageGreen,
|
||||
foregroundColor: KittieColors.cream,
|
||||
disabledBackgroundColor: KittieColors.creamDark,
|
||||
backgroundColor: skinColors.sageGreen,
|
||||
foregroundColor: skinColors.cream,
|
||||
disabledBackgroundColor: skinColors.creamDark,
|
||||
),
|
||||
child: Text(t('tasks.save_task')),
|
||||
),
|
||||
@@ -507,16 +535,18 @@ class _Step4Confirmation extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final t = ref.watch(tProvider);
|
||||
final skinColors = ref.watch(skinColorsProvider);
|
||||
final skinTypo = ref.watch(skinTypographyProvider);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
const Icon(
|
||||
Icon(
|
||||
Icons.check_circle_rounded,
|
||||
color: KittieColors.sageGreen,
|
||||
color: skinColors.sageGreen,
|
||||
size: 64,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(t('tasks.task_saved'), style: KittieTypography.h2),
|
||||
Text(t('tasks.task_saved'), style: skinTypo.h2),
|
||||
const SizedBox(height: 32),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
|
||||
@@ -2,8 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../core/i18n/i18n_provider.dart';
|
||||
import '../../../core/theme/kittie_colors.dart';
|
||||
import '../../../core/theme/kittie_typography.dart';
|
||||
import '../../../core/theme/skin_provider.dart';
|
||||
import '../providers/pet_providers.dart';
|
||||
import '../providers/task_providers.dart';
|
||||
|
||||
@@ -19,12 +18,15 @@ class PetScreen extends ConsumerWidget {
|
||||
final level = ref.watch(petLevelProvider);
|
||||
final selfCareCount = ref.watch(selfCareCountProvider);
|
||||
final t = ref.watch(tProvider);
|
||||
final skinColors = ref.watch(skinColorsProvider);
|
||||
final skinTypo = ref.watch(skinTypographyProvider);
|
||||
|
||||
final unlocked = petState.unlockedItems.isEmpty
|
||||
? <String>[]
|
||||
: petState.unlockedItems.split(',');
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: KittieColors.cream,
|
||||
backgroundColor: skinColors.cream,
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
@@ -33,14 +35,14 @@ class PetScreen extends ConsumerWidget {
|
||||
// Pet name header
|
||||
Text(
|
||||
user.petName,
|
||||
style: KittieTypography.h1.copyWith(fontSize: 24),
|
||||
style: skinTypo.h1.copyWith(fontSize: 24),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
t('pet.companion_label',
|
||||
params: {'petType': user.petType.label}),
|
||||
style: KittieTypography.bodyMedium.copyWith(
|
||||
color: KittieColors.brownLight,
|
||||
style: skinTypo.bodyMedium.copyWith(
|
||||
color: skinColors.brownLight,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
@@ -54,9 +56,9 @@ class PetScreen extends ConsumerWidget {
|
||||
height: 130,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: KittieColors.creamDark,
|
||||
color: skinColors.creamDark,
|
||||
border: Border.all(
|
||||
color: KittieColors.creamBorder,
|
||||
color: skinColors.creamBorder,
|
||||
width: 3,
|
||||
),
|
||||
),
|
||||
@@ -74,10 +76,10 @@ class PetScreen extends ConsumerWidget {
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: _moodColor(mood),
|
||||
color: _moodColor(mood, skinColors),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: KittieColors.cream,
|
||||
color: skinColors.cream,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
@@ -107,25 +109,29 @@ class PetScreen extends ConsumerWidget {
|
||||
label: t('pet.stat_streak'),
|
||||
value: t('pet.stat_streak_value',
|
||||
params: {'days': '${petState.streakDays}'}),
|
||||
color: KittieColors.orange,
|
||||
color: skinColors.orange,
|
||||
skinTypo: skinTypo,
|
||||
),
|
||||
_StatTile(
|
||||
icon: Icons.check_circle_rounded,
|
||||
label: t('pet.stat_tasks_done'),
|
||||
value: '${petState.totalTasksDone}',
|
||||
color: KittieColors.sageGreen,
|
||||
color: skinColors.sageGreen,
|
||||
skinTypo: skinTypo,
|
||||
),
|
||||
_StatTile(
|
||||
icon: Icons.favorite_rounded,
|
||||
label: t('pet.stat_self_care'),
|
||||
value: '$selfCareCount',
|
||||
color: KittieColors.purple,
|
||||
color: skinColors.purple,
|
||||
skinTypo: skinTypo,
|
||||
),
|
||||
_StatTile(
|
||||
icon: Icons.star_rounded,
|
||||
label: t('pet.stat_level'),
|
||||
value: '$level',
|
||||
color: KittieColors.amber,
|
||||
color: skinColors.amber,
|
||||
skinTypo: skinTypo,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -134,8 +140,7 @@ class PetScreen extends ConsumerWidget {
|
||||
// Unlocked items header
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(t('pet.unlocked_items'),
|
||||
style: KittieTypography.h3),
|
||||
child: Text(t('pet.unlocked_items'), style: skinTypo.h3),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
@@ -149,21 +154,29 @@ class PetScreen extends ConsumerWidget {
|
||||
name: t('pet.item_bow'),
|
||||
icon: Icons.catching_pokemon_rounded,
|
||||
unlocked: unlocked.contains('masle'),
|
||||
skinColors: skinColors,
|
||||
skinTypo: skinTypo,
|
||||
),
|
||||
_ItemCard(
|
||||
name: t('pet.item_bed'),
|
||||
icon: Icons.bed_rounded,
|
||||
unlocked: unlocked.contains('pelisek'),
|
||||
skinColors: skinColors,
|
||||
skinTypo: skinTypo,
|
||||
),
|
||||
_ItemCard(
|
||||
name: t('pet.item_crown'),
|
||||
icon: Icons.auto_awesome_rounded,
|
||||
unlocked: unlocked.contains('koruna'),
|
||||
skinColors: skinColors,
|
||||
skinTypo: skinTypo,
|
||||
),
|
||||
_ItemCard(
|
||||
name: t('pet.item_wings'),
|
||||
icon: Icons.flight_rounded,
|
||||
unlocked: unlocked.contains('kridla'),
|
||||
skinColors: skinColors,
|
||||
skinTypo: skinTypo,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -177,10 +190,10 @@ class PetScreen extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Color _moodColor(PetMood mood) => switch (mood) {
|
||||
PetMood.happy => KittieColors.sageGreen,
|
||||
PetMood.neutral => KittieColors.amber,
|
||||
PetMood.sad => KittieColors.red,
|
||||
Color _moodColor(PetMood mood, dynamic skinColors) => switch (mood) {
|
||||
PetMood.happy => skinColors.sageGreen,
|
||||
PetMood.neutral => skinColors.amber,
|
||||
PetMood.sad => skinColors.red,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -191,12 +204,14 @@ class _StatTile extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
final Color color;
|
||||
final dynamic skinTypo;
|
||||
|
||||
const _StatTile({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.color,
|
||||
required this.skinTypo,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -213,14 +228,8 @@ class _StatTile extends StatelessWidget {
|
||||
children: [
|
||||
Icon(icon, color: color, size: 24),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
value,
|
||||
style: KittieTypography.h2.copyWith(color: color),
|
||||
),
|
||||
Text(
|
||||
label,
|
||||
style: KittieTypography.bodySmall.copyWith(color: color),
|
||||
),
|
||||
Text(value, style: skinTypo.h2.copyWith(color: color)),
|
||||
Text(label, style: skinTypo.bodySmall.copyWith(color: color)),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -231,11 +240,15 @@ class _ItemCard extends StatelessWidget {
|
||||
final String name;
|
||||
final IconData icon;
|
||||
final bool unlocked;
|
||||
final dynamic skinColors;
|
||||
final dynamic skinTypo;
|
||||
|
||||
const _ItemCard({
|
||||
required this.name,
|
||||
required this.icon,
|
||||
required this.unlocked,
|
||||
required this.skinColors,
|
||||
required this.skinTypo,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -246,9 +259,9 @@ class _ItemCard extends StatelessWidget {
|
||||
width: 80,
|
||||
margin: const EdgeInsets.only(right: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: KittieColors.creamDark,
|
||||
color: skinColors.creamDark,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: KittieColors.creamBorder),
|
||||
border: Border.all(color: skinColors.creamBorder),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -256,19 +269,19 @@ class _ItemCard extends StatelessWidget {
|
||||
Icon(
|
||||
icon,
|
||||
size: 32,
|
||||
color: unlocked ? KittieColors.amber : KittieColors.brownLight,
|
||||
color: unlocked ? skinColors.amber : skinColors.brownLight,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
name,
|
||||
style: KittieTypography.labelSmall,
|
||||
style: skinTypo.labelSmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (!unlocked)
|
||||
Icon(
|
||||
Icons.lock_rounded,
|
||||
size: 12,
|
||||
color: KittieColors.brownLight,
|
||||
color: skinColors.brownLight,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
import '../../../core/i18n/i18n_provider.dart';
|
||||
import '../../../core/theme/kittie_colors.dart';
|
||||
import '../../../core/theme/skin_provider.dart';
|
||||
import '../screens/add_task_screen.dart';
|
||||
import '../../../domain/entities/pet_type.dart';
|
||||
import '../../../domain/entities/task_status.dart';
|
||||
@@ -29,6 +28,8 @@ class TasksScreen extends ConsumerWidget {
|
||||
final petMood = ref.watch(petMoodProvider);
|
||||
final userName = ref.watch(userNameProvider);
|
||||
final t = ref.watch(tProvider);
|
||||
final skinColors = ref.watch(skinColorsProvider);
|
||||
final skinTypo = ref.watch(skinTypographyProvider);
|
||||
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
@@ -37,10 +38,8 @@ class TasksScreen extends ConsumerWidget {
|
||||
error: (e, _) =>
|
||||
Center(child: Text(t('tasks.error', params: {'error': '$e'}))),
|
||||
data: (tasks) {
|
||||
final pendingTasks =
|
||||
tasks.where((t) => !t.isCompleted).toList();
|
||||
final completedTasks =
|
||||
tasks.where((t) => t.isCompleted).toList();
|
||||
final pendingTasks = tasks.where((t) => !t.isCompleted).toList();
|
||||
final completedTasks = tasks.where((t) => t.isCompleted).toList();
|
||||
final pendingCount = pendingTasks.length;
|
||||
|
||||
if (tasks.isEmpty) {
|
||||
@@ -55,10 +54,8 @@ class TasksScreen extends ConsumerWidget {
|
||||
// 1. Date header
|
||||
Text(
|
||||
localizedDateHeader(DateTime.now(), t),
|
||||
style: GoogleFonts.lexend(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: KittieColors.brownLight,
|
||||
style: skinTypo.labelSmall.copyWith(
|
||||
color: skinColors.brownLight,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
@@ -67,10 +64,8 @@ class TasksScreen extends ConsumerWidget {
|
||||
// 2. Greeting
|
||||
Text(
|
||||
'${localizedGreeting(t)}, $userName',
|
||||
style: GoogleFonts.lexend(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: KittieColors.brownDark,
|
||||
style: skinTypo.h2.copyWith(
|
||||
color: skinColors.brownDark,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
@@ -100,10 +95,8 @@ class TasksScreen extends ConsumerWidget {
|
||||
children: [
|
||||
Text(
|
||||
t('tasks.section_today'),
|
||||
style: GoogleFonts.lexend(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: KittieColors.brownLight,
|
||||
style: skinTypo.labelSmall.copyWith(
|
||||
color: skinColors.brownLight,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
@@ -111,15 +104,14 @@ class TasksScreen extends ConsumerWidget {
|
||||
Container(
|
||||
width: 22,
|
||||
height: 22,
|
||||
decoration: const BoxDecoration(
|
||||
color: KittieColors.amber,
|
||||
decoration: BoxDecoration(
|
||||
color: skinColors.amber,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'$pendingCount',
|
||||
style: GoogleFonts.lexend(
|
||||
fontSize: 11,
|
||||
style: skinTypo.labelSmall.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white,
|
||||
),
|
||||
@@ -180,12 +172,9 @@ class TasksScreen extends ConsumerWidget {
|
||||
String Function(String, {Map<String, String>? params}) t,
|
||||
) {
|
||||
return switch (mood) {
|
||||
PetMood.happy =>
|
||||
t('pet.mood_happy', params: {'petName': petName}),
|
||||
PetMood.neutral =>
|
||||
t('pet.mood_neutral', params: {'petName': petName}),
|
||||
PetMood.sad =>
|
||||
t('pet.mood_sad', params: {'petName': petName}),
|
||||
PetMood.happy => t('pet.mood_happy', params: {'petName': petName}),
|
||||
PetMood.neutral => t('pet.mood_neutral', params: {'petName': petName}),
|
||||
PetMood.sad => t('pet.mood_sad', params: {'petName': petName}),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -199,6 +188,8 @@ class _EmptyState extends ConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final emoji = petType.emoji;
|
||||
final t = ref.watch(tProvider);
|
||||
final skinColors = ref.watch(skinColorsProvider);
|
||||
final skinTypo = ref.watch(skinTypographyProvider);
|
||||
|
||||
return Center(
|
||||
child: Column(
|
||||
@@ -208,19 +199,12 @@ class _EmptyState extends ConsumerWidget {
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
t('tasks.empty_title'),
|
||||
style: GoogleFonts.lexend(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: KittieColors.brownLight,
|
||||
),
|
||||
style: skinTypo.h3.copyWith(color: skinColors.brownLight),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
t('tasks.empty_subtitle'),
|
||||
style: GoogleFonts.lexend(
|
||||
fontSize: 14,
|
||||
color: KittieColors.brownLight,
|
||||
),
|
||||
style: skinTypo.bodyMedium.copyWith(color: skinColors.brownLight),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -3,8 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../core/i18n/i18n_provider.dart';
|
||||
import '../../../core/theme/kittie_colors.dart';
|
||||
import '../../../core/theme/kittie_typography.dart';
|
||||
import '../../../core/theme/skin_provider.dart';
|
||||
import '../../../shared/widgets/energy_badge.dart';
|
||||
import '../../../domain/entities/task_entity.dart';
|
||||
import '../../tasks/providers/task_providers.dart';
|
||||
@@ -21,9 +20,11 @@ class ZenScreen extends ConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final task = ref.watch(nextZenTaskProvider);
|
||||
final t = ref.watch(tProvider);
|
||||
final skinColors = ref.watch(skinColorsProvider);
|
||||
final skinTypo = ref.watch(skinTypographyProvider);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: KittieColors.creamDark,
|
||||
backgroundColor: skinColors.creamDark,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -34,7 +35,7 @@ class ZenScreen extends ConsumerWidget {
|
||||
padding: const EdgeInsets.only(left: 8, top: 8),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_rounded),
|
||||
color: KittieColors.brown,
|
||||
color: skinColors.brown,
|
||||
onPressed: () => context.pop(),
|
||||
),
|
||||
),
|
||||
@@ -44,8 +45,8 @@ class ZenScreen extends ConsumerWidget {
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: task == null
|
||||
? _buildEmpty(t)
|
||||
: _buildTask(context, ref, task, t),
|
||||
? _buildEmpty(t, skinColors, skinTypo)
|
||||
: _buildTask(context, ref, task, t, skinColors, skinTypo),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -58,20 +59,27 @@ class ZenScreen extends ConsumerWidget {
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
ref.read(taskRepositoryProvider).completeTask(task.id);
|
||||
ref.read(petStateProvider.notifier).incrementTasksDone();
|
||||
ref
|
||||
.read(taskRepositoryProvider)
|
||||
.completeTask(task.id);
|
||||
ref
|
||||
.read(petStateProvider.notifier)
|
||||
.incrementTasksDone();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: KittieColors.sageGreen,
|
||||
backgroundColor: skinColors.sageGreen,
|
||||
foregroundColor: Colors.white,
|
||||
minimumSize: const Size(0, 52),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
child: Text(t('zen.done'),
|
||||
style: KittieTypography.labelLarge
|
||||
.copyWith(color: Colors.white)),
|
||||
child: Text(
|
||||
t('zen.done'),
|
||||
style: skinTypo.labelLarge.copyWith(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
@@ -80,22 +88,24 @@ class ZenScreen extends ConsumerWidget {
|
||||
onPressed: () {
|
||||
ref.read(taskRepositoryProvider).updateTask(
|
||||
task.copyWith(
|
||||
sortOrder: task.sortOrder + 100),
|
||||
sortOrder: task.sortOrder + 100,
|
||||
),
|
||||
);
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: KittieColors.brownLight,
|
||||
foregroundColor: skinColors.brownLight,
|
||||
minimumSize: const Size(0, 52),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: const BorderSide(
|
||||
color: KittieColors.creamBorder,
|
||||
),
|
||||
side: BorderSide(color: skinColors.creamBorder),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
t('zen.skip'),
|
||||
style: skinTypo.labelLarge.copyWith(
|
||||
color: skinColors.brownLight,
|
||||
),
|
||||
),
|
||||
child: Text(t('zen.skip'),
|
||||
style: KittieTypography.labelLarge
|
||||
.copyWith(color: KittieColors.brownLight)),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -107,7 +117,11 @@ class ZenScreen extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmpty(String Function(String, {Map<String, String>? params}) t) {
|
||||
Widget _buildEmpty(
|
||||
String Function(String, {Map<String, String>? params}) t,
|
||||
dynamic skinColors,
|
||||
dynamic skinTypo,
|
||||
) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -116,7 +130,7 @@ class ZenScreen extends ConsumerWidget {
|
||||
Text(
|
||||
t('zen.all_done_title'),
|
||||
textAlign: TextAlign.center,
|
||||
style: KittieTypography.h2.copyWith(color: KittieColors.brown),
|
||||
style: skinTypo.h2.copyWith(color: skinColors.brown),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -127,6 +141,8 @@ class ZenScreen extends ConsumerWidget {
|
||||
WidgetRef ref,
|
||||
TaskEntity task,
|
||||
String Function(String, {Map<String, String>? params}) t,
|
||||
dynamic skinColors,
|
||||
dynamic skinTypo,
|
||||
) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
@@ -136,7 +152,7 @@ class ZenScreen extends ConsumerWidget {
|
||||
Text(
|
||||
task.title,
|
||||
textAlign: TextAlign.center,
|
||||
style: KittieTypography.h1.copyWith(fontSize: 28),
|
||||
style: skinTypo.h1.copyWith(fontSize: 28),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
EnergyBadge(energyLevel: task.energyLevel),
|
||||
|
||||
+12
-1
@@ -6,10 +6,19 @@ import 'core/i18n/i18n_provider.dart';
|
||||
import 'core/i18n/translation_service.dart';
|
||||
import 'core/router/app_router.dart';
|
||||
import 'core/theme/kittie_theme.dart';
|
||||
import 'core/theme/skin_provider.dart';
|
||||
import 'core/theme/skin_type.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
// Restore saved skin before runApp so the first frame uses the right theme.
|
||||
final savedSkinKey = prefs.getString('app_skin');
|
||||
if (savedSkinKey != null) {
|
||||
setInitialSkin(SkinTypeExtension.fromStorageKey(savedSkinKey));
|
||||
}
|
||||
|
||||
final savedLocale = prefs.getString('app_locale') ?? 'cs';
|
||||
await TranslationService.instance.init(locale: savedLocale);
|
||||
runApp(const ProviderScope(child: KittieApp()));
|
||||
@@ -22,10 +31,12 @@ class KittieApp extends ConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
ref.watch(localeProvider);
|
||||
final router = ref.watch(appRouterProvider);
|
||||
final skinColors = ref.watch(skinColorsProvider);
|
||||
final skinTypo = ref.watch(skinTypographyProvider);
|
||||
|
||||
return MaterialApp.router(
|
||||
title: 'KittieMobile',
|
||||
theme: KittieTheme.light,
|
||||
theme: KittieTheme.fromSkin(skinColors, skinTypo),
|
||||
routerConfig: router,
|
||||
debugShowCheckedModeBanner: false,
|
||||
);
|
||||
|
||||
@@ -3,8 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../core/i18n/i18n_provider.dart';
|
||||
import '../../core/theme/kittie_colors.dart';
|
||||
import '../../core/theme/kittie_typography.dart';
|
||||
import '../../core/theme/skin_provider.dart';
|
||||
import '../../core/constants/app_constants.dart';
|
||||
|
||||
/// Bottom navigation shell wrapping the main tabs.
|
||||
@@ -16,15 +15,17 @@ class AppShell extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final t = ref.watch(tProvider);
|
||||
final skinColors = ref.watch(skinColorsProvider);
|
||||
final skinTypo = ref.watch(skinTypographyProvider);
|
||||
|
||||
return Scaffold(
|
||||
body: navigationShell,
|
||||
bottomNavigationBar: Container(
|
||||
height: AppConstants.bottomNavHeight,
|
||||
decoration: const BoxDecoration(
|
||||
color: KittieColors.cream,
|
||||
decoration: BoxDecoration(
|
||||
color: skinColors.cream,
|
||||
border: Border(
|
||||
top: BorderSide(color: KittieColors.creamBorder, width: 0.5),
|
||||
top: BorderSide(color: skinColors.creamBorder, width: 0.5),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
@@ -35,18 +36,24 @@ class AppShell extends ConsumerWidget {
|
||||
label: t('nav.tasks'),
|
||||
isActive: navigationShell.currentIndex == 0,
|
||||
onTap: () => navigationShell.goBranch(0),
|
||||
skinColors: skinColors,
|
||||
skinTypo: skinTypo,
|
||||
),
|
||||
_NavItem(
|
||||
icon: Icons.pets_rounded,
|
||||
label: t('nav.pet'),
|
||||
isActive: navigationShell.currentIndex == 1,
|
||||
onTap: () => navigationShell.goBranch(1),
|
||||
skinColors: skinColors,
|
||||
skinTypo: skinTypo,
|
||||
),
|
||||
_NavItem(
|
||||
icon: Icons.person_rounded,
|
||||
label: t('nav.profile'),
|
||||
isActive: navigationShell.currentIndex == 2,
|
||||
onTap: () => navigationShell.goBranch(2),
|
||||
skinColors: skinColors,
|
||||
skinTypo: skinTypo,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -60,17 +67,22 @@ class _NavItem extends StatelessWidget {
|
||||
final String label;
|
||||
final bool isActive;
|
||||
final VoidCallback onTap;
|
||||
final dynamic skinColors;
|
||||
final dynamic skinTypo;
|
||||
|
||||
const _NavItem({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.isActive,
|
||||
required this.onTap,
|
||||
required this.skinColors,
|
||||
required this.skinTypo,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = isActive ? KittieColors.brown : KittieColors.brownLight;
|
||||
final color =
|
||||
isActive ? skinColors.brown : skinColors.brownLight;
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
@@ -82,14 +94,17 @@ class _NavItem extends StatelessWidget {
|
||||
children: [
|
||||
Icon(icon, color: color, size: 24),
|
||||
const SizedBox(height: 2),
|
||||
Text(label, style: KittieTypography.labelSmall.copyWith(color: color)),
|
||||
Text(
|
||||
label,
|
||||
style: skinTypo.labelSmall.copyWith(color: color),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
if (isActive)
|
||||
Container(
|
||||
width: 4,
|
||||
height: 4,
|
||||
decoration: const BoxDecoration(
|
||||
color: KittieColors.amber,
|
||||
decoration: BoxDecoration(
|
||||
color: skinColors.amber,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../core/constants/app_constants.dart';
|
||||
import '../../core/theme/kittie_colors.dart';
|
||||
import '../../core/theme/kittie_typography.dart';
|
||||
import '../../core/theme/skin_provider.dart';
|
||||
|
||||
/// Reusable bottom sheet with drag handle, progress dots, title, content,
|
||||
/// and action buttons.
|
||||
///
|
||||
/// Use with [showModalBottomSheet] or as a child of [DraggableScrollableSheet].
|
||||
class BottomSheetScaffold extends StatelessWidget {
|
||||
class BottomSheetScaffold extends ConsumerWidget {
|
||||
const BottomSheetScaffold({
|
||||
super.key,
|
||||
required this.title,
|
||||
@@ -38,7 +38,10 @@ class BottomSheetScaffold extends StatelessWidget {
|
||||
final VoidCallback? onSecondaryPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final skinColors = ref.watch(skinColorsProvider);
|
||||
final skinTypo = ref.watch(skinTypographyProvider);
|
||||
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 12, 24, 24),
|
||||
@@ -50,7 +53,7 @@ class BottomSheetScaffold extends StatelessWidget {
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: KittieColors.creamBorder,
|
||||
color: skinColors.creamBorder,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
@@ -58,20 +61,28 @@ class BottomSheetScaffold extends StatelessWidget {
|
||||
|
||||
// Progress dots
|
||||
if (totalSteps != null && currentStep != null) ...[
|
||||
_ProgressDots(total: totalSteps!, current: currentStep!),
|
||||
_ProgressDots(
|
||||
total: totalSteps!,
|
||||
current: currentStep!,
|
||||
skinColors: skinColors,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
// Title
|
||||
Text(title, style: KittieTypography.h2, textAlign: TextAlign.center),
|
||||
Text(
|
||||
title,
|
||||
style: skinTypo.h2,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
// Subtitle
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: KittieTypography.bodySmall.copyWith(
|
||||
color: KittieColors.brownLight,
|
||||
style: skinTypo.bodySmall.copyWith(
|
||||
color: skinColors.brownLight,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
@@ -110,10 +121,15 @@ class BottomSheetScaffold extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _ProgressDots extends StatelessWidget {
|
||||
const _ProgressDots({required this.total, required this.current});
|
||||
const _ProgressDots({
|
||||
required this.total,
|
||||
required this.current,
|
||||
required this.skinColors,
|
||||
});
|
||||
|
||||
final int total;
|
||||
final int current;
|
||||
final dynamic skinColors;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -128,7 +144,7 @@ class _ProgressDots extends StatelessWidget {
|
||||
margin: EdgeInsets.only(left: i > 0 ? 6 : 0),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: isActive ? KittieColors.amber : KittieColors.creamBorder,
|
||||
color: isActive ? skinColors.amber : skinColors.creamBorder,
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -2,8 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../core/i18n/i18n_provider.dart';
|
||||
import '../../core/theme/kittie_colors.dart';
|
||||
import '../../core/theme/kittie_typography.dart';
|
||||
import '../../core/theme/skin_provider.dart';
|
||||
|
||||
/// Energy level for the badge display.
|
||||
enum EnergyLevel {
|
||||
@@ -48,7 +47,30 @@ class EnergyBadge extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final t = ref.watch(tProvider);
|
||||
final (bgColor, textColor, icon, labelKey) = _props;
|
||||
final skinColors = ref.watch(skinColorsProvider);
|
||||
final skinTypo = ref.watch(skinTypographyProvider);
|
||||
|
||||
final (bgColor, textColor, icon, labelKey) = switch (_effectiveLevel) {
|
||||
EnergyLevel.low => (
|
||||
skinColors.greenLight,
|
||||
skinColors.greenDark,
|
||||
Icons.eco_outlined,
|
||||
'energy.low',
|
||||
),
|
||||
EnergyLevel.medium => (
|
||||
skinColors.orangeLight,
|
||||
skinColors.orange,
|
||||
Icons.wb_sunny_outlined,
|
||||
'energy.medium',
|
||||
),
|
||||
EnergyLevel.high => (
|
||||
skinColors.redLight,
|
||||
skinColors.red,
|
||||
Icons.local_fire_department_outlined,
|
||||
'energy.high',
|
||||
),
|
||||
};
|
||||
|
||||
final label = t(labelKey);
|
||||
|
||||
return Container(
|
||||
@@ -65,32 +87,10 @@ class EnergyBadge extends ConsumerWidget {
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
label,
|
||||
style: KittieTypography.labelSmall.copyWith(color: textColor),
|
||||
style: skinTypo.labelSmall.copyWith(color: textColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
(Color bg, Color text, IconData icon, String labelKey) get _props =>
|
||||
switch (_effectiveLevel) {
|
||||
EnergyLevel.low => (
|
||||
KittieColors.greenLight,
|
||||
KittieColors.greenDark,
|
||||
Icons.eco_outlined,
|
||||
'energy.low',
|
||||
),
|
||||
EnergyLevel.medium => (
|
||||
KittieColors.orangeLight,
|
||||
KittieColors.orange,
|
||||
Icons.wb_sunny_outlined,
|
||||
'energy.medium',
|
||||
),
|
||||
EnergyLevel.high => (
|
||||
KittieColors.redLight,
|
||||
KittieColors.red,
|
||||
Icons.local_fire_department_outlined,
|
||||
'energy.high',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../core/constants/app_constants.dart';
|
||||
import '../../core/theme/kittie_colors.dart';
|
||||
import '../../core/theme/kittie_typography.dart';
|
||||
import '../../core/theme/skin_provider.dart';
|
||||
|
||||
/// Horizontal energy bar with "ENERGIE" label.
|
||||
/// Horizontal energy bar with label.
|
||||
///
|
||||
/// Shows a track with creamDark background and amber fill
|
||||
/// that animates to the current [fraction] (0.0–1.0).
|
||||
/// Alternatively, pass [energyLevel] (1–3) to compute the fraction.
|
||||
class EnergyBar extends StatelessWidget {
|
||||
class EnergyBar extends ConsumerWidget {
|
||||
const EnergyBar({
|
||||
super.key,
|
||||
this.fraction,
|
||||
@@ -33,15 +33,18 @@ class EnergyBar extends StatelessWidget {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final skinColors = ref.watch(skinColorsProvider);
|
||||
final skinTypo = ref.watch(skinTypographyProvider);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: KittieTypography.labelSmall.copyWith(
|
||||
color: KittieColors.brownLight,
|
||||
style: skinTypo.labelSmall.copyWith(
|
||||
color: skinColors.brownLight,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
@@ -57,14 +60,14 @@ class EnergyBar extends StatelessWidget {
|
||||
// Track background
|
||||
Container(
|
||||
width: constraints.maxWidth,
|
||||
color: KittieColors.creamDark,
|
||||
color: skinColors.creamDark,
|
||||
),
|
||||
// Amber fill
|
||||
AnimatedContainer(
|
||||
duration: AppConstants.animationNormal,
|
||||
curve: Curves.easeInOut,
|
||||
width: constraints.maxWidth * _effectiveFraction,
|
||||
color: KittieColors.amber,
|
||||
color: skinColors.amber,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../core/constants/app_constants.dart';
|
||||
import '../../core/theme/kittie_colors.dart';
|
||||
import '../../core/theme/kittie_typography.dart';
|
||||
import '../../core/theme/skin_provider.dart';
|
||||
import '../../domain/entities/pet_type.dart';
|
||||
|
||||
/// Pet area card with a speech bubble.
|
||||
///
|
||||
/// Shows the pet emoji on the left and a white speech bubble on the right
|
||||
/// Shows the pet emoji on the left and a speech bubble on the right
|
||||
/// containing the pet name, mood, and a message.
|
||||
class PetBubble extends StatelessWidget {
|
||||
class PetBubble extends ConsumerWidget {
|
||||
const PetBubble({
|
||||
super.key,
|
||||
required this.petType,
|
||||
@@ -24,11 +24,14 @@ class PetBubble extends StatelessWidget {
|
||||
final String message;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final skinColors = ref.watch(skinColorsProvider);
|
||||
final skinTypo = ref.watch(skinTypographyProvider);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: KittieColors.creamDark,
|
||||
color: skinColors.creamDark,
|
||||
borderRadius: BorderRadius.circular(AppConstants.cardBorderRadius),
|
||||
),
|
||||
child: Row(
|
||||
@@ -45,8 +48,9 @@ class PetBubble extends StatelessWidget {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: skinColors.cream,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: skinColors.creamBorder),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -56,13 +60,13 @@ class PetBubble extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
petName,
|
||||
style: KittieTypography.labelLarge,
|
||||
style: skinTypo.labelLarge,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
mood,
|
||||
style: KittieTypography.labelSmall.copyWith(
|
||||
color: KittieColors.brownLight,
|
||||
style: skinTypo.labelSmall.copyWith(
|
||||
color: skinColors.brownLight,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -70,8 +74,8 @@ class PetBubble extends StatelessWidget {
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
message,
|
||||
style: KittieTypography.bodySmall.copyWith(
|
||||
color: KittieColors.brownLight,
|
||||
style: skinTypo.bodySmall.copyWith(
|
||||
color: skinColors.brownLight,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -2,12 +2,12 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../core/i18n/i18n_provider.dart';
|
||||
import '../../core/theme/kittie_colors.dart';
|
||||
import '../../core/theme/kittie_typography.dart';
|
||||
import '../../core/theme/skin_provider.dart';
|
||||
import '../../features/tasks/providers/pet_providers.dart';
|
||||
import '../widgets/language_picker.dart';
|
||||
import '../widgets/skin_picker.dart';
|
||||
|
||||
/// User profile screen — name, pet info, stats, export.
|
||||
/// User profile screen — name, pet info, stats, language, skin.
|
||||
class ProfileScreen extends ConsumerWidget {
|
||||
const ProfileScreen({super.key});
|
||||
|
||||
@@ -17,9 +17,11 @@ class ProfileScreen extends ConsumerWidget {
|
||||
final petState = ref.watch(petStateProvider);
|
||||
final level = ref.watch(petLevelProvider);
|
||||
final t = ref.watch(tProvider);
|
||||
final skinColors = ref.watch(skinColorsProvider);
|
||||
final skinTypo = ref.watch(skinTypographyProvider);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: KittieColors.cream,
|
||||
backgroundColor: skinColors.cream,
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
@@ -29,7 +31,7 @@ class ProfileScreen extends ConsumerWidget {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(t('profile.title'), style: KittieTypography.h2),
|
||||
Text(t('profile.title'), style: skinTypo.h2),
|
||||
const LanguagePicker(),
|
||||
],
|
||||
),
|
||||
@@ -45,7 +47,11 @@ class ProfileScreen extends ConsumerWidget {
|
||||
initialValue: user.name,
|
||||
onSave: (v) => ref.read(userProvider.notifier).updateName(v),
|
||||
t: t,
|
||||
skinColors: skinColors,
|
||||
skinTypo: skinTypo,
|
||||
),
|
||||
skinColors: skinColors,
|
||||
skinTypo: skinTypo,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
@@ -60,7 +66,11 @@ class ProfileScreen extends ConsumerWidget {
|
||||
onSave: (v) =>
|
||||
ref.read(userProvider.notifier).updatePetName(v),
|
||||
t: t,
|
||||
skinColors: skinColors,
|
||||
skinTypo: skinTypo,
|
||||
),
|
||||
skinColors: skinColors,
|
||||
skinTypo: skinTypo,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
@@ -68,19 +78,21 @@ class ProfileScreen extends ConsumerWidget {
|
||||
_InfoRow(
|
||||
label: t('profile.pet_type_label'),
|
||||
value: user.petType.label,
|
||||
skinColors: skinColors,
|
||||
skinTypo: skinTypo,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Stats summary
|
||||
Text(t('profile.stats_title'), style: KittieTypography.h3),
|
||||
Text(t('profile.stats_title'), style: skinTypo.h3),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: KittieColors.creamDark,
|
||||
color: skinColors.creamDark,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: KittieColors.creamBorder),
|
||||
border: Border.all(color: skinColors.creamBorder),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -89,27 +101,36 @@ class ProfileScreen extends ConsumerWidget {
|
||||
label: t('pet.stat_streak'),
|
||||
value: t('pet.stat_streak_value',
|
||||
params: {'days': '${petState.streakDays}'}),
|
||||
color: KittieColors.orange,
|
||||
color: skinColors.orange,
|
||||
skinTypo: skinTypo,
|
||||
),
|
||||
const Divider(color: KittieColors.creamBorder, height: 24),
|
||||
Divider(color: skinColors.creamBorder, height: 24),
|
||||
_StatRow(
|
||||
icon: Icons.check_circle_rounded,
|
||||
label: t('pet.stat_tasks_done'),
|
||||
value: '${petState.totalTasksDone}',
|
||||
color: KittieColors.sageGreen,
|
||||
color: skinColors.sageGreen,
|
||||
skinTypo: skinTypo,
|
||||
),
|
||||
const Divider(color: KittieColors.creamBorder, height: 24),
|
||||
Divider(color: skinColors.creamBorder, height: 24),
|
||||
_StatRow(
|
||||
icon: Icons.star_rounded,
|
||||
label: t('pet.stat_level'),
|
||||
value: '$level',
|
||||
color: KittieColors.amber,
|
||||
color: skinColors.amber,
|
||||
skinTypo: skinTypo,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Skin picker section
|
||||
Text(t('skin.title'), style: skinTypo.h3),
|
||||
const SizedBox(height: 12),
|
||||
const SkinPicker(),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Export button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
@@ -117,15 +138,18 @@ class ProfileScreen extends ConsumerWidget {
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(t('profile.export_preparing'))),
|
||||
content: Text(t('profile.export_preparing')),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.download_rounded),
|
||||
label: Text(t('profile.export_data'),
|
||||
style: KittieTypography.labelLarge),
|
||||
label: Text(
|
||||
t('profile.export_data'),
|
||||
style: skinTypo.labelLarge,
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: KittieColors.brown,
|
||||
side: const BorderSide(color: KittieColors.creamBorder),
|
||||
foregroundColor: skinColors.brown,
|
||||
side: BorderSide(color: skinColors.creamBorder),
|
||||
minimumSize: const Size(0, 48),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
@@ -139,8 +163,8 @@ class ProfileScreen extends ConsumerWidget {
|
||||
Center(
|
||||
child: Text(
|
||||
t('profile.version'),
|
||||
style: KittieTypography.bodySmall.copyWith(
|
||||
color: KittieColors.brownLight,
|
||||
style: skinTypo.bodySmall.copyWith(
|
||||
color: skinColors.brownLight,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -158,17 +182,19 @@ class ProfileScreen extends ConsumerWidget {
|
||||
required String initialValue,
|
||||
required ValueChanged<String> onSave,
|
||||
required String Function(String, {Map<String, String>? params}) t,
|
||||
required dynamic skinColors,
|
||||
required dynamic skinTypo,
|
||||
}) {
|
||||
final controller = TextEditingController(text: initialValue);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
backgroundColor: KittieColors.cream,
|
||||
title: Text(title, style: KittieTypography.h3),
|
||||
backgroundColor: skinColors.cream,
|
||||
title: Text(title, style: skinTypo.h3),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
style: KittieTypography.bodyLarge,
|
||||
style: skinTypo.bodyLarge,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
@@ -195,11 +221,15 @@ class _EditableRow extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
final VoidCallback onEdit;
|
||||
final dynamic skinColors;
|
||||
final dynamic skinTypo;
|
||||
|
||||
const _EditableRow({
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.onEdit,
|
||||
required this.skinColors,
|
||||
required this.skinTypo,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -210,17 +240,20 @@ class _EditableRow extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label,
|
||||
style: KittieTypography.labelMedium
|
||||
.copyWith(color: KittieColors.brownLight)),
|
||||
Text(
|
||||
label,
|
||||
style: skinTypo.labelMedium.copyWith(
|
||||
color: skinColors.brownLight,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(value, style: KittieTypography.bodyLarge),
|
||||
Text(value, style: skinTypo.bodyLarge),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit_rounded, size: 20),
|
||||
color: KittieColors.brownLight,
|
||||
color: skinColors.brownLight,
|
||||
onPressed: onEdit,
|
||||
),
|
||||
],
|
||||
@@ -231,19 +264,28 @@ class _EditableRow extends StatelessWidget {
|
||||
class _InfoRow extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
final dynamic skinColors;
|
||||
final dynamic skinTypo;
|
||||
|
||||
const _InfoRow({required this.label, required this.value});
|
||||
const _InfoRow({
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.skinColors,
|
||||
required this.skinTypo,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label,
|
||||
style: KittieTypography.labelMedium
|
||||
.copyWith(color: KittieColors.brownLight)),
|
||||
Text(
|
||||
label,
|
||||
style:
|
||||
skinTypo.labelMedium.copyWith(color: skinColors.brownLight),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(value, style: KittieTypography.bodyLarge),
|
||||
Text(value, style: skinTypo.bodyLarge),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -254,12 +296,14 @@ class _StatRow extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
final Color color;
|
||||
final dynamic skinTypo;
|
||||
|
||||
const _StatRow({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.color,
|
||||
required this.skinTypo,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -268,9 +312,8 @@ class _StatRow extends StatelessWidget {
|
||||
children: [
|
||||
Icon(icon, color: color, size: 20),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: Text(label, style: KittieTypography.bodyMedium)),
|
||||
Text(value,
|
||||
style: KittieTypography.labelLarge.copyWith(color: color)),
|
||||
Expanded(child: Text(label, style: skinTypo.bodyMedium)),
|
||||
Text(value, style: skinTypo.labelLarge.copyWith(color: color)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../core/i18n/i18n_provider.dart';
|
||||
import '../../core/theme/skin_provider.dart';
|
||||
import '../../core/theme/skin_type.dart';
|
||||
|
||||
/// Horizontal wrap of 7 skin tiles for selecting the active app theme.
|
||||
///
|
||||
/// Tapping a tile calls [SkinNotifier.setSkin] and persists the choice.
|
||||
class SkinPicker extends ConsumerWidget {
|
||||
const SkinPicker({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final currentSkin = ref.watch(skinProvider);
|
||||
final skinColors = ref.watch(skinColorsProvider);
|
||||
final skinTypo = ref.watch(skinTypographyProvider);
|
||||
final t = ref.watch(tProvider);
|
||||
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: SkinType.values.map((skin) {
|
||||
final isSelected = skin == currentSkin;
|
||||
final colors = skin.colors;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => ref.read(skinProvider.notifier).setSkin(skin),
|
||||
child: SizedBox(
|
||||
width: 90,
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.cream,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? skinColors.brown
|
||||
: Colors.grey.withValues(alpha: 0.3),
|
||||
width: isSelected ? 2 : 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Color dots preview
|
||||
Row(
|
||||
children: [
|
||||
_ColorDot(color: colors.brown),
|
||||
const SizedBox(width: 4),
|
||||
_ColorDot(color: colors.sageGreen),
|
||||
const SizedBox(width: 4),
|
||||
_ColorDot(color: colors.energyHigh),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
|
||||
// Skin name
|
||||
Text(
|
||||
t(skin.nameKey),
|
||||
style: skinTypo.labelSmall.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: skinColors.brownDark,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
|
||||
// Skin description
|
||||
Text(
|
||||
t(skin.descriptionKey),
|
||||
style: skinTypo.labelSmall.copyWith(
|
||||
fontSize: 9,
|
||||
color: Colors.grey,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Selected checkmark overlay
|
||||
if (isSelected)
|
||||
Positioned(
|
||||
top: 4,
|
||||
right: 4,
|
||||
child: Icon(
|
||||
Icons.check_circle,
|
||||
size: 16,
|
||||
color: skinColors.brown,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ColorDot extends StatelessWidget {
|
||||
const _ColorDot({required this.color});
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: color,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../core/i18n/i18n_provider.dart';
|
||||
import '../../core/theme/kittie_colors.dart';
|
||||
import '../../core/theme/kittie_typography.dart';
|
||||
import '../../core/theme/skin_provider.dart';
|
||||
|
||||
/// Day status for a single streak dot.
|
||||
enum DayStatus { done, today, future }
|
||||
@@ -11,9 +10,9 @@ enum DayStatus { done, today, future }
|
||||
/// A row of 7 day-dots (Mon–Sun) showing weekly task completion streak.
|
||||
///
|
||||
/// Colors:
|
||||
/// - done: green (#7BAE82)
|
||||
/// - today: amber (#C4A882)
|
||||
/// - future: grey (#E0DBD0)
|
||||
/// - done: sageGreen
|
||||
/// - today: amber
|
||||
/// - future: creamBorder
|
||||
///
|
||||
/// Shows fire emoji + streak count above the dots.
|
||||
class StreakDots extends ConsumerWidget {
|
||||
@@ -65,6 +64,8 @@ class StreakDots extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final t = ref.watch(tProvider);
|
||||
final skinColors = ref.watch(skinColorsProvider);
|
||||
final skinTypo = ref.watch(skinTypographyProvider);
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -77,8 +78,8 @@ class StreakDots extends ConsumerWidget {
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'$_effectiveCount',
|
||||
style: KittieTypography.labelLarge.copyWith(
|
||||
color: KittieColors.brown,
|
||||
style: skinTypo.labelLarge.copyWith(
|
||||
color: skinColors.brown,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -88,11 +89,31 @@ class StreakDots extends ConsumerWidget {
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: List.generate(7, (i) {
|
||||
final color = switch (_effectiveDays[i]) {
|
||||
DayStatus.done => skinColors.sageGreen,
|
||||
DayStatus.today => skinColors.amber,
|
||||
DayStatus.future => skinColors.creamBorder,
|
||||
};
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(left: i > 0 ? 8 : 0),
|
||||
child: _DayDot(
|
||||
label: t(_dayKeys[i]),
|
||||
status: _effectiveDays[i],
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration:
|
||||
BoxDecoration(shape: BoxShape.circle, color: color),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
t(_dayKeys[i]),
|
||||
style: skinTypo.labelSmall.copyWith(
|
||||
color: skinColors.brownLight,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
@@ -101,38 +122,3 @@ class StreakDots extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DayDot extends StatelessWidget {
|
||||
const _DayDot({required this.label, required this.status});
|
||||
|
||||
final String label;
|
||||
final DayStatus status;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = switch (status) {
|
||||
DayStatus.done => KittieColors.sageGreen,
|
||||
DayStatus.today => KittieColors.amber,
|
||||
DayStatus.future => KittieColors.creamBorder,
|
||||
};
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
label,
|
||||
style: KittieTypography.labelSmall.copyWith(
|
||||
color: KittieColors.brownLight,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../core/constants/app_constants.dart';
|
||||
import '../../core/i18n/i18n_provider.dart';
|
||||
import '../../core/theme/kittie_colors.dart';
|
||||
import '../../core/theme/kittie_typography.dart';
|
||||
import '../../core/theme/skin_provider.dart';
|
||||
import '../../domain/entities/task_entity.dart';
|
||||
import 'energy_badge.dart';
|
||||
|
||||
@@ -18,7 +17,7 @@ import 'energy_badge.dart';
|
||||
/// - Purple "Self-care" tag when [isSelfCare] is true
|
||||
/// - Red tint overlay when [isOverdue] is true
|
||||
/// - Minimum height of 56px (Apple HIG touch target)
|
||||
class TaskCard extends StatelessWidget {
|
||||
class TaskCard extends ConsumerWidget {
|
||||
const TaskCard({
|
||||
super.key,
|
||||
this.task,
|
||||
@@ -53,32 +52,43 @@ class TaskCard extends StatelessWidget {
|
||||
if (task?.deadline != null) return task!.deadline!.isBefore(DateTime.now());
|
||||
return isOverdue;
|
||||
}
|
||||
|
||||
VoidCallback? get _toggleCallback => onComplete ?? onToggle;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final skinColors = ref.watch(skinColorsProvider);
|
||||
final skinTypo = ref.watch(skinTypographyProvider);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(minHeight: AppConstants.minTouchTarget),
|
||||
constraints:
|
||||
const BoxConstraints(minHeight: AppConstants.minTouchTarget),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: _isOverdue
|
||||
? KittieColors.redLight
|
||||
: KittieColors.creamDark,
|
||||
color: _isOverdue ? skinColors.redLight : skinColors.creamDark,
|
||||
borderRadius: BorderRadius.circular(AppConstants.cardBorderRadius),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_Checkbox(isDone: _isDone, onToggle: _toggleCallback),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: _Content(
|
||||
title: _title,
|
||||
energyLevel: _energyLevel,
|
||||
_Checkbox(
|
||||
isDone: _isDone,
|
||||
isSelfCare: _isSelfCare,
|
||||
timeText: timeText,
|
||||
)),
|
||||
onToggle: _toggleCallback,
|
||||
skinColors: skinColors,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _Content(
|
||||
title: _title,
|
||||
energyLevel: _energyLevel,
|
||||
isDone: _isDone,
|
||||
isSelfCare: _isSelfCare,
|
||||
timeText: timeText,
|
||||
skinColors: skinColors,
|
||||
skinTypo: skinTypo,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -87,10 +97,15 @@ class TaskCard extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _Checkbox extends StatelessWidget {
|
||||
const _Checkbox({required this.isDone, this.onToggle});
|
||||
const _Checkbox({
|
||||
required this.isDone,
|
||||
required this.skinColors,
|
||||
this.onToggle,
|
||||
});
|
||||
|
||||
final bool isDone;
|
||||
final VoidCallback? onToggle;
|
||||
final dynamic skinColors;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -105,9 +120,10 @@ class _Checkbox extends StatelessWidget {
|
||||
height: 28,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: isDone ? KittieColors.sageGreen : Colors.transparent,
|
||||
color: isDone ? skinColors.sageGreen : Colors.transparent,
|
||||
border: Border.all(
|
||||
color: isDone ? KittieColors.sageGreen : KittieColors.brownLight,
|
||||
color:
|
||||
isDone ? skinColors.sageGreen : skinColors.brownLight,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
@@ -125,6 +141,8 @@ class _Content extends StatelessWidget {
|
||||
required this.energyLevel,
|
||||
required this.isDone,
|
||||
required this.isSelfCare,
|
||||
required this.skinColors,
|
||||
required this.skinTypo,
|
||||
this.timeText,
|
||||
});
|
||||
|
||||
@@ -133,6 +151,8 @@ class _Content extends StatelessWidget {
|
||||
final bool isDone;
|
||||
final bool isSelfCare;
|
||||
final String? timeText;
|
||||
final dynamic skinColors;
|
||||
final dynamic skinTypo;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -142,11 +162,11 @@ class _Content extends StatelessWidget {
|
||||
children: [
|
||||
AnimatedDefaultTextStyle(
|
||||
duration: AppConstants.animationFast,
|
||||
style: KittieTypography.bodyMedium.copyWith(
|
||||
style: skinTypo.bodyMedium.copyWith(
|
||||
decoration: isDone ? TextDecoration.lineThrough : null,
|
||||
color: isDone
|
||||
? KittieColors.brown.withValues(alpha: 0.4)
|
||||
: KittieColors.brown,
|
||||
? skinColors.brown.withValues(alpha: 0.4)
|
||||
: skinColors.brown,
|
||||
),
|
||||
child: Text(title, maxLines: 2, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
@@ -162,8 +182,8 @@ class _Content extends StatelessWidget {
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
timeText!,
|
||||
style: KittieTypography.labelSmall.copyWith(
|
||||
color: KittieColors.brownLight,
|
||||
style: skinTypo.labelSmall.copyWith(
|
||||
color: skinColors.brownLight,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -180,19 +200,21 @@ class _SelfCareTag extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final t = ref.watch(tProvider);
|
||||
final skinColors = ref.watch(skinColorsProvider);
|
||||
final skinTypo = ref.watch(skinTypographyProvider);
|
||||
|
||||
return Container(
|
||||
height: 24,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: KittieColors.purpleLight,
|
||||
color: skinColors.purpleLight,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
t('self_care.tag'),
|
||||
style: KittieTypography.labelSmall.copyWith(
|
||||
color: KittieColors.purple,
|
||||
style: skinTypo.labelSmall.copyWith(
|
||||
color: skinColors.purple,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -281,7 +281,7 @@ void main() {
|
||||
for (final skin in AppSkin.values) {
|
||||
final theme = skin.buildTheme();
|
||||
final bgColor = theme.elevatedButtonTheme.style!.backgroundColor
|
||||
?.resolve({}) as Color?;
|
||||
?.resolve({});
|
||||
expect(bgColor, theme.colorScheme.primary,
|
||||
reason: '${skin.key} button bg should match primary');
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:kittie_mobile/core/theme/app_skin.dart';
|
||||
import 'package:kittie_mobile/core/theme/skin_provider.dart';
|
||||
import 'package:kittie_mobile/core/theme/skin_type.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
setInitialSkin(SkinType.defaultSkin);
|
||||
});
|
||||
|
||||
group('SkinNotifier — initial state', () {
|
||||
@@ -14,7 +15,15 @@ void main() {
|
||||
final container = ProviderContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
expect(container.read(skinProvider), AppSkin.defaultSkin);
|
||||
expect(container.read(skinProvider), SkinType.defaultSkin);
|
||||
});
|
||||
|
||||
test('uses initialSkin set via setInitialSkin', () {
|
||||
setInitialSkin(SkinType.epic);
|
||||
final container = ProviderContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
expect(container.read(skinProvider), SkinType.epic);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,18 +32,18 @@ void main() {
|
||||
final container = ProviderContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container.read(skinProvider.notifier).setSkin(AppSkin.epic);
|
||||
expect(container.read(skinProvider), AppSkin.epic);
|
||||
await container.read(skinProvider.notifier).setSkin(SkinType.epic);
|
||||
expect(container.read(skinProvider), SkinType.epic);
|
||||
});
|
||||
|
||||
test('can switch through all 7 skins', () async {
|
||||
final container = ProviderContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
for (final skin in AppSkin.values) {
|
||||
for (final skin in SkinType.values) {
|
||||
await container.read(skinProvider.notifier).setSkin(skin);
|
||||
expect(container.read(skinProvider), skin,
|
||||
reason: 'should have switched to ${skin.key}');
|
||||
reason: 'should have switched to ${skin.storageKey}');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -42,14 +51,14 @@ void main() {
|
||||
final container = ProviderContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container.read(skinProvider.notifier).setSkin(AppSkin.senior);
|
||||
await container.read(skinProvider.notifier).setSkin(SkinType.senior);
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
expect(prefs.getString('app_skin'), 'senior');
|
||||
});
|
||||
|
||||
test('persists correct key for each skin', () async {
|
||||
for (final skin in AppSkin.values) {
|
||||
for (final skin in SkinType.values) {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final container = ProviderContainer();
|
||||
addTearDown(container.dispose);
|
||||
@@ -59,72 +68,67 @@ void main() {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
expect(
|
||||
prefs.getString('app_skin'),
|
||||
skin.key,
|
||||
reason: '${skin.key} should be persisted',
|
||||
skin.storageKey,
|
||||
reason: '${skin.storageKey} should be persisted',
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group('SkinNotifier.loadSaved', () {
|
||||
test('restores persisted skin from SharedPreferences', () async {
|
||||
SharedPreferences.setMockInitialValues({'app_skin': 'future'});
|
||||
group('setInitialSkin — pre-runApp skin loading', () {
|
||||
test('restores persisted skin via setInitialSkin', () {
|
||||
setInitialSkin(SkinTypeExtension.fromStorageKey('future'));
|
||||
final container = ProviderContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container.read(skinProvider.notifier).loadSaved();
|
||||
|
||||
expect(container.read(skinProvider), AppSkin.future);
|
||||
expect(container.read(skinProvider), SkinType.future);
|
||||
});
|
||||
|
||||
test('stays at defaultSkin when no value is persisted', () async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
test('stays at defaultSkin when no value is persisted', () {
|
||||
setInitialSkin(SkinTypeExtension.fromStorageKey(''));
|
||||
final container = ProviderContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container.read(skinProvider.notifier).loadSaved();
|
||||
|
||||
expect(container.read(skinProvider), AppSkin.defaultSkin);
|
||||
expect(container.read(skinProvider), SkinType.defaultSkin);
|
||||
});
|
||||
|
||||
test('falls back to defaultSkin for unknown persisted key', () async {
|
||||
SharedPreferences.setMockInitialValues({'app_skin': 'nonexistent'});
|
||||
test('falls back to defaultSkin for unknown persisted key', () {
|
||||
setInitialSkin(SkinTypeExtension.fromStorageKey('nonexistent'));
|
||||
final container = ProviderContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container.read(skinProvider.notifier).loadSaved();
|
||||
|
||||
expect(container.read(skinProvider), AppSkin.defaultSkin);
|
||||
expect(container.read(skinProvider), SkinType.defaultSkin);
|
||||
});
|
||||
|
||||
test('can restore all 7 skins', () async {
|
||||
for (final skin in AppSkin.values) {
|
||||
SharedPreferences.setMockInitialValues({'app_skin': skin.key});
|
||||
test('can restore all 7 skins', () {
|
||||
for (final skin in SkinType.values) {
|
||||
setInitialSkin(SkinTypeExtension.fromStorageKey(skin.storageKey));
|
||||
final container = ProviderContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container.read(skinProvider.notifier).loadSaved();
|
||||
|
||||
expect(
|
||||
container.read(skinProvider),
|
||||
skin,
|
||||
reason: '${skin.key} should be restored',
|
||||
reason: '${skin.storageKey} should be restored',
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group('SkinNotifier — round-trip persistence', () {
|
||||
test('setSkin then loadSaved restores same skin', () async {
|
||||
test('setSkin then reading from prefs restores same skin', () async {
|
||||
final container1 = ProviderContainer();
|
||||
await container1.read(skinProvider.notifier).setSkin(AppSkin.kids);
|
||||
await container1.read(skinProvider.notifier).setSkin(SkinType.kids);
|
||||
container1.dispose();
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final key = prefs.getString('app_skin')!;
|
||||
setInitialSkin(SkinTypeExtension.fromStorageKey(key));
|
||||
|
||||
final container2 = ProviderContainer();
|
||||
addTearDown(container2.dispose);
|
||||
await container2.read(skinProvider.notifier).loadSaved();
|
||||
|
||||
expect(container2.read(skinProvider), AppSkin.kids);
|
||||
expect(container2.read(skinProvider), SkinType.kids);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user