1
0
mirror of https://github.com/flutter/samples.git synced 2025-11-08 22:09:06 +00:00

revives date_planner (#2593)

This commit is contained in:
Eric Windmill
2025-03-05 08:50:05 -05:00
committed by GitHub
parent 70042fcfbf
commit b1f8fa57b5
57 changed files with 2187 additions and 6 deletions

View File

@@ -0,0 +1,28 @@
// Copyright 2024 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math';
import 'package:flutter/cupertino.dart';
enum ColorOptions {
primary(CupertinoColors.black),
gray(CupertinoColors.lightBackgroundGray),
red(CupertinoColors.systemRed),
orange(CupertinoColors.systemOrange),
yellow(CupertinoColors.systemYellow),
green(CupertinoColors.systemGreen),
mint(CupertinoColors.systemMint),
cyan(CupertinoColors.systemCyan),
indigo(CupertinoColors.systemIndigo),
purple(CupertinoColors.systemPurple);
final Color color;
static final _rnd = Random();
const ColorOptions(this.color);
factory ColorOptions.random() =>
ColorOptions.values[_rnd.nextInt(ColorOptions.values.length)];
}

View File

@@ -0,0 +1,81 @@
// Copyright 2024 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:intl/intl.dart';
import 'package:uuid/uuid.dart';
import 'color_options.dart';
import 'event_task.dart';
class Event implements Comparable<Event> {
static const _uuid = Uuid();
final id = _uuid.v4();
String title;
ColorOptions color;
IconData icon;
List<EventTask> tasks;
DateTime date;
Event({
required this.title,
ColorOptions? color,
this.icon = CupertinoIcons.add,
List<EventTask>? tasks,
DateTime? date,
}) : color = color ?? ColorOptions.random(),
tasks = tasks ?? [EventTask(text: '')],
date = date ?? DateTime.now();
Event copy() {
return Event(
title: title,
color: color,
icon: icon,
tasks: tasks,
date: date,
);
}
updateWith(Event e) {
title = e.title;
color = e.color;
icon = e.icon;
tasks = e.tasks;
date = e.date;
}
int get remainingTaskCount => tasks.where((e) => !e.isCompleted).length;
bool get isComplete => remainingTaskCount == 0;
bool get isPast => DateTime.now().isAfter(date);
bool get isWithinSevenDays => !isPast && date.isBefore(FromNow.sevenDays);
bool get isWithinSevenToThirtyDays =>
!isPast && !isWithinSevenDays && date.isBefore(FromNow.thirtyDays);
bool get isDistant => date.isAfter(FromNow.thirtyDays);
String get dateFormated =>
'${DateFormat.yMMMd().format(date)} at '
'${DateFormat.Hm().format(date)}';
@override
int compareTo(Event other) => date.compareTo(other.date);
}
class FromNow {
static DateTime get sevenDays => DateTime.now().add(const Duration(days: 7));
static DateTime get thirtyDays =>
DateTime.now().add(const Duration(days: 30));
static DateTime roundedHours(int hours) {
final date = DateTime.now().add(Duration(hours: hours));
return DateTime(date.year, date.month, date.day, date.hour);
}
}

View File

@@ -0,0 +1,166 @@
// Copyright 2024 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'color_options.dart';
import 'event.dart';
import 'event_task.dart';
class EventData with ChangeNotifier {
static final _events = buildSampleData();
void add(Event event) {
_events.add(event);
notifyListeners();
}
void delete(Event event) {
_events.remove(event);
notifyListeners();
}
void update(Event original, Event updated) {
_events.firstWhere((e) => e.id == original.id).updateWith(updated);
notifyListeners();
}
void exists(Event event) => _events.contains(event);
List<Event> sorted(Period period) =>
_events
.where(
(e) => switch (period) {
Period.nextSevenDays => e.isWithinSevenDays,
Period.nextThirtyDays => e.isWithinSevenToThirtyDays,
Period.past => e.isPast,
Period.future => e.isDistant,
},
)
.toList()
..sort((e1, e2) => e1.date.compareTo(e2.date));
}
enum Period {
nextSevenDays(name: 'Next 7 Days'),
nextThirtyDays(name: 'Next 30 Days'),
future(name: 'Future'),
past(name: 'Past');
const Period({required this.name});
final String name;
}
List<Event> buildSampleData() {
return [
Event(
title: 'Maya\'s Birthday',
color: ColorOptions.red,
icon: CupertinoIcons.gift,
tasks: EventTask.buildList([
'Guava kombucha',
'Paper cups and plates',
'Cheese plate',
'Party poppers',
]),
date: FromNow.roundedHours(24 * 30),
),
Event(
title: 'Pagliacci',
color: ColorOptions.yellow,
// TODO(mit-mit): Use the icon "theatermasks.fill".
icon: CupertinoIcons.thermometer_snowflake,
tasks: EventTask.buildList([
'Buy new tux',
'Get tickets',
'Pick up Carmen at the airport and bring her to the show',
]),
date: FromNow.roundedHours(22),
),
Event(
title: 'Doctor\'s Appointment',
// TODO(mit-mit): Use the icon "facemask.fill".
icon: CupertinoIcons.lab_flask_solid,
color: ColorOptions.indigo,
tasks: EventTask.buildList([
'Bring medical ID',
'Record heart rate data',
]),
date: FromNow.roundedHours(24 * 4),
),
Event(
title: 'Camping Trip',
// TODO(mit-mit): Use the icon "leaf.fill".
icon: CupertinoIcons.leaf_arrow_circlepath,
color: ColorOptions.green,
tasks: EventTask.buildList([
'Find a sleeping bag',
'Bug spray',
'Paper towels',
'Food for 4 meals',
'Straw hat',
]),
date: FromNow.roundedHours(36),
),
Event(
title: 'Game Night',
icon: CupertinoIcons.gamecontroller_fill,
color: ColorOptions.cyan,
tasks: EventTask.buildList([
'Find a board game to bring',
'Bring a desert to share',
]),
date: FromNow.roundedHours(24 * 2),
),
Event(
title: 'First Day of School',
// TODO(mit-mit): Use the icon "graduationcap.fill".
icon: CupertinoIcons.hammer,
color: ColorOptions.primary,
tasks: EventTask.buildList([
'Notebooks',
'Pencils',
'Binder',
'First day of school outfit',
]),
date: FromNow.roundedHours(24 * 365),
),
Event(
title: 'Book Launch',
icon: CupertinoIcons.book_fill,
color: ColorOptions.purple,
tasks: EventTask.buildList([
'Finish first draft',
'Send draft to editor',
'Final read-through',
]),
date: FromNow.roundedHours(24 * 365 * 2),
),
Event(
title: 'WWDC',
// TODO(mit-mit): Use the icon "globe.americas.fill"
icon: CupertinoIcons.globe,
color: ColorOptions.gray,
tasks: EventTask.buildList([
'Watch Keynote',
'Watch What\'s new in SwiftUI',
'Go to DT developer labs',
'Learn about Create ML',
]),
date: DateTime(7, 6, 2021),
),
Event(
title: 'Sayulita Trip',
icon: CupertinoIcons.briefcase_fill,
color: ColorOptions.orange,
tasks: EventTask.buildList([
'Buy plane tickets',
'Get a new bathing suit',
'Find a hotel room',
]),
date: FromNow.roundedHours(24 * 19),
),
];
}

View File

@@ -0,0 +1,143 @@
// Copyright 2024 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'color_options.dart';
import 'event.dart';
import 'event_task.dart';
import 'symbol_editor.dart';
import 'task_row.dart';
class EventDetail extends StatefulWidget {
final Event event;
final bool isEditing;
const EventDetail({super.key, required this.event, required this.isEditing});
@override
State<EventDetail> createState() => _EventDetailState();
}
class _EventDetailState extends State<EventDetail> {
final _eventText = TextEditingController();
@override
void initState() {
_eventText.text = widget.event.title;
super.initState();
}
@override
Widget build(BuildContext context) {
const titleStyle = TextStyle(fontWeight: FontWeight.bold, fontSize: 22);
final event = widget.event;
// TODO(mit-mit): Investigate manual overriding of colors and padding.
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Container(
padding: const EdgeInsets.fromLTRB(16, 8, 0, 0),
color: CupertinoColors.systemBackground,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
if (widget.isEditing)
CupertinoButton(
padding: EdgeInsets.zero,
minimumSize: Size.zero,
onPressed: () {
Navigator.of(context)
.push(
CupertinoPageRoute<(IconData, ColorOptions)?>(
builder:
(_) =>
SymbolEditor(event.icon, event.color),
),
)
.then(((IconData, ColorOptions)? data) {
if (data != null) {
setState(() {
var (icon, color) = data;
event.icon = icon;
event.color = color;
});
}
});
},
child: Icon(
event.icon,
size: 28,
color: event.color.color,
),
),
if (!widget.isEditing)
Icon(event.icon, size: 28, color: event.color.color),
const SizedBox(width: 12),
if (widget.isEditing)
Expanded(
child: CupertinoTextField(
decoration: null,
padding: EdgeInsets.zero,
style: titleStyle,
controller: _eventText,
onChanged: (value) => event.title = value,
),
),
if (!widget.isEditing) Text(event.title, style: titleStyle),
],
),
const SizedBox(height: 12),
// TODO(mit-mit): Use a widget for picking a date.
// Issue: Blocked on not having the right calendar widget:
// https://github.com/flutter/flutter/issues/63693
Text(event.dateFormated),
CupertinoListSection(
// TODO(mit-mit): The list of tasks should be left-flush with the date above.
margin: EdgeInsets.zero,
backgroundColor: CupertinoColors.systemBackground,
decoration: null,
header: const Text(
'Tasks',
style: TextStyle(
color: CupertinoColors.black,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
children: [
for (EventTask t in event.tasks)
TaskRow(task: t, isEditing: widget.isEditing),
if (widget.isEditing)
// TODO(mit-mit): CupertinoButton with icon support?
// Consider if CupertinoButton could support setting
// both a label and an icon directly:
// https://www.kodeco.com/books/swiftui-cookbook/v1.0/chapters/8-add-an-icon-to-a-button-in-swiftui
CupertinoButton(
child: const Row(
children: [
Icon(CupertinoIcons.plus),
Text('Add task'),
],
),
onPressed: () {
setState(() {
event.tasks.add(EventTask(text: ''));
});
},
),
],
),
],
),
),
],
),
);
}
}

