mirror of
https://github.com/Iconica-Development/flutter_timetable.git
synced 2025-05-18 19:43:43 +02:00
commit
f0ffd233c7
10 changed files with 156 additions and 97 deletions
|
@ -124,7 +124,7 @@ linter:
|
|||
prefer_asserts_with_message: true
|
||||
prefer_collection_literals: true
|
||||
prefer_conditional_assignment: true
|
||||
prefer_const_constructors: false
|
||||
prefer_const_constructors: true
|
||||
prefer_const_constructors_in_immutables: true
|
||||
prefer_const_declarations: false
|
||||
prefer_const_literals_to_create_immutables: false
|
||||
|
|
|
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:timetable/timetable.dart';
|
||||
|
||||
void main() {
|
||||
runApp(MaterialApp(home: TimetableDemo()));
|
||||
runApp(const MaterialApp(home: TimetableDemo()));
|
||||
}
|
||||
|
||||
class TimetableDemo extends StatefulWidget {
|
||||
|
@ -17,54 +17,54 @@ class _TimetableDemoState extends State<TimetableDemo> {
|
|||
final ScrollController _scrollController = ScrollController();
|
||||
final List<TimeBlock> blocks = [
|
||||
TimeBlock(
|
||||
start: TimeOfDay(hour: 14, minute: 0),
|
||||
end: TimeOfDay(hour: 15, minute: 0),
|
||||
start: const TimeOfDay(hour: 14, minute: 0),
|
||||
end: const TimeOfDay(hour: 15, minute: 0),
|
||||
id: 0,
|
||||
),
|
||||
TimeBlock(
|
||||
start: TimeOfDay(hour: 8, minute: 0),
|
||||
end: TimeOfDay(hour: 9, minute: 0),
|
||||
start: const TimeOfDay(hour: 8, minute: 0),
|
||||
end: const TimeOfDay(hour: 9, minute: 0),
|
||||
id: 1,
|
||||
),
|
||||
TimeBlock(
|
||||
start: TimeOfDay(hour: 9, minute: 15),
|
||||
end: TimeOfDay(hour: 10, minute: 0),
|
||||
start: const TimeOfDay(hour: 9, minute: 15),
|
||||
end: const TimeOfDay(hour: 10, minute: 0),
|
||||
id: 1,
|
||||
),
|
||||
TimeBlock(
|
||||
start: TimeOfDay(hour: 10, minute: 15),
|
||||
end: TimeOfDay(hour: 11, minute: 0),
|
||||
start: const TimeOfDay(hour: 10, minute: 15),
|
||||
end: const TimeOfDay(hour: 11, minute: 0),
|
||||
child: Container(color: Colors.purple, height: 300, width: 50),
|
||||
id: 2,
|
||||
),
|
||||
TimeBlock(
|
||||
start: TimeOfDay(hour: 6, minute: 15),
|
||||
end: TimeOfDay(hour: 7, minute: 0),
|
||||
start: const TimeOfDay(hour: 6, minute: 15),
|
||||
end: const TimeOfDay(hour: 7, minute: 0),
|
||||
child: Container(color: Colors.blue, height: 300, width: 200),
|
||||
id: 2,
|
||||
),
|
||||
TimeBlock(
|
||||
start: TimeOfDay(hour: 18, minute: 0),
|
||||
end: TimeOfDay(hour: 18, minute: 15),
|
||||
child: Text('High Tea'),
|
||||
start: const TimeOfDay(hour: 18, minute: 0),
|
||||
end: const TimeOfDay(hour: 18, minute: 15),
|
||||
child: const Text('High Tea'),
|
||||
id: 10,
|
||||
),
|
||||
TimeBlock(
|
||||
start: TimeOfDay(hour: 18, minute: 0),
|
||||
end: TimeOfDay(hour: 18, minute: 15),
|
||||
child: Text('High Tea'),
|
||||
start: const TimeOfDay(hour: 18, minute: 0),
|
||||
end: const TimeOfDay(hour: 18, minute: 15),
|
||||
child: const Text('High Tea'),
|
||||
id: 10,
|
||||
),
|
||||
TimeBlock(
|
||||
start: TimeOfDay(hour: 18, minute: 0),
|
||||
end: TimeOfDay(hour: 18, minute: 15),
|
||||
child: Text('High Tea'),
|
||||
start: const TimeOfDay(hour: 18, minute: 0),
|
||||
end: const TimeOfDay(hour: 18, minute: 15),
|
||||
child: const Text('High Tea'),
|
||||
id: 10,
|
||||
),
|
||||
TimeBlock(
|
||||
start: TimeOfDay(hour: 18, minute: 0),
|
||||
end: TimeOfDay(hour: 18, minute: 15),
|
||||
child: Text('High Tea'),
|
||||
start: const TimeOfDay(hour: 18, minute: 0),
|
||||
end: const TimeOfDay(hour: 18, minute: 15),
|
||||
child: const Text('High Tea'),
|
||||
id: 0,
|
||||
),
|
||||
];
|
||||
|
@ -88,7 +88,7 @@ class _TimetableDemoState extends State<TimetableDemo> {
|
|||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Grouped'),
|
||||
const Text('Grouped'),
|
||||
Switch(
|
||||
value: _grouped,
|
||||
onChanged: (value) {
|
||||
|
@ -106,7 +106,7 @@ class _TimetableDemoState extends State<TimetableDemo> {
|
|||
timeBlocks: blocks,
|
||||
scrollController: _scrollController,
|
||||
tablePaddingStart: 0,
|
||||
collapseBlocks: true,
|
||||
combineBlocks: true,
|
||||
mergeBlocks: false,
|
||||
)
|
||||
] else ...[
|
||||
|
@ -116,7 +116,7 @@ class _TimetableDemoState extends State<TimetableDemo> {
|
|||
timeBlocks: blocks,
|
||||
scrollController: _scrollController,
|
||||
tablePaddingStart: 0,
|
||||
collapseBlocks: true,
|
||||
combineBlocks: true,
|
||||
mergeBlocks: true,
|
||||
),
|
||||
],
|
||||
|
|
|
@ -1,26 +1,19 @@
|
|||
part of timetable;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:timetable/src/models/time_block.dart';
|
||||
|
||||
/// Combine blocks that have the same id and the same time.
|
||||
List<TimeBlock> collapseBlocks(List<TimeBlock> blocks) {
|
||||
List<TimeBlock> combineBlocksWithId(List<TimeBlock> blocks) {
|
||||
var newBlocks = <TimeBlock>[];
|
||||
var groupedBlocks = <List<TimeBlock>>[];
|
||||
// order blocks by id and collides with another block
|
||||
for (var block in blocks) {
|
||||
// check if the block is already in one of the grouped blocks
|
||||
var found = false;
|
||||
if (block.id == 0) {
|
||||
newBlocks.add(block);
|
||||
continue;
|
||||
}
|
||||
for (var groupedBlock in groupedBlocks) {
|
||||
if (groupedBlock.first.id == block.id &&
|
||||
groupedBlock.first.start == block.start &&
|
||||
groupedBlock.first.end == block.end) {
|
||||
groupedBlock.add(block);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
found = true;
|
||||
} else {
|
||||
found = _checkIfBlockWithIdExists(groupedBlocks, block);
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
if (blocks
|
||||
.where(
|
||||
|
@ -38,16 +31,24 @@ List<TimeBlock> collapseBlocks(List<TimeBlock> blocks) {
|
|||
}
|
||||
}
|
||||
|
||||
_combineGroupedBlocks(groupedBlocks, newBlocks);
|
||||
return newBlocks;
|
||||
}
|
||||
|
||||
void _combineGroupedBlocks(
|
||||
List<List<TimeBlock>> groupedBlocks,
|
||||
List<TimeBlock> newBlocks,
|
||||
) {
|
||||
for (var block in groupedBlocks) {
|
||||
// combine the blocks into one block
|
||||
// calculate the endtime of the combined block
|
||||
var startMinute = block.first.start.minute + block.first.start.hour * 60;
|
||||
var endMinute = block.first.end.minute + block.first.end.hour * 60;
|
||||
var startMinute = block.first.start.minute +
|
||||
block.first.start.hour * Duration.minutesPerHour;
|
||||
var endMinute =
|
||||
block.first.end.minute + block.first.end.hour * Duration.minutesPerHour;
|
||||
var durationMinute = (endMinute - startMinute) * block.length;
|
||||
|
||||
var endTime = TimeOfDay(
|
||||
hour: (startMinute + durationMinute) ~/ 60,
|
||||
minute: (startMinute + durationMinute) % 60,
|
||||
hour: (startMinute + durationMinute) ~/ Duration.minutesPerHour,
|
||||
minute: (startMinute + durationMinute) % Duration.minutesPerHour,
|
||||
);
|
||||
var newBlock = TimeBlock(
|
||||
start: block.first.start,
|
||||
|
@ -61,7 +62,21 @@ List<TimeBlock> collapseBlocks(List<TimeBlock> blocks) {
|
|||
);
|
||||
newBlocks.add(newBlock);
|
||||
}
|
||||
return newBlocks;
|
||||
}
|
||||
|
||||
bool _checkIfBlockWithIdExists(
|
||||
List<List<TimeBlock>> groupedBlocks,
|
||||
TimeBlock block,
|
||||
) {
|
||||
for (var groupedBlock in groupedBlocks) {
|
||||
if (groupedBlock.first.id == block.id &&
|
||||
groupedBlock.first.start == block.start &&
|
||||
groupedBlock.first.end == block.end) {
|
||||
groupedBlock.add(block);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Group blocks with the same id together.
|
||||
|
@ -95,7 +110,6 @@ List<List<TimeBlock>> groupBlocksById(List<TimeBlock> blocks) {
|
|||
/// Nerge blocks that fit below eachother into one column.
|
||||
List<List<TimeBlock>> mergeBlocksInColumns(List<TimeBlock> blocks) {
|
||||
var mergedBlocks = <List<TimeBlock>>[];
|
||||
// try to put blocks in the same column if the time doesn´t collide
|
||||
for (var block in blocks) {
|
||||
var mergeIndex = 0;
|
||||
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
part of timetable;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class TableTheme {
|
||||
/// The [TableTheme] to style the [Table] with. Configure the line, text
|
||||
/// and offsets here.
|
||||
const TableTheme({
|
||||
this.lineColor = const Color(0x809E9E9E),
|
||||
this.lineHeight = 2,
|
||||
this.tableTextOffset = 5,
|
||||
this.lineDashFrequency = 25,
|
||||
this.timeStyle = const TextStyle(),
|
||||
});
|
||||
|
||||
|
@ -13,6 +17,12 @@ class TableTheme {
|
|||
/// The height of the lines.
|
||||
final double lineHeight;
|
||||
|
||||
/// The amount of dashes on the line.
|
||||
final int lineDashFrequency;
|
||||
|
||||
/// Distance between the time text and the line.
|
||||
final double tableTextOffset;
|
||||
|
||||
/// The style of the time text.
|
||||
final TextStyle timeStyle;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
part of timetable;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class TimeBlock {
|
||||
/// The model used for a [Block] in a [TimeTable] which can contain a Widget.
|
||||
TimeBlock({
|
||||
required this.start,
|
||||
required this.end,
|
||||
|
|
|
@ -1,6 +1,17 @@
|
|||
part of timetable;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:timetable/src/block_service.dart';
|
||||
import 'package:timetable/src/models/table_theme.dart';
|
||||
import 'package:timetable/src/models/time_block.dart';
|
||||
import 'package:timetable/src/widgets/block.dart';
|
||||
import 'package:timetable/src/widgets/table.dart' as table;
|
||||
|
||||
class Timetable extends StatefulWidget {
|
||||
/// [Timetable] widget that displays a timetable with [TimeBlock]s.
|
||||
/// The timetable automatically scrolls to the first item.
|
||||
/// A [TableTheme] can be provided to customize the look of the timetable.
|
||||
/// [mergeBlocks] and [combineBlocks] can be used to combine blocks
|
||||
/// and merge columns of blocks when possible.
|
||||
const Timetable({
|
||||
this.timeBlocks = const [],
|
||||
this.scrollController,
|
||||
|
@ -14,7 +25,7 @@ class Timetable extends StatefulWidget {
|
|||
this.tablePaddingEnd = 15,
|
||||
this.theme = const TableTheme(),
|
||||
this.mergeBlocks = false,
|
||||
this.collapseBlocks = true,
|
||||
this.combineBlocks = true,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
|
@ -56,7 +67,7 @@ class Timetable extends StatefulWidget {
|
|||
|
||||
/// Whether or not to collapse blocks in 1 column if they have the same id.
|
||||
/// If blocks have the same id and time they will be combined into one block.
|
||||
final bool collapseBlocks;
|
||||
final bool combineBlocks;
|
||||
|
||||
@override
|
||||
State<Timetable> createState() => _TimetableState();
|
||||
|
@ -83,8 +94,8 @@ class _TimetableState extends State<Timetable> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<TimeBlock> blocks;
|
||||
if (widget.collapseBlocks) {
|
||||
blocks = collapseBlocks(widget.timeBlocks);
|
||||
if (widget.combineBlocks) {
|
||||
blocks = combineBlocksWithId(widget.timeBlocks);
|
||||
} else {
|
||||
blocks = widget.timeBlocks;
|
||||
}
|
||||
|
@ -93,17 +104,16 @@ class _TimetableState extends State<Timetable> {
|
|||
controller: _scrollController,
|
||||
child: Stack(
|
||||
children: [
|
||||
Table(
|
||||
table.Table(
|
||||
startHour: widget.startHour,
|
||||
endHour: widget.endHour,
|
||||
columnHeight: widget.hourHeight,
|
||||
hourHeight: widget.hourHeight,
|
||||
tableOffset: _calculateTableStart(),
|
||||
theme: widget.theme,
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.only(
|
||||
left: _calculateTableTextSize().width +
|
||||
widget.tablePaddingStart +
|
||||
5,
|
||||
left: _calculateTableStart(),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
physics: widget.scrollPhysics ?? const BouncingScrollPhysics(),
|
||||
|
@ -112,7 +122,7 @@ class _TimetableState extends State<Timetable> {
|
|||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.mergeBlocks || widget.collapseBlocks) ...[
|
||||
if (widget.mergeBlocks || widget.combineBlocks) ...[
|
||||
for (var orderedBlocks in (widget.mergeBlocks)
|
||||
? mergeBlocksInColumns(blocks)
|
||||
: groupBlocksById(blocks)) ...[
|
||||
|
@ -132,7 +142,9 @@ class _TimetableState extends State<Timetable> {
|
|||
SizedBox(
|
||||
width: widget.tablePaddingEnd,
|
||||
height: widget.hourHeight *
|
||||
(widget.endHour - widget.startHour + 0.5),
|
||||
(widget.endHour -
|
||||
widget.startHour +
|
||||
0.5), // empty halfhour at the end
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -144,6 +156,12 @@ class _TimetableState extends State<Timetable> {
|
|||
);
|
||||
}
|
||||
|
||||
double _calculateTableStart() {
|
||||
return _calculateTableTextSize().width +
|
||||
widget.tablePaddingStart +
|
||||
widget.theme.tableTextOffset;
|
||||
}
|
||||
|
||||
Widget _showBlock(TimeBlock block) {
|
||||
return Block(
|
||||
start: block.start,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
part of timetable;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Block extends StatelessWidget {
|
||||
/// The [Block] to create a Widget or container in a [TimeTable].
|
||||
const Block({
|
||||
required this.start,
|
||||
required this.end,
|
||||
|
@ -41,24 +42,28 @@ class Block extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: EdgeInsets.only(
|
||||
top:
|
||||
(((start.hour - startHour) * 60) + start.minute) * sizePerMinute() +
|
||||
linePadding,
|
||||
top: (((start.hour - startHour) * Duration.minutesPerHour) +
|
||||
start.minute) *
|
||||
_sizePerMinute() +
|
||||
linePadding,
|
||||
),
|
||||
height: (((end.hour - start.hour) * 60) + end.minute - start.minute) *
|
||||
sizePerMinute(),
|
||||
height: (((end.hour - start.hour) * Duration.minutesPerHour) +
|
||||
end.minute -
|
||||
start.minute) *
|
||||
_sizePerMinute(),
|
||||
child: child ??
|
||||
Container(
|
||||
height:
|
||||
(((end.hour - start.hour) * 60) + end.minute - start.minute) *
|
||||
sizePerMinute(),
|
||||
height: (((end.hour - start.hour) * Duration.minutesPerHour) +
|
||||
end.minute -
|
||||
start.minute) *
|
||||
_sizePerMinute(),
|
||||
width: blockWidth,
|
||||
color: blockColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
double sizePerMinute() {
|
||||
return hourHeight / 60;
|
||||
double _sizePerMinute() {
|
||||
return hourHeight / Duration.minutesPerHour;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,30 @@
|
|||
part of timetable;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:timetable/src/models/table_theme.dart';
|
||||
|
||||
class Table extends StatelessWidget {
|
||||
/// The [Table] to draw an overview of timerange with corresponding hour lines
|
||||
const Table({
|
||||
required this.startHour,
|
||||
required this.endHour,
|
||||
this.columnHeight = 80,
|
||||
this.hourHeight = 80,
|
||||
this.tableOffset = 40,
|
||||
this.theme = const TableTheme(),
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
/// The hour the table starts at.
|
||||
final int startHour;
|
||||
|
||||
/// The hour the table ends at.
|
||||
final int endHour;
|
||||
final double columnHeight;
|
||||
|
||||
/// The height of a single hour in the table.
|
||||
final double hourHeight;
|
||||
|
||||
/// The offset of the table;
|
||||
final double tableOffset;
|
||||
|
||||
/// The theme used by the table.
|
||||
final TableTheme theme;
|
||||
|
||||
@override
|
||||
|
@ -20,7 +33,7 @@ class Table extends StatelessWidget {
|
|||
children: [
|
||||
for (int i = startHour; i <= endHour; i++) ...[
|
||||
SizedBox(
|
||||
height: i == endHour ? columnHeight / 2 : columnHeight,
|
||||
height: i == endHour ? hourHeight / 2 : hourHeight,
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
|
@ -29,8 +42,8 @@ class Table extends StatelessWidget {
|
|||
'${i.toString().padLeft(2, '0')}:00',
|
||||
style: theme.timeStyle,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 5,
|
||||
SizedBox(
|
||||
width: theme.tableTextOffset,
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
|
@ -43,16 +56,17 @@ class Table extends StatelessWidget {
|
|||
if (i != endHour) ...[
|
||||
const Spacer(),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(
|
||||
left: 40,
|
||||
margin: EdgeInsets.only(
|
||||
left: tableOffset,
|
||||
),
|
||||
height: theme.lineHeight,
|
||||
child: Row(
|
||||
children: [
|
||||
for (int i = 0; i < 25; i++) ...[
|
||||
for (int i = 0; i < theme.lineDashFrequency; i++) ...[
|
||||
Container(
|
||||
width:
|
||||
(MediaQuery.of(context).size.width - 40) / 25,
|
||||
width: (MediaQuery.of(context).size.width -
|
||||
tableOffset) /
|
||||
theme.lineDashFrequency,
|
||||
height: theme.lineHeight,
|
||||
color:
|
||||
i.isEven ? theme.lineColor : Colors.transparent,
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
library timetable;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
part 'src/timetable.dart';
|
||||
part 'src/block_service.dart';
|
||||
part 'src/widgets/table.dart';
|
||||
part 'src/models/time_block.dart';
|
||||
part 'src/models/table_theme.dart';
|
||||
part 'src/widgets/block.dart';
|
||||
export 'src/models/table_theme.dart';
|
||||
export 'src/models/time_block.dart';
|
||||
export 'src/timetable.dart';
|
||||
export 'src/widgets/block.dart';
|
||||
export 'src/widgets/table.dart';
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:timetable/src/block_service.dart';
|
||||
import 'package:timetable/timetable.dart';
|
||||
|
||||
void main() {
|
||||
group('test collapseBlocks', () {
|
||||
group('test combineBlocksWithId', () {
|
||||
test('new block creation success', () {
|
||||
//Arrange
|
||||
var blocks = [
|
||||
|
@ -25,7 +26,7 @@ void main() {
|
|||
];
|
||||
|
||||
//Act
|
||||
var result = collapseBlocks(blocks);
|
||||
var result = combineBlocksWithId(blocks);
|
||||
|
||||
//Assert
|
||||
expect(result.length, 2);
|
||||
|
@ -51,7 +52,7 @@ void main() {
|
|||
];
|
||||
|
||||
//Act
|
||||
var result = collapseBlocks(blocks);
|
||||
var result = combineBlocksWithId(blocks);
|
||||
|
||||
//Assert
|
||||
expect(result.length, 2);
|
||||
|
|
Loading…
Reference in a new issue