mirror of
https://github.com/Iconica-Development/flutter_timetable.git
synced 2025-05-18 19:43:43 +02:00
feat: added test for block reordering
This commit is contained in:
parent
e31f284c22
commit
19187e921c
7 changed files with 313 additions and 156 deletions
|
@ -54,6 +54,12 @@ class _TimetableDemoState extends State<TimetableDemo> {
|
||||||
child: Text('High Tea'),
|
child: Text('High Tea'),
|
||||||
id: 10,
|
id: 10,
|
||||||
),
|
),
|
||||||
|
TimeBlock(
|
||||||
|
start: TimeOfDay(hour: 18, minute: 0),
|
||||||
|
end: TimeOfDay(hour: 18, minute: 15),
|
||||||
|
child: Text('High Tea'),
|
||||||
|
id: 0,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
final List<TimeBlock> groupedBlocks = [
|
final List<TimeBlock> groupedBlocks = [
|
||||||
|
@ -102,6 +108,12 @@ class _TimetableDemoState extends State<TimetableDemo> {
|
||||||
child: Text('High Tea'),
|
child: Text('High Tea'),
|
||||||
id: 10,
|
id: 10,
|
||||||
),
|
),
|
||||||
|
TimeBlock(
|
||||||
|
start: TimeOfDay(hour: 18, minute: 0),
|
||||||
|
end: TimeOfDay(hour: 18, minute: 15),
|
||||||
|
child: Text('High Tea'),
|
||||||
|
id: 0,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
115
lib/src/block_service.dart
Normal file
115
lib/src/block_service.dart
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
part of timetable;
|
||||||
|
|
||||||
|
/// Combine blocks that have the same id and the same time.
|
||||||
|
List<TimeBlock> collapseBlocks(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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<List<TimeBlock>> groupBlocksById(List<TimeBlock> blocks) {
|
||||||
|
var groupedBlocks = <List<TimeBlock>>[];
|
||||||
|
var defaultGroup = <TimeBlock>[];
|
||||||
|
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<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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ class Timetable extends StatefulWidget {
|
||||||
this.tablePaddingEnd = 15,
|
this.tablePaddingEnd = 15,
|
||||||
this.theme = const TableTheme(),
|
this.theme = const TableTheme(),
|
||||||
this.mergeBlocks = false,
|
this.mergeBlocks = false,
|
||||||
this.collapseBlocks = false,
|
this.collapseBlocks = true,
|
||||||
Key? key,
|
Key? key,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@ -55,6 +55,7 @@ class Timetable extends StatefulWidget {
|
||||||
final bool mergeBlocks;
|
final bool mergeBlocks;
|
||||||
|
|
||||||
/// Whether or not to collapse blocks in 1 column if they have the same id.
|
/// 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 collapseBlocks;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -81,7 +82,12 @@ class _TimetableState extends State<Timetable> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var blocks = _collapseBlocks(widget.timeBlocks);
|
List<TimeBlock> blocks;
|
||||||
|
if (widget.collapseBlocks) {
|
||||||
|
blocks = collapseBlocks(widget.timeBlocks);
|
||||||
|
} else {
|
||||||
|
blocks = widget.timeBlocks;
|
||||||
|
}
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
physics: widget.scrollPhysics ?? const BouncingScrollPhysics(),
|
physics: widget.scrollPhysics ?? const BouncingScrollPhysics(),
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
|
@ -106,54 +112,21 @@ class _TimetableState extends State<Timetable> {
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
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) ...[
|
for (var block in blocks) ...[
|
||||||
Block(
|
_showBlock(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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
SizedBox(
|
SizedBox(
|
||||||
|
@ -171,111 +144,16 @@ class _TimetableState extends State<Timetable> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Copmbine blocks that have the same id and the same time.
|
Widget _showBlock(TimeBlock block) {
|
||||||
List<TimeBlock> _collapseBlocks(List<TimeBlock> blocks) {
|
return Block(
|
||||||
var newBlocks = <TimeBlock>[];
|
start: block.start,
|
||||||
var groupedBlocks = <List<TimeBlock>>[];
|
end: block.end,
|
||||||
// order blocks by id and collides with another block
|
startHour: widget.startHour,
|
||||||
for (var block in blocks) {
|
hourHeight: widget.hourHeight,
|
||||||
// check if the block is already in one of the grouped blocks
|
blockWidth: widget.blockWidth,
|
||||||
var found = false;
|
blockColor: widget.blockColor,
|
||||||
for (var groupedBlock in groupedBlocks) {
|
child: block.child,
|
||||||
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<List<TimeBlock>> _groupBlocksById(List<TimeBlock> blocks) {
|
|
||||||
var groupedBlocks = <List<TimeBlock>>[];
|
|
||||||
var defaultGroup = <TimeBlock>[];
|
|
||||||
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<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;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _scrollToFirstBlock() {
|
void _scrollToFirstBlock() {
|
||||||
|
|
|
@ -4,7 +4,8 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
|
|
||||||
part 'src/timetable.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/time_block.dart';
|
||||||
part 'src/models/table_theme.dart';
|
part 'src/models/table_theme.dart';
|
||||||
part 'src/block.dart';
|
part 'src/widgets/block.dart';
|
||||||
|
|
151
test/block_service_test.dart
Normal file
151
test/block_service_test.dart
Normal file
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in a new issue