View File

@@ -0,0 +1,108 @@
// Copyright 2024 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'event.dart';
import 'event_detail.dart';
class EventEditor extends StatefulWidget {
final Event event;
final bool isNew;
const EventEditor({super.key, required this.event, required this.isNew});
@override
State<EventEditor> createState() => _EventEditorState();
}
class _EventEditorState extends State<EventEditor> {
late Event event;
late bool isNew;
late bool isEditing;
@override
void initState() {
isNew = widget.isNew;
isEditing = isNew;
event = widget.event;
super.initState();
}
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
backgroundColor: CupertinoColors.secondarySystemBackground,
navigationBar: CupertinoNavigationBar(
// TODO(mit-mit): Resolve manual padding issues.
//
// Note that even with the padding overriding below, the chevron/
// back arrow doesn't seem to be far enough to the left.
//
// Is this maybe the issue here?
// https://github.com/flutter/flutter/issues/91715
leading:
isNew
? CupertinoButton(
padding: EdgeInsets.zero,
child: const Text('Cancel'),
onPressed: () => Navigator.pop(context, null),
)
: CupertinoButton(
padding: EdgeInsets.zero,
onPressed: () {
Navigator.pop(context, event);
},
child: const Row(
children: [Icon(CupertinoIcons.back), Text('Date Planner')],
),
),
trailing: CupertinoButton(
padding: EdgeInsets.zero,
child: Text(isNew ? 'Add' : (isEditing ? 'Done' : 'Edit')),
onPressed: () {
if (isNew) {
Navigator.pop(context, event);
} else {
setState(() {
if (isEditing) {
isEditing = false;
} else {
isEditing = true;
}
});
}
},
),
),
// TODO(mit-mit): Why isn't SafeArea included by default?
child: SafeArea(
bottom: false,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
EventDetail(event: event, isEditing: isEditing),
const Spacer(),
if (isEditing && !isNew)
ColoredBox(
color: CupertinoColors.white,
child: Column(
children: [
CupertinoButton(
child: const Text('Delete Event'),
onPressed: () {
setState(() {
Navigator.pop(context, null);
});
},
),
const SizedBox(height: 24),
],
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,102 @@
// Copyright 2024 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';
import 'event.dart';
import 'event_data.dart';
import 'event_editor.dart';
import 'event_row.dart';
class EventList extends StatelessWidget {
const EventList({super.key});
@override
Widget build(BuildContext context) {
return Consumer<EventData>(
builder: (BuildContext context, EventData events, Widget? child) {
return CupertinoPageScaffold(
// TODO(mit-mit): Avoid having to pass nav bar manually.
//
// Would like to pass nav bar and body to CupertinoPageScaffold
// directly, similar to the Material Scaffold's `appBar` and `body`
// args.
// https://github.com/flutter/flutter/issues/149625
child: CustomScrollView(
slivers: <Widget>[
CupertinoSliverNavigationBar(
largeTitle: const Text('Date Planner'),
trailing: CupertinoButton(
padding: EdgeInsets.zero,
child: const Icon(CupertinoIcons.plus),
onPressed: () async {
// Issue: Should go to a sheet, not a a full-screen page.
// Blocked on https://github.com/flutter/flutter/issues/42560.
Event? newEvent = await Navigator.of(context).push(
CupertinoPageRoute<Event>(
builder:
(_) => EventEditor(
event: Event(title: 'New event'),
isNew: true,
),
),
);
if (newEvent != null) {
events.add(newEvent);
}
},
),
),
SliverList.list(
children: [
for (Period p in Period.values)
CupertinoListSection(
header: Text(
p.name.toUpperCase(),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
children: [
for (Event e in events.sorted(p))
// TODO: Support swipe action for deleting.
// Should probably use Dismissable?
// https://api.flutter.dev/flutter/widgets/Dismissible-class.html
EventRow(
event: e,
onTap: () async {
Event? updatedEvent = await Navigator.of(
context,
).push(
CupertinoPageRoute<Event>(
builder:
(_) => EventEditor(
event: e.copy(),
isNew: false,
),
),
);
if (updatedEvent == null) {
// The editor passes back null when it deleted
// the element.
events.delete(e);
} else {
events.update(e, updatedEvent);
}
},
),
],
),
],
),
],
),
);
},
);
}
}

View File

@@ -0,0 +1,44 @@
// Copyright 2024 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'event.dart';
class EventRow extends StatelessWidget {
const EventRow({super.key, required this.event, this.onTap});
final Event event;
final FutureOr<void> Function()? onTap;
@override
Widget build(BuildContext context) {
// TODO(mit-mit): The corners of the tiles should be rounded.
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: CupertinoListTile(
title: Text(
event.title,
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text(event.dateFormated),
leading: Icon(event.icon, size: 28, color: event.color.color),
trailing: Row(
children: [
event.isComplete
? const Icon(CupertinoIcons.check_mark)
: Text(
'${event.remainingTaskCount}',
style: const TextStyle(color: CupertinoColors.systemGrey),
),
const CupertinoListTileChevron(),
],
),
onTap: onTap,
),
);
}
}

View File

@@ -0,0 +1,58 @@
// Copyright 2024 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
// TODO(mit-mit): Update when missing icons are added.
// https://github.com/flutter/flutter/issues/82634
final eventSymbols = <IconData>[
CupertinoIcons.house_fill,
CupertinoIcons.ticket_fill,
CupertinoIcons.gamecontroller_fill,
//CupertinoIcons.theatermasks_fill,
//CupertinoIcons.ladybug_fill,
//CupertinoIcons.books.vertical_fill,
//CupertinoIcons.moon.zzz_fill,
CupertinoIcons.umbrella_fill,
//CupertinoIcons.paintbrush.pointed_fill,
//CupertinoIcons.leaf_fill,
//CupertinoIcons.globe.americas_fill,
CupertinoIcons.clock_fill,
//CupertinoIcons.building.2_fill,
CupertinoIcons.gift_fill,
//CupertinoIcons.graduationcap_fill,
//CupertinoIcons.heart.rectangle_fill,
//CupertinoIcons.phone.bubble.left_fill,
//CupertinoIcons.cloud.rain_fill,
//CupertinoIcons.building.columns_fill,
//CupertinoIcons.mic.circle_fill,
//CupertinoIcons.comb_fill,
//CupertinoIcons.person.3_fill,
CupertinoIcons.bell_fill,
CupertinoIcons.hammer_fill,
CupertinoIcons.star_fill,
//CupertinoIcons.crown_fill,
CupertinoIcons.briefcase_fill,
//CupertinoIcons.speaker.wave.3_fill,
//CupertinoIcons.tshirt_fill,
CupertinoIcons.tag_fill,
CupertinoIcons.airplane,
//CupertinoIcons.pawprint_fill,
//CupertinoIcons.case_fill,
CupertinoIcons.creditcard_fill,
//CupertinoIcons.infinity.circle_fill,
//CupertinoIcons.dice_fill,
CupertinoIcons.heart_fill,
CupertinoIcons.camera_fill,
//CupertinoIcons.bicycle,
//CupertinoIcons.radio_fill,
CupertinoIcons.car_fill,
CupertinoIcons.flag_fill,
CupertinoIcons.map_fill,
//CupertinoIcons.figure.wave,
//CupertinoIcons.mappin.and.ellipse,
//CupertinoIcons.facemask_fill,
CupertinoIcons.eyeglasses,
CupertinoIcons.tram_fill,
];

View File

@@ -0,0 +1,14 @@
// Copyright 2024 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
class EventTask {
String text;
bool isCompleted;
EventTask({required this.text, this.isCompleted = false});
static List<EventTask> buildList(List<String> taskDescriptions) => [
for (var task in taskDescriptions) EventTask(text: task),
];
}

View File

@@ -0,0 +1,32 @@
// Copyright 2024 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';
import 'event_data.dart';
import 'event_list.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => EventData(),
child: const DatePlannerApp(),
),
);
}
CupertinoThemeData cupertinoLight = const CupertinoThemeData(
brightness: Brightness.light,
primaryColor: CupertinoColors.activeBlue,
);
class DatePlannerApp extends StatelessWidget {
const DatePlannerApp({super.key});
@override
Widget build(BuildContext context) {
return CupertinoApp(home: const EventList(), theme: cupertinoLight);
}
}

View File

@@ -0,0 +1,104 @@
// Copyright 2024 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'color_options.dart';
import 'event_symbol.dart';
class SymbolEditor extends StatefulWidget {
final IconData icon;
final ColorOptions color;
const SymbolEditor(this.icon, this.color, {super.key});
@override
State<SymbolEditor> createState() => _SymbolEditorState();
}
class _SymbolEditorState extends State<SymbolEditor> {
late IconData _currentIcon = widget.icon;
late ColorOptions _currentColor = widget.color;
_SymbolEditorState();
@override
Widget build(BuildContext context) {
// TODO(mit-mit): Should use a Sheet
// https://github.com/flutter/flutter/issues/42560
return CupertinoPageScaffold(
backgroundColor: CupertinoColors.white,
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Row(
children: [
const Spacer(),
CupertinoButton(
padding: EdgeInsets.zero,
child: const Text('Done'),
onPressed:
() => Navigator.pop(context, (
_currentIcon,
_currentColor,
)),
),
],
),
const SizedBox(height: 16),
Icon(_currentIcon, size: 48, color: _currentColor.color),
const SizedBox(height: 32),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
for (ColorOptions color in ColorOptions.values)
// TODO(mit-mit): Circles should be bigger and have less padding between them.
CupertinoButton(
padding: EdgeInsets.zero,
minimumSize: Size.zero,
child: Icon(
CupertinoIcons.circle_fill,
color: color.color,
),
onPressed: () {
setState(() {
_currentColor = color;
});
},
),
],
),
const SizedBox(height: 16),
// TODO(mit-mit): File issue for missing Cupertino Divider widget.
// Should have something similar to the Material devider.
// https://api.flutter.dev/flutter/material/Divider-class.html
const Text('. . . . . . . . . . . . . . . '),
const SizedBox(height: 16),
Expanded(
child: GridView.count(
primary: false,
crossAxisCount: 6,
mainAxisSpacing: 10,
children: [
for (var icon in eventSymbols)
CupertinoButton(
padding: EdgeInsets.zero,
child: Icon(icon, size: 32),
onPressed: () {
setState(() {
_currentIcon = icon;
});
},
),
],
),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,61 @@
// Copyright 2024 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'event_task.dart';
class TaskRow extends StatefulWidget {
final EventTask task;
final bool isEditing;
const TaskRow({super.key, required this.task, required this.isEditing});
@override
State<TaskRow> createState() => _TaskRowState();
}
class _TaskRowState extends State<TaskRow> {
final _taskText = TextEditingController();
@override
void initState() {
_taskText.text = widget.task.text;
super.initState();
}
@override
Widget build(BuildContext context) {
return Row(
children: [
CupertinoButton(
onPressed:
widget.isEditing
? () {
setState(() {
widget.task.isCompleted = !widget.task.isCompleted;
});
}
: null,
child: Icon(
widget.task.isCompleted
? CupertinoIcons.checkmark_circle_fill
: CupertinoIcons.circle,
color: CupertinoColors.black,
),
),
Expanded(
child:
widget.isEditing
? CupertinoTextField(
decoration: null,
padding: EdgeInsets.zero,
controller: _taskText,
onChanged: (value) => widget.task.text = value,
)
: Text(widget.task.text),
),
],
);
}
}