397 lines
16 KiB
Dart
397 lines
16 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
import '../controllers/workout_controller.dart';
|
|
import '../models/workout.dart';
|
|
import '../screens/history_screen.dart';
|
|
import '../services/sync_service.dart';
|
|
import '../services/auth_service.dart';
|
|
import 'training_screens.dart';
|
|
import 'meal_journal_screen.dart';
|
|
import '../utils/formatters.dart';
|
|
import 'phone_login_screen.dart';
|
|
|
|
const _ink = Color(0xFF17251E);
|
|
const _green = Color(0xFF456C52);
|
|
const _paper = Color(0xFFF5F7F2);
|
|
const _muted = Color(0xFF778078);
|
|
|
|
class HomeScreen extends StatelessWidget {
|
|
const HomeScreen({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final workoutCtrl = Get.find<WorkoutController>();
|
|
final syncService = Get.find<SyncService>();
|
|
|
|
return Scaffold(
|
|
backgroundColor: _paper,
|
|
body: SafeArea(
|
|
child: CustomScrollView(
|
|
slivers: [
|
|
SliverPadding(
|
|
padding: const EdgeInsets.fromLTRB(22, 14, 22, 28),
|
|
sliver: SliverList.list(children: [
|
|
Row(children: [
|
|
Container(
|
|
width: 38,
|
|
height: 38,
|
|
decoration: BoxDecoration(
|
|
color: _green,
|
|
borderRadius: BorderRadius.circular(13),
|
|
),
|
|
child: const Center(
|
|
child: Text('S',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 23,
|
|
fontFamily: 'Georgia',
|
|
fontStyle: FontStyle.italic)),
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
const Text('SportPath',
|
|
style: TextStyle(
|
|
color: _ink,
|
|
fontSize: 17,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: -.4)),
|
|
const Spacer(),
|
|
Obx(() => Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 10, vertical: 7),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(20)),
|
|
child: Row(children: [
|
|
Icon(
|
|
syncService.isOnline
|
|
? Icons.cloud_done_outlined
|
|
: Icons.cloud_off_outlined,
|
|
size: 16,
|
|
color: syncService.isOnline
|
|
? _green
|
|
: Colors.orange.shade800),
|
|
const SizedBox(width: 5),
|
|
Text(syncService.isOnline ? 'متصل' : 'دون اتصال',
|
|
style:
|
|
const TextStyle(color: _ink, fontSize: 11)),
|
|
]),
|
|
)),
|
|
PopupMenuButton<String>(
|
|
tooltip: 'الحساب',
|
|
icon: const Icon(Icons.account_circle_outlined,
|
|
color: _green, size: 25),
|
|
onSelected: (value) async {
|
|
if (value == 'logout') {
|
|
await Get.find<AuthService>().logout();
|
|
Get.offAll(() => const PhoneLoginScreen());
|
|
}
|
|
},
|
|
itemBuilder: (context) => const [
|
|
PopupMenuItem(
|
|
value: 'logout',
|
|
child: Text('تسجيل الخروج'),
|
|
),
|
|
],
|
|
),
|
|
]),
|
|
const SizedBox(height: 34),
|
|
Text(_arabicToday(),
|
|
style: const TextStyle(
|
|
color: _green,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: .2)),
|
|
const SizedBox(height: 8),
|
|
const Text('خطوة صغيرة،\nفرق كبير.',
|
|
style: TextStyle(
|
|
color: _ink,
|
|
fontSize: 35,
|
|
height: 1.18,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: -1.2)),
|
|
const SizedBox(height: 12),
|
|
const Text('خلّينا نبدأ حركة اليوم بإيقاع يناسبك.',
|
|
style:
|
|
TextStyle(color: _muted, fontSize: 14, height: 1.65)),
|
|
const SizedBox(height: 22),
|
|
const TrainingPlanSpotlight(),
|
|
const SizedBox(height: 26),
|
|
Row(children: [
|
|
const Text('ابدأ الآن',
|
|
style: TextStyle(
|
|
color: _ink,
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w600)),
|
|
const Spacer(),
|
|
TextButton.icon(
|
|
onPressed: () => Get.to(() => const HistoryScreen()),
|
|
icon: const Icon(Icons.arrow_back, size: 15),
|
|
label: const Text('سجل نشاطك'),
|
|
style: TextButton.styleFrom(
|
|
foregroundColor: _green,
|
|
textStyle: const TextStyle(fontSize: 12)),
|
|
),
|
|
]),
|
|
const SizedBox(height: 11),
|
|
Row(children: [
|
|
Expanded(
|
|
child: _ActionCard(
|
|
title: 'مشي بالخارج',
|
|
subtitle: 'سجّل المسافة والوقت',
|
|
icon: Icons.directions_walk_rounded,
|
|
tint: const Color(0xFFE8EFE5),
|
|
onTap: () =>
|
|
workoutCtrl.startWorkout(WorkoutType.walking))),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: _ActionCard(
|
|
title: 'جري خفيف',
|
|
subtitle: 'على قدر طاقتك',
|
|
icon: Icons.directions_run_rounded,
|
|
tint: const Color(0xFFF0EAE0),
|
|
onTap: () =>
|
|
workoutCtrl.startWorkout(WorkoutType.running))),
|
|
]),
|
|
Obx(() => workoutCtrl.currentWorkout.value != null
|
|
? Padding(
|
|
padding: const EdgeInsets.only(top: 16),
|
|
child: _ActiveWorkoutCard(controller: workoutCtrl))
|
|
: const SizedBox.shrink()),
|
|
const SizedBox(height: 28),
|
|
const Text('عادات تدعم يومك',
|
|
style: TextStyle(
|
|
color: _ink,
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w600)),
|
|
const SizedBox(height: 12),
|
|
const _HabitCard(
|
|
icon: Icons.restaurant_outlined,
|
|
title: 'وجبتك القادمة',
|
|
detail: 'سجّلها يدويًا أو صحّح تقديرك.',
|
|
tag: 'تغذية',
|
|
onTap: _openMealJournal),
|
|
const SizedBox(height: 9),
|
|
const _HabitCard(
|
|
icon: Icons.water_drop_outlined,
|
|
title: 'اشرب كوب ماء',
|
|
detail: 'تذكير بسيط للعناية بنفسك.',
|
|
tag: 'عافية'),
|
|
const SizedBox(height: 25),
|
|
Row(children: [
|
|
const Text('برامج التمرين',
|
|
style: TextStyle(
|
|
color: _ink,
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w600)),
|
|
const Spacer(),
|
|
TextButton(
|
|
onPressed: () =>
|
|
Get.to(() => const TrainingLibraryScreen()),
|
|
child: const Text('عرض المكتبة'),
|
|
),
|
|
]),
|
|
const SizedBox(height: 6),
|
|
const Text(
|
|
'الخطط والجلسات تُحمّل من الخادم وتُحفظ للعرض دون اتصال.',
|
|
style: TextStyle(color: _muted, fontSize: 11, height: 1.5)),
|
|
const SizedBox(height: 20),
|
|
Container(
|
|
padding: const EdgeInsets.all(18),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFE8EEE5),
|
|
borderRadius: BorderRadius.circular(18)),
|
|
child: const Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Icon(Icons.favorite_border, color: _green, size: 20),
|
|
SizedBox(width: 11),
|
|
Expanded(
|
|
child: Text(
|
|
'ابدأ بهدوء، وتوقف إذا شعرت بألم أو دوخة. الخطة العامة لا تغني عن نصيحة مختص عند وجود حالة صحية.',
|
|
style: TextStyle(
|
|
color: _green, fontSize: 11, height: 1.7))),
|
|
]),
|
|
),
|
|
]),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
String _arabicToday() {
|
|
const weekdays = [
|
|
'الاثنين',
|
|
'الثلاثاء',
|
|
'الأربعاء',
|
|
'الخميس',
|
|
'الجمعة',
|
|
'السبت',
|
|
'الأحد'
|
|
];
|
|
const months = [
|
|
'يناير',
|
|
'فبراير',
|
|
'مارس',
|
|
'أبريل',
|
|
'مايو',
|
|
'يونيو',
|
|
'يوليو',
|
|
'أغسطس',
|
|
'سبتمبر',
|
|
'أكتوبر',
|
|
'نوفمبر',
|
|
'ديسمبر'
|
|
];
|
|
final now = DateTime.now();
|
|
final day = now.day.toString().replaceAllMapped(
|
|
RegExp(r'\d'), (match) => '٠١٢٣٤٥٦٧٨٩'[int.parse(match.group(0)!)]);
|
|
return '${weekdays[now.weekday - 1]}، $day ${months[now.month - 1]}';
|
|
}
|
|
|
|
class _ActionCard extends StatelessWidget {
|
|
const _ActionCard(
|
|
{required this.title,
|
|
required this.subtitle,
|
|
required this.icon,
|
|
required this.tint,
|
|
required this.onTap});
|
|
final String title, subtitle;
|
|
final IconData icon;
|
|
final Color tint;
|
|
final VoidCallback onTap;
|
|
@override
|
|
Widget build(BuildContext context) => Material(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(17),
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.circular(17),
|
|
onTap: onTap,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(14),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
width: 38,
|
|
height: 38,
|
|
decoration: BoxDecoration(
|
|
color: tint,
|
|
borderRadius: BorderRadius.circular(12)),
|
|
child: Icon(icon, color: _green, size: 21)),
|
|
const SizedBox(height: 13),
|
|
Text(title,
|
|
style: const TextStyle(
|
|
color: _ink,
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w600)),
|
|
const SizedBox(height: 3),
|
|
Text(subtitle,
|
|
style: const TextStyle(color: _muted, fontSize: 10)),
|
|
]))),
|
|
);
|
|
}
|
|
|
|
class _ActiveWorkoutCard extends StatelessWidget {
|
|
const _ActiveWorkoutCard({required this.controller});
|
|
final WorkoutController controller;
|
|
@override
|
|
Widget build(BuildContext context) => Container(
|
|
padding: const EdgeInsets.all(17),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF293A30),
|
|
borderRadius: BorderRadius.circular(18)),
|
|
child: Column(children: [
|
|
const Row(children: [
|
|
Icon(Icons.fiber_manual_record, size: 9, color: Color(0xFFB6D29D)),
|
|
SizedBox(width: 8),
|
|
Text('النشاط جارٍ',
|
|
style:
|
|
TextStyle(color: Colors.white, fontWeight: FontWeight.w600))
|
|
]),
|
|
const SizedBox(height: 15),
|
|
Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [
|
|
_LiveStat('المسافة',
|
|
'${Formatters.distanceKm(controller.liveDistance.value)} كم'),
|
|
_LiveStat(
|
|
'الوقت', Formatters.time(controller.elapsedSeconds.value)),
|
|
_LiveStat('السرعة',
|
|
'${Formatters.speedKmh(controller.liveSpeed.value)} كم/س'),
|
|
]),
|
|
const SizedBox(height: 13),
|
|
Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: TextButton(
|
|
onPressed: controller.stopWorkout,
|
|
style: TextButton.styleFrom(foregroundColor: Colors.white),
|
|
child: const Text('إنهاء وحفظ'))),
|
|
]),
|
|
);
|
|
}
|
|
|
|
class _LiveStat extends StatelessWidget {
|
|
const _LiveStat(this.label, this.value);
|
|
final String label, value;
|
|
@override
|
|
Widget build(BuildContext context) => Column(children: [
|
|
Text(label,
|
|
style: const TextStyle(color: Colors.white60, fontSize: 10)),
|
|
const SizedBox(height: 3),
|
|
Text(value,
|
|
style: const TextStyle(
|
|
color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600))
|
|
]);
|
|
}
|
|
|
|
class _HabitCard extends StatelessWidget {
|
|
const _HabitCard(
|
|
{required this.icon,
|
|
required this.title,
|
|
required this.detail,
|
|
required this.tag,
|
|
this.onTap});
|
|
final IconData icon;
|
|
final String title, detail, tag;
|
|
final VoidCallback? onTap;
|
|
@override
|
|
Widget build(BuildContext context) => GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: onTap,
|
|
child: Container(
|
|
padding: const EdgeInsets.all(14),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white, borderRadius: BorderRadius.circular(15)),
|
|
child: Row(children: [
|
|
Container(
|
|
width: 40,
|
|
height: 40,
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFE8EFE5),
|
|
borderRadius: BorderRadius.circular(13)),
|
|
child: Icon(icon, color: _green, size: 20)),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(title,
|
|
style: const TextStyle(
|
|
color: _ink,
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w600)),
|
|
const SizedBox(height: 2),
|
|
Text(detail,
|
|
style: const TextStyle(color: _muted, fontSize: 10))
|
|
])),
|
|
Text(tag,
|
|
style: const TextStyle(
|
|
color: _green, fontSize: 10, fontWeight: FontWeight.w600)),
|
|
]),
|
|
));
|
|
}
|
|
|
|
void _openMealJournal() => Get.to(() => const MealJournalScreen());
|