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:
28
date_planner/lib/color_options.dart
Normal file
28
date_planner/lib/color_options.dart
Normal 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)];
|
||||
}
|
||||
81
date_planner/lib/event.dart
Normal file
81
date_planner/lib/event.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
166
date_planner/lib/event_data.dart
Normal file
166
date_planner/lib/event_data.dart
Normal 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),
|
||||
),
|
||||
];
|
||||
}
|
||||
143
date_planner/lib/event_detail.dart
Normal file
143
date_planner/lib/event_detail.dart
Normal 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: ''));
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
108
date_planner/lib/event_editor.dart
Normal file
108
date_planner/lib/event_editor.dart
Normal 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),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
102
date_planner/lib/event_list.dart
Normal file
102
date_planner/lib/event_list.dart
Normal 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);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
44
date_planner/lib/event_row.dart
Normal file
44
date_planner/lib/event_row.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
58
date_planner/lib/event_symbol.dart
Normal file
58
date_planner/lib/event_symbol.dart
Normal 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,
|
||||
];
|
||||
14
date_planner/lib/event_task.dart
Normal file
14
date_planner/lib/event_task.dart
Normal 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),
|
||||
];
|
||||
}
|
||||
32
date_planner/lib/main.dart
Normal file
32
date_planner/lib/main.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
104
date_planner/lib/symbol_editor.dart
Normal file
104
date_planner/lib/symbol_editor.dart
Normal 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;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
61
date_planner/lib/task_row.dart
Normal file
61
date_planner/lib/task_row.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user