Rebuild Home around a forest header with streak, score and water
Home now opens on a deep green header: greeting, a Today / Weekly toggle, a seven-day streak with a leaf on every kept day, minutes held with a + ring, plan thumbnails, the report-card letter as the score, and one bar per commitment class (weekly view swaps in per-day bars and class rates). The + opens an "Add to today" sheet over the blurred day. A water tracker sits below the header, kept on the device. The nav marks the selected tab as a filled block and drops the side action button. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KhQh614DMeYoQEH9SoMRUw
This commit is contained in:
@@ -7,22 +7,23 @@ import 'text/Text.dart';
|
||||
|
||||
typedef NavEntry = ({IconData icon, String label, VoidCallback onTap});
|
||||
|
||||
/// The floating glass tab bar with the dark round action button beside it.
|
||||
/// The floating tab bar: the selected tab is a filled night block, the rest
|
||||
/// sit quietly on white. An optional dark action button can sit beside it.
|
||||
class FloatingNav extends StatelessWidget {
|
||||
final List<NavEntry> entries;
|
||||
|
||||
final int selected;
|
||||
|
||||
final IconData actionIcon;
|
||||
final IconData? actionIcon;
|
||||
|
||||
final VoidCallback onAction;
|
||||
final VoidCallback? onAction;
|
||||
|
||||
const FloatingNav({
|
||||
super.key,
|
||||
required this.entries,
|
||||
required this.selected,
|
||||
required this.actionIcon,
|
||||
required this.onAction,
|
||||
this.actionIcon,
|
||||
this.onAction,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -58,6 +59,7 @@ class FloatingNav extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (onAction != null) ...[
|
||||
const SizedBox(width: 10),
|
||||
GestureDetector(
|
||||
onTap: onAction,
|
||||
@@ -81,6 +83,7 @@ class FloatingNav extends StatelessWidget {
|
||||
child: Icon(actionIcon, size: 24, color: colorLime),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -94,9 +97,9 @@ class FloatingNav extends StatelessWidget {
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 220),
|
||||
curve: Curves.easeOutCubic,
|
||||
padding: const EdgeInsets.symmetric(vertical: 9),
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: active ? colorWhite : colorWhite.withValues(alpha: 0.0),
|
||||
color: active ? colorHeaderBottom : colorWhite.withValues(alpha: 0.0),
|
||||
borderRadius: BorderRadius.zero,
|
||||
boxShadow: active
|
||||
? const [
|
||||
@@ -112,10 +115,10 @@ class FloatingNav extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(entry.icon,
|
||||
size: 23, color: active ? colorInk : colorInkSoft),
|
||||
size: 23, color: active ? colorWhite : colorInkSoft),
|
||||
const SizedBox(height: 4),
|
||||
text(entry.label, 10, active ? TextType.Medium : TextType.Regular,
|
||||
color: active ? colorInk : colorInkSoft, maxLines: 1),
|
||||
color: active ? colorWhite : colorInkSoft, maxLines: 1),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -85,10 +85,12 @@ Widget inputField(
|
||||
Widget quickAddField(
|
||||
TextEditingController controller,
|
||||
String hint,
|
||||
ValueChanged<String> onSubmitted,
|
||||
) {
|
||||
ValueChanged<String> onSubmitted, {
|
||||
FocusNode? focusNode,
|
||||
}) {
|
||||
return TextField(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
onSubmitted: onSubmitted,
|
||||
textInputAction: TextInputAction.done,
|
||||
style: TextStyle(
|
||||
|
||||
@@ -120,6 +120,12 @@ class AppDataManager implements DataManager {
|
||||
@override
|
||||
Future setAmnestySpent(int spent) => memory.setAmnestySpent(spent);
|
||||
|
||||
@override
|
||||
Future<int> getWaterToday() => memory.getWaterToday();
|
||||
|
||||
@override
|
||||
Future setWaterToday(int ml) => memory.setWaterToday(ml);
|
||||
|
||||
@override
|
||||
Future<List<TodoItem>> getTodos() => memory.getTodos();
|
||||
|
||||
|
||||
@@ -60,6 +60,11 @@ abstract class ConnectInternalMemory {
|
||||
|
||||
Future setAmnestySpent(int spent);
|
||||
|
||||
/// Water drunk today in ml; a new day reads as 0.
|
||||
Future<int> getWaterToday();
|
||||
|
||||
Future setWaterToday(int ml);
|
||||
|
||||
/// The quick to-do list on Home.
|
||||
Future<List<TodoItem>> getTodos();
|
||||
|
||||
|
||||
@@ -41,6 +41,8 @@ class InternalMemory implements ConnectInternalMemory {
|
||||
|
||||
static const String AMNESTY_SPENT = "AMNESTY_SPENT_Jr4xVh9KmT3gLc6B";
|
||||
|
||||
static const String WATER = "WATER_Qe7tNd4ZsM2kBv9C";
|
||||
|
||||
static const String TODOS = "TODOS_Wm3kRb8TqZ5nHc2F";
|
||||
|
||||
static const String SHOW_ONBOARDING = "SHOW_ONBOARDING_Nz6qGf2SdX7wPb4M";
|
||||
@@ -255,6 +257,31 @@ class InternalMemory implements ConnectInternalMemory {
|
||||
await groundedStorage.write(key: AMNESTY_SPENT, value: spent.toString());
|
||||
}
|
||||
|
||||
String _today() {
|
||||
final DateTime now = DateTime.now();
|
||||
return "${now.year}-${now.month}-${now.day}";
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> getWaterToday() async {
|
||||
String? value = await groundedStorage.read(key: WATER);
|
||||
|
||||
if (value != null) {
|
||||
final Map<String, dynamic> entry = jsonDecode(value);
|
||||
if (entry['day'] == _today()) {
|
||||
return entry['ml'] ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@override
|
||||
Future setWaterToday(int ml) async {
|
||||
await groundedStorage.write(
|
||||
key: WATER, value: jsonEncode({'day': _today(), 'ml': ml}));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<TodoItem>> getTodos() async {
|
||||
String? value = await groundedStorage.read(key: TODOS);
|
||||
|
||||
@@ -19,6 +19,7 @@ class MockMemory extends InternalMemory {
|
||||
setMyDescription(
|
||||
MeDescription(id: "mock-user", name: "Alex", token: "mock-token"));
|
||||
setUserDetails(UserDetails(pic: "", name: "Alex"));
|
||||
setWaterToday(1250);
|
||||
setTodos(<TodoItem>[
|
||||
TodoItem(id: "t1", title: "Refill water bottle"),
|
||||
TodoItem(id: "t2", title: "Book physio for Thursday"),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import '../../about/external/data/Commitment.dart';
|
||||
import '../../about/external/data/ExcuseCluster.dart';
|
||||
import '../../about/external/data/ReportCard.dart';
|
||||
import '../../about/external/data/RoutineChain.dart';
|
||||
import '../../about/internal/application/Standing.dart';
|
||||
import '../../about/internal/application/TodoItem.dart';
|
||||
@@ -15,6 +16,13 @@ abstract class ConnectHome {
|
||||
/// The routine library, for the up-next rail.
|
||||
void onRoutinesLoaded(List<RoutineChain> routines);
|
||||
|
||||
/// This week's report card, for the streak row, the score and the weekly
|
||||
/// view. Null when it could not be loaded.
|
||||
void onWeekLoaded(ReportCard? week);
|
||||
|
||||
/// Water drunk today, in ml.
|
||||
void onWaterLoaded(int ml);
|
||||
|
||||
/// The quick to-do list, read from the device.
|
||||
void onTodosLoaded(List<TodoItem> todos);
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
import '../../about/external/data/Commitment.dart';
|
||||
import '../../about/external/data/ExcuseCluster.dart';
|
||||
import '../../about/external/data/ReportCard.dart';
|
||||
import '../../about/external/data/RoutineChain.dart';
|
||||
import '../../about/external/data/pages/request/CommitmentsRequest.dart';
|
||||
import '../../about/external/data/pages/request/HistoryRequest.dart';
|
||||
@@ -34,9 +35,40 @@ class ViewHome extends ParentViewModel {
|
||||
|
||||
loadTodos();
|
||||
|
||||
loadWater();
|
||||
|
||||
loadWeek();
|
||||
|
||||
loadPlan();
|
||||
}
|
||||
|
||||
/// The last seven days. Quiet on failure: the header falls back to today.
|
||||
void loadWeek() async {
|
||||
try {
|
||||
final DateTime now = DateTime.now();
|
||||
final DateTime start = DateTime(now.year, now.month, now.day)
|
||||
.subtract(const Duration(days: 6));
|
||||
|
||||
final response =
|
||||
await getDataManager().getWeeklyReportCard(ReportCardRequest(
|
||||
periodStart: start.toIso8601String(),
|
||||
periodEnd: now.toIso8601String(),
|
||||
));
|
||||
|
||||
connection.onWeekLoaded(ReportCard.fromJson(response.data));
|
||||
} catch (e) {
|
||||
connection.onWeekLoaded(null);
|
||||
}
|
||||
}
|
||||
|
||||
void loadWater() async {
|
||||
connection.onWaterLoaded(await getDataManager().getWaterToday());
|
||||
}
|
||||
|
||||
void saveWater(int ml) async {
|
||||
await getDataManager().setWaterToday(ml);
|
||||
}
|
||||
|
||||
void loadTodos() async {
|
||||
connection.onTodosLoaded(await getDataManager().getTodos());
|
||||
}
|
||||
|
||||
@@ -127,6 +127,21 @@ const Color colorOverdueWarm = Color(0xFFF9E1D8);
|
||||
const Color colorOverdueWarmEnd = Color(0xFFFFF6EF);
|
||||
const Color colorOverdueGlow = Color(0xFFD7654B);
|
||||
|
||||
// ── Home header ───────────────────────────────────────────────────────────────
|
||||
// The deep forest block the day sits in, with brighter bars that read on it.
|
||||
const Color colorHeaderTop = Color(0xFF203D30);
|
||||
const Color colorHeaderBottom = Color(0xFF15281F);
|
||||
const Color colorHeaderPanel = Color(0x14FFFFFF);
|
||||
const Color colorHeaderLine = Color(0x26FFFFFF);
|
||||
const Color colorHeaderMuted = Color(0x99FFFFFF);
|
||||
const Color colorHeaderSelected = Color(0xFF3D6B53);
|
||||
const Color colorBarNonNegotiable = Color(0xFFE8735A);
|
||||
const Color colorBarStandard = Color(0xFFF2C14E);
|
||||
const Color colorBarElective = Color(0xFF6FD39B);
|
||||
// Water: filled drops and the empty ones.
|
||||
const Color colorWater = Color(0xFF6E8EF0);
|
||||
const Color colorWaterEmpty = Color(0xFFDDE1DE);
|
||||
|
||||
// ── Scene art ─────────────────────────────────────────────────────────────────
|
||||
// The painted landscapes that stand in for photography on cards. Each kind has
|
||||
// a sky (top → horizon), a sun, three ridges (far → near) and an ember accent.
|
||||
|
||||
Reference in New Issue
Block a user