mirror of
https://github.com/kuhyx/wake-alarm.git
synced 2026-11-10 19:27:02 +01:00
- Sick days screen (own name, sick icon, cat-2 fill): tap today or a later day to mark it sick, tap again to clear; past days inert. Writes the sickdays wire format (state sick/cleared + actor, one HLC per write) to sickdays-sync/devices via crdt_sync syncLog. - Mutual exclusion in both pickers: each syncs the other pool on open and refuses a date it holds, naming the pool; an unreadable pool refuses all. - Native SickDays cache (fail closed via DayCache); AlarmReceiver skips the ring with reason "sick_day" (no markFired, no ring_started); StickCheck verdict sick_day, streak skips sick days; stakes card says so. - Pre-alarm pool sync: allow-while-idle alarm at 21:00 and A-30 (request code 1006) runs a headless engine (poolSyncMain) that pulls both pools from RTDB and rewrites both native caches, so a PC mark arrives with the app closed. The ring decision stays a cache read. - JVM unit tests (JUnit 4.13.2) for DayCache, RingDecision, PoolSyncSchedule, StickCheck; Dart tests; Dart->Python interop check. pylint hook skipped: verify.py imports sickdays, not in the hook env until the sickdays-v0.1.0 tag exists; run by hand in a sickdays venv (10.00/10). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xfj1sxKH9VBPqpcAni8Jry
119 lines
3.7 KiB
Dart
119 lines
3.7 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:wake_alarm_sync/screens/day_calendar.dart';
|
|
import 'package:wake_alarm_sync/services/free_day_pool.dart';
|
|
import 'package:wake_alarm_sync/ui/theme.dart';
|
|
|
|
/// One upcoming free day, as the list shows it.
|
|
typedef UpcomingFreeDay = ({DateTime day, String reason});
|
|
|
|
/// The picker's body: the year's budget, the month calendar, the upcoming
|
|
/// days and the status line.
|
|
///
|
|
/// Stateless and fed plain values, like `HomeBody`: the screen owns the pool
|
|
/// and the sync ordering, this only draws.
|
|
class FreeDaysBody extends StatelessWidget {
|
|
/// Creates the body from already-resolved state.
|
|
const FreeDaysBody({
|
|
required this.year,
|
|
required this.remaining,
|
|
required this.upcoming,
|
|
required this.status,
|
|
required this.busy,
|
|
required this.onRelease,
|
|
required this.calendar,
|
|
super.key,
|
|
});
|
|
|
|
/// The month grid, built by the screen, which owns the displayed month.
|
|
final DayCalendar calendar;
|
|
|
|
/// The calendar year the budget line describes.
|
|
final int year;
|
|
|
|
/// Days left in [year].
|
|
final int remaining;
|
|
|
|
/// Free days from today on, ascending.
|
|
final List<UpcomingFreeDay> upcoming;
|
|
|
|
/// The status line; may be a raw `Error: …`.
|
|
final String status;
|
|
|
|
/// Whether a sync is in flight, disabling the release buttons.
|
|
final bool busy;
|
|
|
|
/// Called when the user gives a day back.
|
|
final void Function(DateTime day) onRelease;
|
|
|
|
static const _weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
|
|
|
String _label(DateTime day) =>
|
|
'${_weekdays[day.weekday - 1]} ${FreeDayPool.toIso(day)}';
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final statusColors = theme.extension<AppStatusColors>()!;
|
|
return SafeArea(
|
|
child: ListView(
|
|
padding: const EdgeInsets.all(16),
|
|
children: [
|
|
Text(
|
|
'$remaining of ${FreeDayPool.annualBudget}',
|
|
style: theme.textTheme.displayMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
Text(
|
|
'free days left in $year',
|
|
style: theme.textTheme.bodyMedium?.copyWith(
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'On a free day nothing locks, nothing rings and nothing asks. '
|
|
'Every app, at once.',
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
if (status.isNotEmpty) ...[
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
status,
|
|
maxLines: 4,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
color: status.contains('Error')
|
|
? theme.colorScheme.error
|
|
: statusColors.success,
|
|
),
|
|
),
|
|
],
|
|
const SizedBox(height: 16),
|
|
calendar,
|
|
const SizedBox(height: 24),
|
|
Text('Booked ahead', style: theme.textTheme.titleMedium),
|
|
const SizedBox(height: 8),
|
|
if (upcoming.isEmpty)
|
|
Text(
|
|
'None. Tap a date above to book one.',
|
|
style: TextStyle(color: theme.colorScheme.onSurfaceVariant),
|
|
),
|
|
for (final entry in upcoming)
|
|
ListTile(
|
|
contentPadding: EdgeInsets.zero,
|
|
title: Text(_label(entry.day)),
|
|
subtitle: entry.reason.isEmpty ? null : Text(entry.reason),
|
|
trailing: TextButton(
|
|
onPressed: busy ? null : () => onRelease(entry.day),
|
|
child: const Text('Release'),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|