feat: Murattal player enhancements & prayer schedule auto-scroll

- Murattal: Spotify-style 5-button controls [Shuffle, Prev, Play, Next, Playlist]
- Murattal: Animated 7-bar equalizer visualization in player circle
- Murattal: Unsplash API background with frosted glass player overlay
- Murattal: Transparent AppBar with backdrop blur
- Murattal: Surah playlist bottom sheet with full 114 Surah list
- Murattal: Auto-play disabled on screen open, enabled on navigation
- Murattal: Shuffle mode for random Surah playback
- Murattal: Photographer attribution per Unsplash guidelines
- Dashboard: Auto-scroll prayer schedule to next active prayer
- Fix: setState lifecycle errors on Reading & Murattal screens
- Setup: flutter_dotenv, cached_network_image, url_launcher deps
This commit is contained in:
dwindown
2026-03-13 15:42:17 +07:00
commit faadc1865d
189 changed files with 23834 additions and 0 deletions

View File

View File

@@ -0,0 +1 @@
// TODO: implement

View File

@@ -0,0 +1,210 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:intl/intl.dart';
import '../../../data/services/myquran_sholat_service.dart';
import '../../../data/services/prayer_service.dart';
import '../../../data/services/location_service.dart';
import '../../../data/local/hive_boxes.dart';
import '../../../data/local/models/app_settings.dart';
/// Represents a single prayer time entry.
class PrayerTimeEntry {
final String name;
final String time; // "HH:mm"
final bool isActive;
PrayerTimeEntry({
required this.name,
required this.time,
this.isActive = false,
});
}
/// Full day prayer schedule from myQuran API.
class DaySchedule {
final String cityName;
final String province;
final String date; // yyyy-MM-dd
final String tanggal; // formatted date from API
final Map<String, String> times; // {imsak, subuh, terbit, dhuha, dzuhur, ashar, maghrib, isya}
DaySchedule({
required this.cityName,
required this.province,
required this.date,
required this.tanggal,
required this.times,
});
/// Is this schedule for tomorrow?
bool get isTomorrow {
final todayStr = DateFormat('yyyy-MM-dd').format(DateTime.now());
return date.compareTo(todayStr) > 0;
}
/// Get prayer time entries as a list.
List<PrayerTimeEntry> get prayerList {
final now = DateTime.now();
final formatter = DateFormat('HH:mm');
final currentTime = formatter.format(now);
final prayers = [
PrayerTimeEntry(name: 'Imsak', time: times['imsak'] ?? '-'),
PrayerTimeEntry(name: 'Subuh', time: times['subuh'] ?? '-'),
PrayerTimeEntry(name: 'Terbit', time: times['terbit'] ?? '-'),
PrayerTimeEntry(name: 'Dhuha', time: times['dhuha'] ?? '-'),
PrayerTimeEntry(name: 'Dzuhur', time: times['dzuhur'] ?? '-'),
PrayerTimeEntry(name: 'Ashar', time: times['ashar'] ?? '-'),
PrayerTimeEntry(name: 'Maghrib', time: times['maghrib'] ?? '-'),
PrayerTimeEntry(name: 'Isya', time: times['isya'] ?? '-'),
];
// Find the next prayer
int activeIndex = -1;
if (isTomorrow) {
// User specifically requested to show tomorrow's Subuh as upcoming
activeIndex = 1; // 0=Imsak, 1=Subuh
} else {
for (int i = 0; i < prayers.length; i++) {
if (prayers[i].time != '-' && prayers[i].time.compareTo(currentTime) > 0) {
activeIndex = i;
break;
}
}
}
if (activeIndex >= 0) {
prayers[activeIndex] = PrayerTimeEntry(
name: prayers[activeIndex].name,
time: prayers[activeIndex].time,
isActive: true,
);
}
return prayers;
}
/// Get the next prayer name and time.
PrayerTimeEntry? get nextPrayer {
final list = prayerList;
for (final p in list) {
if (p.isActive) return p;
}
// If none active and it's today, all prayers have passed
return null;
}
}
/// Default Jakarta city ID from myQuran API.
const _defaultCityId = '58a2fc6ed39fd083f55d4182bf88826d';
/// Provider for the user's selected city ID (stored in Hive settings).
final selectedCityIdProvider = StateProvider<String>((ref) {
final box = Hive.box<AppSettings>(HiveBoxes.settings);
final settings = box.get('default');
final stored = settings?.lastCityName ?? '';
if (stored.contains('|')) {
return stored.split('|').last;
}
return _defaultCityId;
});
/// Provider for today's prayer times using myQuran API.
final prayerTimesProvider = FutureProvider<DaySchedule?>((ref) async {
final cityId = ref.watch(selectedCityIdProvider);
final today = DateFormat('yyyy-MM-dd').format(DateTime.now());
DaySchedule? schedule;
// Try API first
final jadwal =
await MyQuranSholatService.instance.getDailySchedule(cityId, today);
if (jadwal != null) {
final cityInfo = await MyQuranSholatService.instance.getCityInfo(cityId);
schedule = DaySchedule(
cityName: cityInfo?['kabko'] ?? 'Jakarta',
province: cityInfo?['prov'] ?? 'DKI Jakarta',
date: today,
tanggal: jadwal['tanggal'] ?? today,
times: jadwal,
);
}
// Check if all prayers today have passed
if (schedule != null && !schedule.isTomorrow && schedule.nextPrayer == null) {
// All prayers passed, fetch tomorrow's schedule
final tomorrow = DateTime.now().add(const Duration(days: 1));
final tomorrowStr = DateFormat('yyyy-MM-dd').format(tomorrow);
final tmrwJadwal =
await MyQuranSholatService.instance.getDailySchedule(cityId, tomorrowStr);
if (tmrwJadwal != null) {
final cityInfo = await MyQuranSholatService.instance.getCityInfo(cityId);
schedule = DaySchedule(
cityName: cityInfo?['kabko'] ?? 'Jakarta',
province: cityInfo?['prov'] ?? 'DKI Jakarta',
date: tomorrowStr,
tanggal: tmrwJadwal['tanggal'] ?? tomorrowStr,
times: tmrwJadwal,
);
}
}
if (schedule != null) {
return schedule;
}
// Fallback to adhan package
final position = await LocationService.instance.getCurrentLocation();
double lat = position?.latitude ?? -6.2088;
double lng = position?.longitude ?? 106.8456;
final result = PrayerService.instance.getPrayerTimes(lat, lng, DateTime.now());
if (result != null) {
final timeFormat = DateFormat('HH:mm');
return DaySchedule(
cityName: 'Jakarta',
province: 'DKI Jakarta',
date: today,
tanggal: DateFormat('EEEE, dd/MM/yyyy').format(DateTime.now()),
times: {
'imsak': timeFormat.format(result.fajr.subtract(const Duration(minutes: 10))),
'subuh': timeFormat.format(result.fajr),
'terbit': timeFormat.format(result.sunrise),
'dhuha': timeFormat.format(result.sunrise.add(const Duration(minutes: 15))),
'dzuhur': timeFormat.format(result.dhuhr),
'ashar': timeFormat.format(result.asr),
'maghrib': timeFormat.format(result.maghrib),
'isya': timeFormat.format(result.isha),
},
);
}
return null;
});
/// Provider for monthly prayer schedule (for Imsakiyah screen).
final monthlyScheduleProvider =
FutureProvider.family<Map<String, Map<String, String>>, String>(
(ref, month) async {
final cityId = ref.watch(selectedCityIdProvider);
return MyQuranSholatService.instance.getMonthlySchedule(cityId, month);
});
/// Provider for current city name.
final cityNameProvider = FutureProvider<String>((ref) async {
final box = Hive.box<AppSettings>(HiveBoxes.settings);
final settings = box.get('default');
final stored = settings?.lastCityName ?? '';
if (stored.contains('|')) {
return stored.split('|').first;
}
final cityId = ref.watch(selectedCityIdProvider);
final info = await MyQuranSholatService.instance.getCityInfo(cityId);
if (info != null) {
return '${info['kabko']}, ${info['prov']}';
}
return 'Kota Jakarta, DKI Jakarta';
});

