diff --git a/example/lib/main.dart b/example/lib/main.dart index 34cdb4c..b0255c8 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -54,6 +54,12 @@ class _TimetableDemoState extends State { child: Text('High Tea'), id: 10, ), + TimeBlock( + start: TimeOfDay(hour: 18, minute: 0), + end: TimeOfDay(hour: 18, minute: 15), + child: Text('High Tea'), + id: 0, + ), ]; final List groupedBlocks = [ @@ -102,6 +108,12 @@ class _TimetableDemoState extends State { child: Text('High Tea'), id: 10, ), + TimeBlock( + start: TimeOfDay(hour: 18, minute: 0), + end: TimeOfDay(hour: 18, minute: 15), + child: Text('High Tea'), + id: 0, + ), ]; @override diff --git a/lib/src/block_service.dart b/lib/src/block_service.dart new file mode 100644 index 0000000..37131f0 --- /dev/null +++ b/lib/src/block_service.dart @@ -0,0 +1,115 @@ +part of timetable; + +/// Combine blocks that have the same id and the same time. +List collapseBlocks(List blocks) { + var newBlocks = []; + var groupedBlocks = >[]; + // 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; + } + } + if (!found) { + if (blocks + .where( + (b) => + b != block && + b.id == block.id && + b.start == block.start && + b.end == block.end, + ) + .isNotEmpty) { + groupedBlocks.add([block]); + } else { + newBlocks.add(block); + } + } + } + + 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 durationMinute = (endMinute - startMinute) * block.length; + + var endTime = TimeOfDay( + hour: (startMinute + durationMinute) ~/ 60, + minute: (startMinute + durationMinute) % 60, + ); + var newBlock = TimeBlock( + start: block.first.start, + end: endTime, + id: block.first.id, + child: Column( + children: [ + for (var b in block) ...[b.child ?? Container()], + ], + ), + ); + newBlocks.add(newBlock); + } + return newBlocks; +} + +/// Group blocks with the same id together. +/// Items in the same group will be displayed in the same column. +List> groupBlocksById(List blocks) { + var groupedBlocks = >[]; + var defaultGroup = []; + for (var block in blocks) { + var found = false; + if (block.id == 0) { + defaultGroup.add(block); + } else { + for (var groupedBlock in groupedBlocks) { + if (groupedBlock.first.id == block.id) { + groupedBlock.add(block); + found = true; + break; + } + } + if (!found) { + groupedBlocks.add([block]); + } + } + } + for (var block in defaultGroup) { + groupedBlocks.add([block]); + } + return groupedBlocks; +} + +/// Nerge blocks that fit below eachother into one column. +List> mergeBlocksInColumns(List blocks) { + var mergedBlocks = >[]; + // try to put blocks in the same column if the time doesn´t collide + for (var block in blocks) { + var mergeIndex = 0; + + for (var mergedBlock in mergedBlocks) { + if (!mergedBlock.any((b) => b.collidesWith(block))) { + mergedBlock.add(block); + break; + } else { + mergeIndex++; + } + } + if (mergedBlocks.length == mergeIndex) { + mergedBlocks.add([block]); + } + } + return mergedBlocks; +} diff --git a/lib/src/timetable.dart b/lib/src/timetable.dart index ce4f535..a161b30 100644 --- a/lib/src/timetable.dart +++ b/lib/src/timetable.dart @@ -14,7 +14,7 @@ class Timetable extends StatefulWidget { this.tablePaddingEnd = 15, this.theme = const TableTheme(), this.mergeBlocks = false, - this.collapseBlocks = false, + this.collapseBlocks = true, Key? key, }) : super(key: key); @@ -55,6 +55,7 @@ class Timetable extends StatefulWidget { final bool mergeBlocks; /// 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; @override @@ -81,7 +82,12 @@ class _TimetableState extends State { @override Widget build(BuildContext context) { - var blocks = _collapseBlocks(widget.timeBlocks); + List blocks; + if (widget.collapseBlocks) { + blocks = collapseBlocks(widget.timeBlocks); + } else { + blocks = widget.timeBlocks; + } return SingleChildScrollView( physics: widget.scrollPhysics ?? const BouncingScrollPhysics(), controller: _scrollController, @@ -106,54 +112,21 @@ class _TimetableState extends State { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (!widget.mergeBlocks && !widget.collapseBlocks) ...[ + if (widget.mergeBlocks || widget.collapseBlocks) ...[ + for (var orderedBlocks in (widget.mergeBlocks) + ? mergeBlocksInColumns(blocks) + : groupBlocksById(blocks)) ...[ + Stack( + children: [ + for (var block in orderedBlocks) ...[ + _showBlock(block), + ], + ], + ), + ], + ] else ...[ for (var block in blocks) ...[ - Block( - start: block.start, - end: block.end, - startHour: widget.startHour, - hourHeight: widget.hourHeight, - blockWidth: widget.blockWidth, - blockColor: widget.blockColor, - child: block.child, - ), - ], - ] else if (widget.mergeBlocks) ...[ - for (var mergedBlocks - in _mergeBlocksInColumns(blocks)) ...[ - Stack( - children: [ - for (var block in mergedBlocks) ...[ - Block( - start: block.start, - end: block.end, - startHour: widget.startHour, - hourHeight: widget.hourHeight, - blockWidth: widget.blockWidth, - blockColor: widget.blockColor, - child: block.child, - ), - ], - ], - ), - ], - ] else if (widget.collapseBlocks) ...[ - for (var groupedBlocks in _groupBlocksById(blocks)) ...[ - Stack( - children: [ - for (var block in groupedBlocks) ...[ - Block( - start: block.start, - end: block.end, - startHour: widget.startHour, - hourHeight: widget.hourHeight, - blockWidth: widget.blockWidth, - blockColor: widget.blockColor, - child: block.child, - ), - ], - ], - ), + _showBlock(block), ], ], SizedBox( @@ -171,111 +144,16 @@ class _TimetableState extends State { ); } - /// Copmbine blocks that have the same id and the same time. - List _collapseBlocks(List blocks) { - var newBlocks = []; - var groupedBlocks = >[]; - // 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; - 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; - } - } - if (!found) { - if (blocks - .where( - (b) => - b != block && - b.id == block.id && - b.start == block.start && - b.end == block.end, - ) - .isNotEmpty) { - groupedBlocks.add([block]); - } else { - newBlocks.add(block); - } - } - } - // 8.10 8.40 8.55 - // - 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 durationMinute = (endMinute - startMinute) * block.length; - - var endTime = TimeOfDay( - hour: (startMinute + durationMinute) ~/ 60, - minute: (startMinute + durationMinute) % 60, - ); - var newBlock = TimeBlock( - start: block.first.start, - end: endTime, - child: Column( - children: [ - for (var b in block) ...[b.child ?? Container()], - ], - ), - ); - newBlocks.add(newBlock); - } - return newBlocks; - } - - List> _groupBlocksById(List blocks) { - var groupedBlocks = >[]; - var defaultGroup = []; - for (var block in blocks) { - var found = false; - if (block.id == 0) { - defaultGroup.add(block); - } else { - for (var groupedBlock in groupedBlocks) { - if (groupedBlock.first.id == block.id) { - groupedBlock.add(block); - found = true; - break; - } - } - if (!found) { - groupedBlocks.add([block]); - } - } - } - for (var block in defaultGroup) { - groupedBlocks.add([block]); - } - return groupedBlocks; - } - - List> _mergeBlocksInColumns(List blocks) { - var mergedBlocks = >[]; - // try to put blocks in the same column if the time doesn´t collide - for (var block in blocks) { - var mergeIndex = 0; - - for (var mergedBlock in mergedBlocks) { - if (!mergedBlock.any((b) => b.collidesWith(block))) { - mergedBlock.add(block); - break; - } else { - mergeIndex++; - } - } - if (mergedBlocks.length == mergeIndex) { - mergedBlocks.add([block]); - } - } - return mergedBlocks; + Widget _showBlock(TimeBlock block) { + return Block( + start: block.start, + end: block.end, + startHour: widget.startHour, + hourHeight: widget.hourHeight, + blockWidth: widget.blockWidth, + blockColor: widget.blockColor, + child: block.child, + ); } void _scrollToFirstBlock() { diff --git a/lib/src/block.dart b/lib/src/widgets/block.dart similarity index 100% rename from lib/src/block.dart rename to lib/src/widgets/block.dart diff --git a/lib/src/table.dart b/lib/src/widgets/table.dart similarity index 100% rename from lib/src/table.dart rename to lib/src/widgets/table.dart diff --git a/lib/timetable.dart b/lib/timetable.dart index 60a17d3..d7494b1 100644 --- a/lib/timetable.dart +++ b/lib/timetable.dart @@ -4,7 +4,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; part 'src/timetable.dart'; -part 'src/table.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/block.dart'; +part 'src/widgets/block.dart'; diff --git a/test/block_service_test.dart b/test/block_service_test.dart new file mode 100644 index 0000000..ac4e61e --- /dev/null +++ b/test/block_service_test.dart @@ -0,0 +1,151 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:timetable/timetable.dart'; + +void main() { + group('test collapseBlocks', () { + test('new block creation success', () { + //Arrange + var blocks = [ + TimeBlock( + start: const TimeOfDay(hour: 2, minute: 0), + end: const TimeOfDay(hour: 2, minute: 15), + id: 5, + ), + TimeBlock( + start: const TimeOfDay(hour: 2, minute: 0), + end: const TimeOfDay(hour: 2, minute: 15), + id: 5, + ), + TimeBlock( + start: const TimeOfDay(hour: 2, minute: 15), + end: const TimeOfDay(hour: 2, minute: 30), + id: 6, + ), + ]; + + //Act + var result = collapseBlocks(blocks); + + //Assert + expect(result.length, 2); + expect( + result.firstWhere((element) => element.id == 5).end, + const TimeOfDay(hour: 2, minute: 30), + ); + }); + + test('elements without id ignored', () { + //Arrange + var blocks = [ + TimeBlock( + start: const TimeOfDay(hour: 2, minute: 0), + end: const TimeOfDay(hour: 2, minute: 15), + id: 0, // default id is 0 + ), + TimeBlock( + start: const TimeOfDay(hour: 2, minute: 0), + end: const TimeOfDay(hour: 2, minute: 15), + id: 0, + ), + ]; + + //Act + var result = collapseBlocks(blocks); + + //Assert + expect(result.length, 2); + expect(result.first.end, const TimeOfDay(hour: 2, minute: 15)); + }); + }); + + group('test groupBlocksById', () { + test('groupBlocksById success', () { + //Arrange + var blocks = [ + TimeBlock( + start: const TimeOfDay(hour: 2, minute: 0), + end: const TimeOfDay(hour: 2, minute: 15), + id: 5, + ), + TimeBlock( + start: const TimeOfDay(hour: 2, minute: 0), + end: const TimeOfDay(hour: 2, minute: 25), + id: 5, + ), + TimeBlock( + start: const TimeOfDay(hour: 2, minute: 20), + end: const TimeOfDay(hour: 2, minute: 30), + id: 6, + ), + ]; + + //Act + var result = groupBlocksById(blocks); + + //Assert + expect(result.length, 2); + }); + + test('groupBlocksById id 0 ignored', () { + // Arrange + var blocks = [ + TimeBlock( + start: const TimeOfDay(hour: 2, minute: 0), + end: const TimeOfDay(hour: 2, minute: 15), + id: 0, + ), + TimeBlock( + start: const TimeOfDay(hour: 2, minute: 0), + end: const TimeOfDay(hour: 2, minute: 25), + id: 0, + ), + TimeBlock( + start: const TimeOfDay(hour: 2, minute: 20), + end: const TimeOfDay(hour: 2, minute: 30), + id: 0, + ), + ]; + + //Act + var result = groupBlocksById(blocks); + + //Assert + expect(result.length, 3); + }); + }); + + group('test mergeBlocksInColumns', () { + test('mergeBlocksInColumns success', () { + //Arrange + var blocks = [ + TimeBlock( + start: const TimeOfDay(hour: 2, minute: 0), + end: const TimeOfDay(hour: 6, minute: 15), + id: 1, + ), + TimeBlock( + start: const TimeOfDay(hour: 8, minute: 0), + end: const TimeOfDay(hour: 10, minute: 25), + id: 2, + ), + TimeBlock( + start: const TimeOfDay(hour: 12, minute: 20), + end: const TimeOfDay(hour: 14, minute: 30), + id: 3, + ), + TimeBlock( + start: const TimeOfDay(hour: 13, minute: 20), + end: const TimeOfDay(hour: 14, minute: 15), + id: 4, + ), + ]; + + //Act + var result = mergeBlocksInColumns(blocks); + + //Assert + expect(result.length, 2); + }); + }); +}