Backend: - Rename recognition_jobs → dish_recognition_jobs; add target_date and target_meal_type columns to capture scan context at submission time - Add job_id FK on meal_diary so entries are linked to their origin job - New GET /ai/jobs endpoint returns today's unlinked jobs for the current user - diary.Entry and CreateRequest gain job_id field; repository reads/writes it - CORS middleware: allow Accept-Language and Cache-Control headers - Logging middleware: implement http.Flusher on responseWriter (needed for SSE) - Consolidate migrations into a single 001_initial_schema.sql Flutter: - POST /ai/recognize-dish now sends target_date and target_meal_type - DishResultSheet accepts jobId; _addToDiary includes it in the diary payload, saves last-used meal type to SharedPreferences, invalidates todayJobsProvider - TodayJobsNotifier + todayJobsProvider: loads unlinked jobs via GET /ai/jobs - Home screen shows _TodayJobsWidget (up to 3 tiles) between macros and meals; tapping a done tile reopens DishResultSheet with the stored result - Quick Actions row: third button "История" → /scan/history - New RecognitionHistoryScreen: full-screen list of today's unlinked jobs - LocalPreferences wrapper over SharedPreferences (last_used_meal_type) - app_theme: apply Google Fonts Roboto as default font family Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
259 lines
8.6 KiB
Dart
259 lines
8.6 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
import 'app_colors.dart';
|
|
|
|
ThemeData appTheme() {
|
|
final base = ColorScheme.fromSeed(
|
|
seedColor: AppColors.primary,
|
|
brightness: Brightness.light,
|
|
surface: AppColors.surface,
|
|
onSurface: AppColors.textPrimary,
|
|
).copyWith(
|
|
primary: AppColors.primary,
|
|
onPrimary: Colors.white,
|
|
secondary: AppColors.primary,
|
|
onSecondary: Colors.white,
|
|
tertiary: AppColors.primary,
|
|
error: AppColors.error,
|
|
surfaceContainerHighest: AppColors.surfaceTertiary,
|
|
onSurfaceVariant: AppColors.textSecondary,
|
|
outline: AppColors.separator,
|
|
);
|
|
|
|
return ThemeData(
|
|
useMaterial3: true,
|
|
colorScheme: base,
|
|
fontFamily: GoogleFonts.roboto().fontFamily,
|
|
scaffoldBackgroundColor: AppColors.background,
|
|
|
|
// ── AppBar ────────────────────────────────────────
|
|
appBarTheme: const AppBarTheme(
|
|
centerTitle: true,
|
|
elevation: 0,
|
|
scrolledUnderElevation: 0,
|
|
backgroundColor: AppColors.background,
|
|
foregroundColor: AppColors.textPrimary,
|
|
systemOverlayStyle: SystemUiOverlayStyle(
|
|
statusBarBrightness: Brightness.light,
|
|
statusBarIconBrightness: Brightness.dark,
|
|
),
|
|
titleTextStyle: TextStyle(
|
|
color: AppColors.textPrimary,
|
|
fontSize: 17,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: -0.4,
|
|
),
|
|
),
|
|
|
|
// ── Card ──────────────────────────────────────────
|
|
// iOS: white cards on grey background, no shadow
|
|
cardTheme: CardThemeData(
|
|
elevation: 0,
|
|
color: AppColors.surface,
|
|
surfaceTintColor: Colors.transparent,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
|
|
// ── Bottom navigation ─────────────────────────────
|
|
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
|
|
backgroundColor: AppColors.surface,
|
|
selectedItemColor: AppColors.primary,
|
|
unselectedItemColor: AppColors.textSecondary,
|
|
type: BottomNavigationBarType.fixed,
|
|
elevation: 0,
|
|
selectedLabelStyle: TextStyle(
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.w500,
|
|
letterSpacing: -0.2,
|
|
),
|
|
unselectedLabelStyle: TextStyle(fontSize: 10),
|
|
),
|
|
|
|
// ── Buttons ───────────────────────────────────────
|
|
filledButtonTheme: FilledButtonThemeData(
|
|
style: FilledButton.styleFrom(
|
|
minimumSize: const Size(double.infinity, 50),
|
|
backgroundColor: AppColors.primary,
|
|
foregroundColor: Colors.white,
|
|
elevation: 0,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
textStyle: const TextStyle(
|
|
fontSize: 17,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: -0.3,
|
|
),
|
|
),
|
|
),
|
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
|
style: ElevatedButton.styleFrom(
|
|
minimumSize: const Size(double.infinity, 50),
|
|
elevation: 0,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
),
|
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
|
style: OutlinedButton.styleFrom(
|
|
minimumSize: const Size(double.infinity, 50),
|
|
side: const BorderSide(color: AppColors.primary),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
textStyle: const TextStyle(
|
|
fontSize: 17,
|
|
fontWeight: FontWeight.w400,
|
|
letterSpacing: -0.3,
|
|
),
|
|
),
|
|
),
|
|
|
|
// ── Inputs ────────────────────────────────────────
|
|
inputDecorationTheme: InputDecorationTheme(
|
|
filled: true,
|
|
fillColor: AppColors.surface,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(10),
|
|
borderSide: const BorderSide(color: AppColors.separator),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(10),
|
|
borderSide: const BorderSide(color: AppColors.separator),
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(10),
|
|
borderSide: const BorderSide(color: AppColors.primary, width: 1.5),
|
|
),
|
|
errorBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(10),
|
|
borderSide: const BorderSide(color: AppColors.error),
|
|
),
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
|
hintStyle: const TextStyle(color: AppColors.textSecondary),
|
|
),
|
|
|
|
// ── ListTile ──────────────────────────────────────
|
|
listTileTheme: const ListTileThemeData(
|
|
contentPadding: EdgeInsets.symmetric(horizontal: 16),
|
|
minVerticalPadding: 12,
|
|
),
|
|
|
|
// ── Divider ───────────────────────────────────────
|
|
dividerTheme: const DividerThemeData(
|
|
color: AppColors.separator,
|
|
space: 1,
|
|
thickness: 0.5,
|
|
indent: 16,
|
|
),
|
|
|
|
// ── Chip ──────────────────────────────────────────
|
|
chipTheme: ChipThemeData(
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
side: const BorderSide(color: AppColors.separator),
|
|
),
|
|
|
|
// ── Dialog ───────────────────────────────────────
|
|
dialogTheme: DialogThemeData(
|
|
backgroundColor: AppColors.surface,
|
|
surfaceTintColor: Colors.transparent,
|
|
elevation: 0,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(14),
|
|
),
|
|
),
|
|
|
|
// ── Bottom sheet ─────────────────────────────────
|
|
bottomSheetTheme: const BottomSheetThemeData(
|
|
backgroundColor: AppColors.surface,
|
|
surfaceTintColor: Colors.transparent,
|
|
elevation: 0,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
|
|
),
|
|
),
|
|
|
|
// ── Text ─────────────────────────────────────────
|
|
textTheme: const TextTheme(
|
|
headlineLarge: TextStyle(
|
|
fontSize: 34,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: -0.5,
|
|
color: AppColors.textPrimary,
|
|
),
|
|
headlineMedium: TextStyle(
|
|
fontSize: 28,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: -0.4,
|
|
color: AppColors.textPrimary,
|
|
),
|
|
headlineSmall: TextStyle(
|
|
fontSize: 22,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: -0.3,
|
|
color: AppColors.textPrimary,
|
|
),
|
|
titleLarge: TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: -0.3,
|
|
color: AppColors.textPrimary,
|
|
),
|
|
titleMedium: TextStyle(
|
|
fontSize: 17,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: -0.3,
|
|
color: AppColors.textPrimary,
|
|
),
|
|
titleSmall: TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: -0.2,
|
|
color: AppColors.textPrimary,
|
|
),
|
|
bodyLarge: TextStyle(
|
|
fontSize: 17,
|
|
fontWeight: FontWeight.w400,
|
|
letterSpacing: -0.3,
|
|
color: AppColors.textPrimary,
|
|
),
|
|
bodyMedium: TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w400,
|
|
letterSpacing: -0.2,
|
|
color: AppColors.textPrimary,
|
|
),
|
|
bodySmall: TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w400,
|
|
letterSpacing: -0.1,
|
|
color: AppColors.textSecondary,
|
|
),
|
|
labelLarge: TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w500,
|
|
letterSpacing: -0.2,
|
|
color: AppColors.textPrimary,
|
|
),
|
|
labelMedium: TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w500,
|
|
letterSpacing: -0.1,
|
|
color: AppColors.textPrimary,
|
|
),
|
|
labelSmall: TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w400,
|
|
letterSpacing: 0,
|
|
color: AppColors.textSecondary,
|
|
),
|
|
),
|
|
);
|
|
}
|