View File

View File

@@ -0,0 +1 @@
// TODO: implement

View File

@@ -0,0 +1,673 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import 'package:go_router/go_router.dart';
import 'package:hive_flutter/hive_flutter.dart';
import '../../../app/theme/app_colors.dart';
import '../../../core/widgets/prayer_time_card.dart';
import '../../../data/local/hive_boxes.dart';
import '../../../data/local/models/daily_worship_log.dart';
import '../data/prayer_times_provider.dart';
class DashboardScreen extends ConsumerStatefulWidget {
const DashboardScreen({super.key});
@override
ConsumerState<DashboardScreen> createState() => _DashboardScreenState();
}
class _DashboardScreenState extends ConsumerState<DashboardScreen> {
Timer? _countdownTimer;
Duration _countdown = Duration.zero;
String _nextPrayerName = '';
final ScrollController _prayerScrollController = ScrollController();
@override
void dispose() {
_countdownTimer?.cancel();
_prayerScrollController.dispose();
super.dispose();
}
void _startCountdown(DaySchedule schedule) {
_countdownTimer?.cancel();
_updateCountdown(schedule);
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (_) {
_updateCountdown(schedule);
});
}
void _updateCountdown(DaySchedule schedule) {
final next = schedule.nextPrayer;
if (next != null && next.time != '-') {
final parts = next.time.split(':');
if (parts.length == 2) {
final now = DateTime.now();
var target = DateTime(now.year, now.month, now.day,
int.parse(parts[0]), int.parse(parts[1]));
if (target.isBefore(now)) {
target = target.add(const Duration(days: 1));
}
setState(() {
_nextPrayerName = next.name;
_countdown = target.difference(now);
if (_countdown.isNegative) _countdown = Duration.zero;
});
}
}
}
String _formatCountdown(Duration d) {
final h = d.inHours.toString().padLeft(2, '0');
final m = (d.inMinutes % 60).toString().padLeft(2, '0');
final s = (d.inSeconds % 60).toString().padLeft(2, '0');
return '$h:$m:$s';
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
final prayerTimesAsync = ref.watch(prayerTimesProvider);
return Scaffold(
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
_buildHeader(context, isDark),
const SizedBox(height: 20),
prayerTimesAsync.when(
data: (schedule) {
if (schedule != null) {
_startCountdown(schedule);
return _buildHeroCard(context, schedule);
}
return _buildHeroCardPlaceholder(context);
},
loading: () => _buildHeroCardPlaceholder(context),
error: (_, __) => _buildHeroCardPlaceholder(context),
),
const SizedBox(height: 24),
_buildPrayerTimesSection(context, prayerTimesAsync),
const SizedBox(height: 24),
_buildChecklistSummary(context, isDark),
const SizedBox(height: 24),
_buildWeeklyProgress(context, isDark),
const SizedBox(height: 24),
],
),
),
),
);
}
Widget _buildHeader(BuildContext context, bool isDark) {
return Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: AppColors.primary, width: 2),
color: AppColors.primary.withValues(alpha: 0.2),
),
child: const Icon(Icons.person, size: 20, color: AppColors.primary),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Selamat datang,',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: isDark
? AppColors.textSecondaryDark
: AppColors.textSecondaryLight,
),
),
Text(
"Assalamu'alaikum",
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
),
),
],
),
),
Row(
children: [
IconButton(
onPressed: () {},
icon: Icon(
Icons.notifications_outlined,
color: isDark
? AppColors.textSecondaryDark
: AppColors.textSecondaryLight,
),
),
IconButton(
onPressed: () => context.push('/settings'),
icon: Icon(
Icons.settings_outlined,
color: isDark
? AppColors.textSecondaryDark
: AppColors.textSecondaryLight,
),
),
],
),
],
);
}
Widget _buildHeroCard(BuildContext context, DaySchedule schedule) {
final next = schedule.nextPrayer;
final name = _nextPrayerName.isNotEmpty
? _nextPrayerName
: (next?.name ?? 'Isya');
final time = next?.time ?? '--:--';
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColors.primary,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: AppColors.primary.withValues(alpha: 0.3),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: Stack(
children: [
Positioned(
top: -20,
right: -20,
child: Container(
width: 120,
height: 120,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withValues(alpha: 0.15),
),
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.schedule,
size: 16,
color: AppColors.onPrimary.withValues(alpha: 0.8)),
const SizedBox(width: 6),
Text(
'SHOLAT BERIKUTNYA',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
letterSpacing: 1.5,
color: AppColors.onPrimary.withValues(alpha: 0.8),
),
),
],
),
const SizedBox(height: 8),
Text(
'$name$time',
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.w800,
color: AppColors.onPrimary,
),
),
const SizedBox(height: 4),
Text(
'Hitung mundur: ${_formatCountdown(_countdown)}',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: AppColors.onPrimary.withValues(alpha: 0.8),
),
),
const SizedBox(height: 4),
// City name
Text(
'📍 ${schedule.cityName}',
style: TextStyle(
fontSize: 13,
color: AppColors.onPrimary.withValues(alpha: 0.7),
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: GestureDetector(
onTap: () => context.push('/tools/qibla'),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: AppColors.onPrimary,
borderRadius: BorderRadius.circular(50),
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.explore, size: 18, color: Colors.white),
SizedBox(width: 8),
Text(
'Arah Kiblat',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
],
),
),
),
),
const SizedBox(width: 12),
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.2),
shape: BoxShape.circle,
),
child: const Icon(
Icons.volume_up,
color: AppColors.onPrimary,
size: 22,
),
),
],
),
],
),
],
),
);
}
Widget _buildHeroCardPlaceholder(BuildContext context) {
return Container(
width: double.infinity,
height: 180,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColors.primary,
borderRadius: BorderRadius.circular(24),
),
child: const Center(
child: CircularProgressIndicator(color: AppColors.onPrimary),
),
);
}
Widget _buildPrayerTimesSection(
BuildContext context, AsyncValue<DaySchedule?> prayerTimesAsync) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
prayerTimesAsync.value?.isTomorrow == true
? 'Jadwal Sholat Besok'
: 'Jadwal Sholat Hari Ini',
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(fontWeight: FontWeight.w700)),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: AppColors.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(50),
),
child: Text(
prayerTimesAsync.value?.isTomorrow == true ? 'BESOK' : 'HARI INI',
style: TextStyle(
color: AppColors.primary,
fontSize: 10,
fontWeight: FontWeight.w700,
letterSpacing: 1.5,
),
),
),
],
),
const SizedBox(height: 12),
SizedBox(
height: 110,
child: prayerTimesAsync.when(
data: (schedule) {
if (schedule == null) return const SizedBox();
final prayers = schedule.prayerList.where(
(p) => ['Subuh', 'Dzuhur', 'Ashar', 'Maghrib', 'Isya']
.contains(p.name),
).toList();
return ListView.separated(
controller: _prayerScrollController,
scrollDirection: Axis.horizontal,
itemCount: prayers.length,
separatorBuilder: (_, __) => const SizedBox(width: 12),
itemBuilder: (context, i) {
final p = prayers[i];
final icon = _prayerIcon(p.name);
// Auto-scroll to active prayer on first build
if (p.isActive && i > 0) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_prayerScrollController.hasClients) {
final targetOffset = i * 124.0; // 112 width + 12 gap
_prayerScrollController.animateTo(
targetOffset.clamp(0, _prayerScrollController.position.maxScrollExtent),
duration: const Duration(milliseconds: 400),
curve: Curves.easeOut,
);
}
});
}
return PrayerTimeCard(
prayerName: p.name,
time: p.time,
icon: icon,
isActive: p.isActive,
);
},
);
},
loading: () =>
const Center(child: CircularProgressIndicator()),
error: (_, __) =>
const Center(child: Text('Gagal memuat jadwal')),
),
),
],
);
}
IconData _prayerIcon(String name) {
switch (name) {
case 'Subuh':
return Icons.wb_twilight;
case 'Dzuhur':
return Icons.wb_sunny;
case 'Ashar':
return Icons.filter_drama;
case 'Maghrib':
return Icons.wb_twilight;
case 'Isya':
return Icons.dark_mode;
default:
return Icons.schedule;
}
}
Widget _buildChecklistSummary(BuildContext context, bool isDark) {
final todayKey = DateFormat('yyyy-MM-dd').format(DateTime.now());
final box = Hive.box<DailyWorshipLog>(HiveBoxes.worshipLogs);
final log = box.get(todayKey);
final points = log?.totalPoints ?? 0;
// We can assume a max "excellent" day is around 150 points for the progress ring scale
final percent = (points / 150).clamp(0.0, 1.0);
// Prepare dynamic preview lines
int fardhuCompleted = 0;
if (log != null) {
fardhuCompleted = log.shalatLogs.values.where((l) => l.completed).length;
}
String amalanText = 'Belum ada data';
if (log != null) {
List<String> aList = [];
if (log.tilawahLog?.isCompleted == true) aList.add('Tilawah');
if (log.puasaLog?.completed == true) aList.add('Puasa');
if (log.dzikirLog?.pagi == true) aList.add('Dzikir');
if (aList.isNotEmpty) {
amalanText = aList.join(', ');
}
}
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: isDark ? AppColors.surfaceDark : AppColors.surfaceLight,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isDark
? AppColors.primary.withValues(alpha: 0.1)
: AppColors.cream,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Poin Ibadah Hari Ini',
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(fontWeight: FontWeight.w700)),
const SizedBox(height: 4),
Text(
'Kumpulkan poin dengan konsisten!',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: isDark
? AppColors.textSecondaryDark
: AppColors.textSecondaryLight,
),
),
],
),
),
SizedBox(
width: 48,
height: 48,
child: Stack(
alignment: Alignment.center,
children: [
CircularProgressIndicator(
value: percent,
strokeWidth: 4,
backgroundColor:
AppColors.primary.withValues(alpha: 0.15),
valueColor: const AlwaysStoppedAnimation<Color>(
AppColors.primary),
),
Text(
'$points',
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w800,
color: AppColors.primary,
),
),
],
),
),
],
),
const SizedBox(height: 16),
_checklistPreviewItem(
context, isDark, 'Sholat Fardhu', '$fardhuCompleted dari 5 selesai', fardhuCompleted == 5),
const SizedBox(height: 8),
_checklistPreviewItem(
context, isDark, 'Amalan Selesai', amalanText, points > 50),
const SizedBox(height: 16),
GestureDetector(
onTap: () => context.go('/checklist'),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: AppColors.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(50),
),
child: const Center(
child: Text(
'Lihat Semua Checklist',
style: TextStyle(
color: AppColors.primary,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
),
),
],
),
);
}
Widget _checklistPreviewItem(BuildContext context, bool isDark, String title,
String subtitle, bool completed) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isDark
? AppColors.primary.withValues(alpha: 0.05)
: AppColors.backgroundLight,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Icon(
completed ? Icons.check_circle : Icons.radio_button_unchecked,
color: AppColors.primary,
size: 22,
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(fontWeight: FontWeight.w600)),
Text(subtitle,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: isDark
? AppColors.textSecondaryDark
: AppColors.textSecondaryLight,
)),
],
),
],
),
);
}
Widget _buildWeeklyProgress(BuildContext context, bool isDark) {
final box = Hive.box<DailyWorshipLog>(HiveBoxes.worshipLogs);
final now = DateTime.now();
// Reverse so today is on the far right (index 6)
final last7Days = List.generate(7, (i) => now.subtract(Duration(days: 6 - i)));
final daysLabels = ['Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab', 'Min'];
final weekPoints = <int>[];
for (final d in last7Days) {
final k = DateFormat('yyyy-MM-dd').format(d);
final l = box.get(k);
weekPoints.add(l?.totalPoints ?? 0);
}
// Find the max points acquired this week to scale the bars, with a minimum floor of 50
final maxPts = weekPoints.reduce((a, b) => a > b ? a : b).clamp(50, 300);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Progres Poin Mingguan',
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(fontWeight: FontWeight.w700)),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: isDark ? AppColors.surfaceDark : AppColors.surfaceLight,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isDark
? AppColors.primary.withValues(alpha: 0.1)
: AppColors.cream,
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: List.generate(7, (i) {
final val = weekPoints[i];
final ratio = (val / maxPts).clamp(0.1, 1.0);
return Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 80,
child: Align(
alignment: Alignment.bottomCenter,
child: Container(
width: 24,
height: 80 * ratio,
decoration: BoxDecoration(
color: val > 0
? AppColors.primary.withValues(
alpha: 0.2 + ratio * 0.8)
: AppColors.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
),
),
),
),
const SizedBox(height: 8),
Text(
daysLabels[last7Days[i].weekday - 1], // Correct localized day
style: TextStyle(
fontSize: 10,
fontWeight: i == 6 ? FontWeight.w800 : FontWeight.w600,
color: i == 6
? AppColors.primary
: (isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight),
),
),
],
),
),
);
}),
),
),
],
);
}
}

View File

@@ -0,0 +1 @@
// TODO: implement