diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 9625e10..7c56964 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 11.0 + 12.0 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 572b8d6..9926ec5 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -168,7 +168,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C8080294A63A400263BE5 = { @@ -344,7 +344,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -471,7 +471,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -520,7 +520,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index e42adcb..8e3ca5d 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ { - get width => + double get width => Platform.isWindows || Platform.isLinux || Platform.isMacOS ? 350 : 250; + final _controller = KanbanBoardController(); @override Widget build(BuildContext context) { + final kanbanGroups = List.generate( + kanbanData.length, + (index) => KanbanBoardGroup( + id: kanbanData.keys.elementAt(index), + name: kanbanData.keys.elementAt(index), + items: (kanbanData.values.elementAt(index)['items'] as List) + .map( + (e) => KanbanCardImpl( + itemId: index.toString(), + title: e['title'], + date: e['date'], + avatar: persons[ + kanbanData.values.elementAt(index)['items'].indexOf(e) % 4], + completedTasks: + int.parse(e['tasks'].toString().split('/').first), + totalTasks: int.parse(e['tasks'].toString().split('/').last), + ), + ) + .toList(), + ), + ); return Scaffold( backgroundColor: const Color.fromRGBO(249, 244, 240, 1), body: SafeArea( @@ -36,60 +58,74 @@ class _BoardBuilderState extends State { ), Expanded( child: KanbanBoard( - List.generate(kanbanData.length, (index) { - final element = kanbanData.values.elementAt(index); - return BoardListsData( - backgroundColor: const Color.fromRGBO(249, 244, 240, 1), - width: width, - footer: const ListFooter(), - headerBackgroundColor: - const Color.fromRGBO(249, 244, 240, 1), - header: ListHeader( - title: kanbanData.keys.elementAt(index), - stateColor: element['color'], + controller: _controller, + groupHeaderBuilder: (context, groupId) => ListHeader( + updateBoard: _updateBoard, + controller: _controller, + stateColor: kanbanData[groupId]!['color'], + title: groupId, ), - items: List.generate(element['items'].length, (index) { - int totalTasks = int.parse(element['items'][index] - ['tasks'] - .toString() - .split('/') - .last); - int completedTasks = int.parse(element['items'][index] - ['tasks'] - .toString() - .split('/') - .first); - - return KanbanCard( - title: element['items'][index]['title'], - completedTasks: completedTasks, - totalTasks: totalTasks, - date: element['items'][index]['date'], - tasks: element['items'][index]['tasks'], - avatar: persons[index % 4], - ); - })); - }), - onItemLongPress: (cardIndex, listIndex) {}, - onItemReorder: - (oldCardIndex, newCardIndex, oldListIndex, newListIndex) {}, - onListLongPress: (listIndex) {}, - onListReorder: (oldListIndex, newListIndex) {}, - onItemTap: (cardIndex, listIndex) {}, - onListTap: (listIndex) {}, - onListRename: (oldName, newName) {}, - backgroundColor: const Color.fromRGBO(249, 244, 240, 1), - displacementY: 124, - displacementX: 100, - textStyle: const TextStyle( - fontSize: 18, - color: Colors.black, - fontWeight: FontWeight.w500), - ), - ), + groupFooterBuilder: (context, groupId) => const ListFooter(), + boardDecoration: const BoxDecoration( + color: Color.fromRGBO(249, 244, 240, 1), + ), + groupDecoration: const BoxDecoration( + color: Color.fromRGBO(249, 244, 240, 1), + ), + groupConstraints: BoxConstraints( + minWidth: width, maxWidth: width, minHeight: 300), + itemGhost: DottedBorder( + child: const Center( + child: Text( + "Drop your task here", + style: + TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + )), + ), + groups: kanbanGroups, + groupItemBuilder: (context, groupId, itemIndex) { + final groupItem = kanbanGroups + .firstWhere((element) => element.id == groupId) + .items + .elementAt(itemIndex); + return GroupCard( + title: groupItem.title, + completedTasks: groupItem.completedTasks, + totalTasks: groupItem.totalTasks, + date: groupItem.date, + tasks: groupItem.totalTasks.toString(), + avatar: persons[itemIndex % 4], + ); + }), + ) ], ), ), ); } + + void _updateBoard() { + setState(() {}); + } +} + +class KanbanCardImpl extends KanbanBoardGroupItem { + final String itemId; + final String title; + final String date; + final String avatar; + final int completedTasks; + final int totalTasks; + + KanbanCardImpl({ + required this.itemId, + required this.title, + required this.date, + required this.avatar, + required this.completedTasks, + required this.totalTasks, + }); + + @override + String get id => itemId; } diff --git a/example/lib/kanban_data.dart b/example/lib/kanban_data.dart index 4e30175..ada4f4b 100644 --- a/example/lib/kanban_data.dart +++ b/example/lib/kanban_data.dart @@ -2,10 +2,10 @@ import 'package:flutter/material.dart'; Map kanbanData = { 'Blocked': { - 'color': const Color.fromRGBO(239, 147, 148, 1), + 'color': const Color.fromARGB(255, 0, 0, 0), 'items': [ { - 'title': 'Making A New Trend In Poster', + 'title': 'Creative Outdoor Ads', 'date': '17 Dec 2022', 'tasks': '30/48' }, @@ -39,9 +39,48 @@ Map kanbanData = { { 'title': 'Advertising Outdoors', 'date': '17 Dec 2022', - 'tasks': '53/70' + 'tasks': '53/70', }, {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, + {'title': 'Create Remarkable', 'date': '17 Nov 2022', 'tasks': '15/56'}, { 'title': 'Manufacturing Equipment', 'date': '17 Dec 2022', @@ -84,11 +123,81 @@ Map kanbanData = { }, ] }, + 'Testing': { + 'color': const Color.fromARGB(255, 235, 235, 148), + 'items': [ + { + 'title': 'Creative Outdoor Ads', + 'date': '23 Dec 2022', + 'tasks': '20/20' + }, + { + 'title': 'Promotional Advertising Speciality', + 'date': '17 Nov 2022', + 'tasks': '15/15' + }, + { + 'title': 'Search Engine OPtimization', + 'date': '22 Oct 2022', + 'tasks': '67/67' + }, + ] + }, + 'Review': { + 'color': const Color.fromARGB(255, 250, 234, 89), + 'items': [ + { + 'title': 'Creative Outdoor Ads', + 'date': '23 Dec 2022', + 'tasks': '20/20' + }, + { + 'title': 'Promotional Advertising Speciality', + 'date': '17 Nov 2022', + 'tasks': '15/15' + }, + { + 'title': 'Search Engine OPtimization', + 'date': '22 Oct 2022', + 'tasks': '67/67' + }, + ] + }, + 'Cancelled': { + 'color': const Color.fromARGB(255, 248, 41, 30), + 'items': [ + { + 'title': 'Creative Outdoor Ads', + 'date': '23 Dec 2022', + 'tasks': '20/20' + }, + ] + }, + 'Production': { + 'color': const Color.fromARGB(255, 0, 248, 58), + 'items': [ + { + 'title': 'Creative Outdoor Ads', + 'date': '23 Dec 2022', + 'tasks': '20/20' + }, + ] + }, + 'Staging': { + 'color': const Color.fromARGB(255, 8, 61, 232), + 'items': [ + { + 'title': 'Creative Outdoor Ads', + 'date': '23 Dec 2022', + 'tasks': '20/20' + }, + ] + }, }; - List persons = [ - 'assets/person1.jpg', - 'assets/person2.jpg', - 'assets/person3.jpg', - 'assets/person4.jpg', - ]; +List persons = [ + 'assets/person1.jpg', + 'assets/person2.jpg', + 'assets/person3.jpg', + 'assets/person4.jpg', +]; diff --git a/example/lib/models/group_more_action.dart b/example/lib/models/group_more_action.dart new file mode 100644 index 0000000..9a57a4b --- /dev/null +++ b/example/lib/models/group_more_action.dart @@ -0,0 +1,9 @@ +enum GroupMoreAction { + editTitle('Edit title'), + createNewTask('Create new task'), + deleteGroup('Delete group'); + + final String label; + + const GroupMoreAction(this.label); +} diff --git a/example/lib/widgets/board_header.dart b/example/lib/widgets/board_header.dart index b92b64a..5a05e79 100644 --- a/example/lib/widgets/board_header.dart +++ b/example/lib/widgets/board_header.dart @@ -1,5 +1,4 @@ import 'dart:io'; - import 'package:flutter/material.dart'; class BoardHeader extends StatelessWidget { @@ -52,11 +51,14 @@ class BoardHeader extends StatelessWidget { child: const Row( children: [ Icon(Icons.add_circle_outlined), - Text("Create new", - style: TextStyle( - fontSize: 14, - color: Colors.black, - fontWeight: FontWeight.bold)), + Text( + "Create new", + style: TextStyle( + fontSize: 14, + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), ], ), ), diff --git a/example/lib/widgets/card.dart b/example/lib/widgets/group_card.dart similarity index 93% rename from example/lib/widgets/card.dart rename to example/lib/widgets/group_card.dart index 12a71a1..84123b6 100644 --- a/example/lib/widgets/card.dart +++ b/example/lib/widgets/group_card.dart @@ -1,12 +1,14 @@ import 'package:flutter/material.dart'; -class KanbanCard extends StatelessWidget { - const KanbanCard( +class GroupCard extends StatelessWidget { + const GroupCard( {super.key, required this.title, required this.completedTasks, required this.totalTasks, - required this.date, required this.tasks, required this.avatar}); + required this.date, + required this.tasks, + required this.avatar}); final String title; final int completedTasks; final int totalTasks; @@ -89,8 +91,7 @@ class KanbanCard extends StatelessWidget { width: 25, decoration: BoxDecoration( image: DecorationImage( - fit: BoxFit.cover, - image: Image.asset(avatar).image), + fit: BoxFit.cover, image: Image.asset(avatar).image), color: const Color.fromRGBO(174, 122, 255, 1), borderRadius: BorderRadius.circular(30)), ) diff --git a/example/lib/widgets/list_header.dart b/example/lib/widgets/list_header.dart index 20e3ef5..e117012 100644 --- a/example/lib/widgets/list_header.dart +++ b/example/lib/widgets/list_header.dart @@ -1,9 +1,19 @@ import 'package:flutter/material.dart'; +import 'package:kanban_board/kanban_board.dart'; class ListHeader extends StatelessWidget { - const ListHeader({super.key, required this.title, required this.stateColor}); + const ListHeader({ + super.key, + required this.controller, + required this.title, + required this.stateColor, + required this.updateBoard, + }); final String title; final Color stateColor; + final KanbanBoardController controller; + final VoidCallback updateBoard; + @override Widget build(BuildContext context) { return Container( @@ -23,20 +33,6 @@ class ListHeader extends StatelessWidget { color: Colors.black, fontWeight: FontWeight.bold)), const Spacer(), - Container( - height: 25, - width: 25, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(2), - border: Border.all( - color: Colors.black, - )), - child: const Icon( - Icons.more_horiz, - size: 16, - color: Colors.black, - ), - ) ], ), ); diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj index 6a9cc9c..a88578a 100644 --- a/example/macos/Runner.xcodeproj/project.pbxproj +++ b/example/macos/Runner.xcodeproj/project.pbxproj @@ -227,7 +227,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C80D4294CF70F00263BE5 = { diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index feac689..c8582ae 100644 --- a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ Bool { return true diff --git a/example/pubspec.lock b/example/pubspec.lock index 64d6dc8..70e3c60 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -37,10 +37,18 @@ packages: dependency: transitive description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.18.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 + url: "https://pub.dev" + source: hosted + version: "3.0.5" cupertino_icons: dependency: "direct main" description: @@ -65,6 +73,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" flutter: dependency: "direct main" description: flutter @@ -91,14 +107,6 @@ packages: description: flutter source: sdk version: "0.0.0" - js: - dependency: transitive - description: - name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 - url: "https://pub.dev" - source: hosted - version: "0.6.7" kanban_board: dependency: "direct main" description: @@ -106,6 +114,30 @@ packages: relative: true source: path version: "0.1.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + url: "https://pub.dev" + source: hosted + version: "10.0.5" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" lints: dependency: transitive description: @@ -118,34 +150,34 @@ packages: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.15.0" path: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_drawing: dependency: transitive description: @@ -179,18 +211,26 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "7.0.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" state_notifier: dependency: transitive description: @@ -203,10 +243,10 @@ packages: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" string_scanner: dependency: transitive description: @@ -227,10 +267,26 @@ packages: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + uuid: + dependency: transitive + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "4.5.1" vector_math: dependency: transitive description: @@ -239,6 +295,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + url: "https://pub.dev" + source: hosted + version: "14.2.5" sdks: - dart: ">=3.0.5 <4.0.0" - flutter: ">=3.0.0" + dart: ">=3.4.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/lib/Provider/board_list_provider.dart b/lib/Provider/board_list_provider.dart deleted file mode 100644 index 21b6d71..0000000 --- a/lib/Provider/board_list_provider.dart +++ /dev/null @@ -1,282 +0,0 @@ -import 'dart:developer'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:kanban_board/constants.dart'; -import 'package:kanban_board/draggable/draggable_state.dart'; -import '../custom/list_item.dart'; -import '../custom/text_field.dart'; -import '../models/item_state.dart'; -import 'provider_list.dart'; - -class BoardListProvider extends ChangeNotifier { - BoardListProvider(ChangeNotifierProviderRef this.ref); - Ref ref; - var scrolling = false; - var scrollingUp = false; - var scrollingDown = false; - var newList = false; - - void calculateSizePosition( - {required int listIndex, - required BuildContext context, - required VoidCallback setstate}) { - var prov = ref.read(ProviderList.boardProvider); - prov.board.lists[listIndex].context = context; - var box = context.findRenderObject() as RenderBox; - var location = box.localToGlobal(Offset.zero); - prov.board.lists[listIndex].x = - location.dx - prov.board.displacementX! - 10; - prov.board.lists[listIndex].setState = setstate; - prov.board.lists[listIndex].y = - location.dy - prov.board.displacementY! + 24; - prov.board.lists[listIndex].width ??= box.size.width; - prov.board.lists[listIndex].height ??= box.size.height; - } - - Future addNewCard({required String position, required int listIndex}) async { - var prov = ref.read(ProviderList.boardProvider); - final cardState = prov.newCardState; - if (cardState.isFocused == true) { - ref.read(ProviderList.cardProvider).saveNewCard(); - } - - var scroll = prov.board.lists[listIndex].scrollController; - - // log("MAX EXTENT =${scroll.position.maxScrollExtent}"); - - prov.board.lists[listIndex].items.insert( - position == "TOP" ? 0 : prov.board.lists[listIndex].items.length, - ListItem( - child: Container( - width: prov.board.lists[listIndex].width, - color: Colors.white, - margin: const EdgeInsets.only(bottom: 10), - child: const TField()), - listIndex: listIndex, - isNew: true, - index: prov.board.lists[listIndex].items.length, - prevChild: Container( - width: prov.board.lists[listIndex].width, - color: Colors.white, - margin: const EdgeInsets.only(bottom: 10), - padding: const EdgeInsets.all(10), - child: const TField()), - )); - position == "TOP" ? await scrollToMin(scroll) : scrollToMax(scroll); - cardState.listIndex = listIndex; - cardState.isFocused = true; - cardState.cardIndex = - position == "TOP" ? 0 : prov.board.lists[listIndex].items.length - 1; - prov.board.lists[listIndex].setState!(); - } - - void onListLongpress( - {required int listIndex, - required BuildContext context, - required VoidCallback setstate}) { - var prov = ref.read(ProviderList.boardProvider); - final draggableProv = ref.read(ProviderList.draggableNotifier.notifier); - for (var element in prov.board.lists) { - if (element.context == null) break; - var of = (element.context!.findRenderObject() as RenderBox) - .localToGlobal(Offset.zero); - element.x = of.dx; - element.width = element.context!.size!.width - LIST_GAP; - element.height = element.context!.size!.height; - element.y = of.dy; - } - var box = context.findRenderObject() as RenderBox; - var location = box.localToGlobal(Offset.zero); - prov.updateValue( - dx: location.dx - prov.board.displacementX!, - dy: location.dy - prov.board.displacementY!); - - prov.board.dragItemIndex = null; - prov.board.dragItemOfListIndex = listIndex; - prov.draggedItemState = DraggedItemState( - child: Container( - width: box.size.width - LIST_GAP, - height: box.size.height, - color: prov.board.lists[listIndex].backgroundColor, - child: Column(children: [ - prov.board.lists[listIndex].header ?? - Container( - padding: const EdgeInsets.only(left: 15, bottom: 10), - alignment: Alignment.centerLeft, - child: Text( - prov.board.lists[listIndex].title, - style: const TextStyle( - fontSize: 20, - color: Colors.black, - fontWeight: FontWeight.bold), - ), - ), - Expanded( - child: MediaQuery.removePadding( - context: context, - removeTop: true, - child: ListView.builder( - physics: const ClampingScrollPhysics(), - controller: null, - itemCount: prov.board.lists[listIndex].items.length, - shrinkWrap: true, - itemBuilder: (ctx, index) { - return Item( - color: prov.board.lists[listIndex].items[index] - .backgroundColor ?? - Colors.grey.shade200, - itemIndex: index, - listIndex: listIndex, - ); - }, - - // itemCount: prov.items.length, - ), - ), - ), - ]), - ), - listIndex: listIndex, - itemIndex: null, - height: box.size.height, - width: box.size.width, - x: location.dx - prov.board.displacementX!, - y: location.dy - prov.board.displacementY!); - prov.draggedItemState!.setState = () => setstate; - prov.board.dragItemIndex = null; - draggableProv.setDraggableType(DraggableType.list); - prov.board.dragItemOfListIndex = listIndex; - setstate(); - } - - Future scrollToMax(ScrollController controller) async { - if (controller.position.pixels == controller.position.maxScrollExtent) { - return; - } - - //log(controller.position.extentAfter.toString()); - await controller.animateTo( - controller.position.pixels + controller.position.extentAfter, - duration: Duration( - milliseconds: (int.parse(controller.position.extentAfter - .toString() - .substring(0, 3) - .split('.') - .first))), - curve: Curves.linear, - ); - scrollToMax(controller); - } - - Future scrollToMin(ScrollController controller) async { - if (controller.position.pixels == controller.position.minScrollExtent) { - return; - } - - log(controller.position.extentBefore.toString()); - await controller.animateTo( - controller.position.pixels - controller.position.extentBefore, - duration: Duration( - milliseconds: (int.parse(controller.position.extentBefore - .toString() - .substring(0, 3) - .split('.') - .first))), - curve: Curves.linear, - ); - scrollToMin(controller); - } - - void maybeListScroll() async { - var prov = ref.read(ProviderList.boardProvider); - final draggableProv = ref.read(ProviderList.draggableNotifier); - if (!draggableProv.isCardDragged || scrolling) { - return; - } - var controller = - prov.board.lists[prov.board.dragItemOfListIndex!].scrollController; - if (controller.offset < controller.position.maxScrollExtent && - prov.valueNotifier.value.dy > - controller.position.viewportDimension - 50) { - scrolling = true; - scrollingDown = true; - if (prov.board.listScrollConfig == null) { - await controller.animateTo(controller.offset + 45, - duration: const Duration(milliseconds: 250), curve: Curves.linear); - } else { - await controller.animateTo( - prov.board.listScrollConfig!.offset + controller.offset, - duration: prov.board.listScrollConfig!.duration, - curve: prov.board.listScrollConfig!.curve); - } - scrolling = false; - scrollingDown = false; - - maybeListScroll(); - } else if (controller.offset > 0 && prov.valueNotifier.value.dy < 100) { - scrolling = true; - scrollingUp = true; - if (prov.board.listScrollConfig == null) { - await controller.animateTo(controller.offset - 45, - duration: const Duration(milliseconds: 250), curve: Curves.linear); - } else { - await controller.animateTo( - controller.offset - prov.board.listScrollConfig!.offset, - duration: prov.board.listScrollConfig!.duration, - curve: prov.board.listScrollConfig!.curve); - } - scrolling = false; - scrollingUp = false; - maybeListScroll(); - } else { - return; - } - } - - void moveListRight() { - var prov = ref.read(ProviderList.boardProvider); - if (prov.draggedItemState!.listIndex == prov.board.lists.length - 1) { - return; - } - if (prov.valueNotifier.value.dx + - prov.board.lists[prov.draggedItemState!.listIndex!].width! / 2 < - prov.board.lists[prov.draggedItemState!.listIndex! + 1].x!) { - return; - } - // dev.log("LIST RIGHT"); - prov.board.lists.insert(prov.draggedItemState!.listIndex! + 1, - prov.board.lists.removeAt(prov.draggedItemState!.listIndex!)); - prov.draggedItemState!.listIndex = prov.draggedItemState!.listIndex! + 1; - prov.board.dragItemOfListIndex = null; - prov.board.dragItemIndex = null; - prov.draggedItemState!.itemIndex = null; - prov.board.lists[prov.draggedItemState!.listIndex! - 1].setState!(); - prov.board.lists[prov.draggedItemState!.listIndex!].setState!(); - } - - void moveListLeft() { - var prov = ref.read(ProviderList.boardProvider); - if (prov.draggedItemState!.listIndex == 0) { - return; - } - if (prov.valueNotifier.value.dx > - prov.board.lists[prov.draggedItemState!.listIndex! - 1].x! + - (prov.board.lists[prov.draggedItemState!.listIndex! - 1].width! / - 2)) { - // dev.log( - // "RETURN LEFT LIST ${prov.valueNotifier.value.dx} ${prov.board.lists[prov.draggedItemState!.listIndex! - 1].x! + (prov.board.lists[prov.draggedItemState!.listIndex! - 1].width! / 2)} "); - return; - } - // dev.log("LIST LEFT ${prov.valueNotifier.value.dx} ${prov.board.lists[prov.draggedItemState!.listIndex! - 1].x! + (prov.board.lists[prov.draggedItemState!.listIndex! - 1].width! / 2)} "); - prov.board.lists.insert(prov.draggedItemState!.listIndex! - 1, - prov.board.lists.removeAt(prov.draggedItemState!.listIndex!)); - prov.draggedItemState!.listIndex = prov.draggedItemState!.listIndex! - 1; - prov.board.dragItemOfListIndex = null; - prov.board.dragItemIndex = null; - prov.draggedItemState!.itemIndex = null; - prov.board.lists[prov.draggedItemState!.listIndex!].setState!(); - prov.board.lists[prov.draggedItemState!.listIndex! + 1].setState!(); - } - - void createNewList() {} -} diff --git a/lib/Provider/board_provider.dart b/lib/Provider/board_provider.dart deleted file mode 100644 index 674f8d8..0000000 --- a/lib/Provider/board_provider.dart +++ /dev/null @@ -1,167 +0,0 @@ -import 'dart:developer'; - -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:kanban_board/Provider/provider_list.dart'; -import 'package:kanban_board/draggable/card_draggable.dart'; -import 'package:kanban_board/draggable/draggable_state.dart'; -import 'package:kanban_board/draggable/list_draggable.dart'; -import '../models/inputs.dart'; -import '../models/board.dart'; -import '../models/board_list.dart'; -import '../models/item_state.dart'; - -class BoardProvider extends ChangeNotifier { - BoardProvider(ChangeNotifierProviderRef this.ref); - Ref ref; - ValueNotifier valueNotifier = ValueNotifier(Offset.zero); - String move = ""; - DraggedItemState? draggedItemState; - CardDraggable? cardDraggable; - ListDraggable? listDraggable; - NewCardState newCardState = NewCardState(); - Offset delta = const Offset(0, 0); - - late BoardState board; - var scrolling = false; - var scrollingRight = false; - var scrollingLeft = false; - - void initializeBoard( - {required List data, - Color backgroundColor = Colors.white, - TextStyle? textStyle, - Function(int? itemIndex, int? listIndex)? onItemTap, - Function(int? itemIndex, int? listIndex)? onItemLongPress, - Function(int? listIndex)? onListTap, - Function(int? listIndex)? onListLongPress, - double? displacementX, - double? displacementY, - void Function(int? oldCardIndex, int? newCardIndex, int? oldListIndex, - int? newListIndex)? - onItemReorder, - void Function(int? oldListIndex, int? newListIndex)? onListReorder, - void Function(String? oldName, String? newName)? onListRename, - void Function(String? cardIndex, String? listIndex, String? text)? - onNewCardInsert, - Decoration? boardDecoration, - Decoration? listDecoration, - Widget Function(Widget child, Animation animation)? - listTransitionBuilder, - Widget Function(Widget child, Animation animation)? - cardTransitionBuilder, - required Duration cardTransitionDuration, - required Duration listTransitionDuration, - Color? cardPlaceHolderColor, - Color? listPlaceHolderColor, - ScrollConfig? boardScrollConfig, - ScrollConfig? listScrollConfig}) { - board = BoardState( - textStyle: textStyle, - lists: [], - displacementX: displacementX, - displacementY: displacementY, - onItemTap: onItemTap, - onItemLongPress: onItemLongPress, - onListTap: onListTap, - onListLongPress: onListLongPress, - onItemReorder: onItemReorder, - onListReorder: onListReorder, - onListRename: onListRename, - onNewCardInsert: onNewCardInsert, - boardScrollConfig: boardScrollConfig, - listScrollConfig: listScrollConfig, - transitionHandler: TransitionHandler( - cardTransitionBuilder: - cardTransitionBuilder ?? (child, animation) => child, - listTransitionBuilder: - listTransitionBuilder ?? (child, animation) => child, - cardTransitionDuration: cardTransitionDuration, - listTransitionDuration: listTransitionDuration), - controller: ScrollController(), - backgroundColor: backgroundColor, - cardPlaceholderColor: cardPlaceHolderColor, - listPlaceholderColor: listPlaceHolderColor, - listDecoration: listDecoration, - boardDecoration: boardDecoration); - - for (int i = 0; i < data.length; i++) { - List listItems = []; - for (int j = 0; j < data[i].items.length; j++) { - listItems.add(ListItem( - child: data[i].items[j], - listIndex: i, - index: j, - prevChild: data[i].items[j])); - } - board.lists.add(BoardList( - header: data[i].header, - footer: data[i].footer, - headerBackgroundColor: data[i].headerBackgroundColor, - footerBackgroundColor: data[i].footerBackgroundColor, - backgroundColor: data[i].backgroundColor, - items: listItems, - width: data[i].width, - scrollController: ScrollController(), - title: data[i].title ?? 'LIST ${i + 1}')); - } - } - - void updateValue({ - required double dx, - required double dy, - }) { - valueNotifier.value = Offset(dx, dy); - } - - void setsState() { - notifyListeners(); - } - - void boardScroll() async { - final draggableProv = ref.read(ProviderList.draggableNotifier); - if ((draggableProv.draggableType == DraggableType.none) || scrolling) { - return; - } - if (board.controller.offset < board.controller.position.maxScrollExtent && - valueNotifier.value.dx + (draggedItemState!.width / 2) > - board.controller.position.viewportDimension - 100) { - scrolling = true; - scrollingRight = true; - if (board.boardScrollConfig == null) { - log("HEREEEE"); - await board.controller.animateTo(board.controller.offset + 100, - duration: const Duration(milliseconds: 100), curve: Curves.linear); - } else { - await board.controller.animateTo( - board.boardScrollConfig!.offset + board.controller.offset, - duration: board.boardScrollConfig!.duration, - curve: board.boardScrollConfig!.curve); - } - scrolling = false; - scrollingRight = false; - boardScroll(); - } else if (board.controller.offset > 0 && valueNotifier.value.dx <= 0) { - scrolling = true; - scrollingLeft = true; - - if (board.boardScrollConfig == null) { - await board.controller.animateTo(board.controller.offset - 100, - duration: - Duration(milliseconds: valueNotifier.value.dx < 20 ? 50 : 100), - curve: Curves.linear); - } else { - await board.controller.animateTo( - board.controller.offset - board.boardScrollConfig!.offset, - duration: board.boardScrollConfig!.duration, - curve: board.boardScrollConfig!.curve); - } - - scrolling = false; - scrollingLeft = false; - boardScroll(); - } else { - return; - } - } -} diff --git a/lib/Provider/draggable_provider.dart b/lib/Provider/draggable_provider.dart deleted file mode 100644 index 67242b6..0000000 --- a/lib/Provider/draggable_provider.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:kanban_board/draggable/card_draggable.dart'; -import 'package:kanban_board/draggable/draggable_state.dart'; -import 'package:kanban_board/draggable/list_draggable.dart'; - -class DraggableProviderState { - CardDraggable? cardDraggable; - ListDraggable? listDraggable; - DraggableType draggableType; - bool get isCardDragged => draggableType == DraggableType.card; - bool get isListDragged => draggableType == DraggableType.list; - - DraggableProviderState( - {this.cardDraggable, - this.draggableType = DraggableType.none, - this.listDraggable}); - - DraggableProviderState copyWith( - {CardDraggable? cardDraggable, - ListDraggable? listDraggable, - DraggableType? draggableType}) { - return DraggableProviderState( - cardDraggable: cardDraggable ?? this.cardDraggable, - listDraggable: listDraggable ?? this.listDraggable, - draggableType: draggableType ?? this.draggableType); - } -} - -class DraggableNotfier extends StateNotifier { - DraggableNotfier(this.ref) : super(DraggableProviderState()); - StateNotifierProviderRef ref; - void setDraggableType(DraggableType draggableType) { - state = state.copyWith(draggableType: draggableType); - } - - void stopDragging() { - setDraggableType(DraggableType.none); - } -} diff --git a/lib/Provider/list_item_provider.dart b/lib/Provider/list_item_provider.dart deleted file mode 100644 index 74e8462..0000000 --- a/lib/Provider/list_item_provider.dart +++ /dev/null @@ -1,573 +0,0 @@ -import 'dart:developer'; -import 'package:dotted_border/dotted_border.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:kanban_board/draggable/draggable_state.dart'; -import '../models/board_list.dart'; -import '../models/item_state.dart'; -import 'provider_list.dart'; - -class ListItemProvider extends ChangeNotifier { - ListItemProvider(ChangeNotifierProviderRef this.ref); - Ref ref; - TextEditingController newCardTextController = TextEditingController(); - - void calculateCardPositionSize( - {required int listIndex, - required int itemIndex, - required BuildContext context, - required VoidCallback setsate}) { - var prov = ref.read(ProviderList.boardProvider); - if (!context.mounted) return; - prov.board.lists[listIndex].items[itemIndex].context = context; - var box = context.findRenderObject() as RenderBox; - var location = box.localToGlobal(Offset.zero); - prov.board.lists[listIndex].items[itemIndex].setState = setsate; - prov.board.lists[listIndex].items[itemIndex].x = - location.dx - prov.board.displacementX!; - prov.board.lists[listIndex].items[itemIndex].y = - location.dy - prov.board.displacementY!; - prov.board.lists[listIndex].items[itemIndex].actualSize ??= box.size; - prov.board.lists[listIndex].items[itemIndex].width = box.size.width; - prov.board.lists[listIndex].items[itemIndex].height = box.size.height; - } - - void resetCardWidget() { - var prov = ref.read(ProviderList.boardProvider); - prov.board.lists[prov.board.dragItemOfListIndex!] - .items[prov.board.dragItemIndex!].placeHolderAt = PlaceHolderAt.none; - prov.board.lists[prov.board.dragItemOfListIndex!] - .items[prov.board.dragItemIndex!].placeHolderAt = PlaceHolderAt.none; - prov.board.lists[prov.board.dragItemOfListIndex!] - .items[prov.board.dragItemIndex!].child = - prov.board.lists[prov.board.dragItemOfListIndex!] - .items[prov.board.dragItemIndex!].prevChild; - } - - bool calculateSizePosition({ - required int listIndex, - required int itemIndex, - }) { - var prov = ref.read(ProviderList.boardProvider); - var item = prov.board.lists[listIndex].items[itemIndex]; - var list = prov.board.lists[listIndex]; - if (item.context == null || - list.context == null || - !item.context!.mounted) { - return true; - } - var box = item.context!.findRenderObject(); - var listBox = list.context!.findRenderObject(); - if (box == null || listBox == null) return true; - - box = box as RenderBox; - listBox = listBox as RenderBox; - var location = box.localToGlobal(Offset.zero); - item.x = location.dx - prov.board.displacementX!; - item.y = location.dy - prov.board.displacementY!; - - item.actualSize ??= box.size; - - // log("EXECUTED"); - - item.width = box.size.width; - item.height = box.size.height; - list.x = listBox.localToGlobal(Offset.zero).dx - prov.board.displacementX!; - list.y = listBox.localToGlobal(Offset.zero).dy - prov.board.displacementY!; - return false; - } - - void addPlaceHolder({required int listIndex, required int itemIndex}) { - var prov = ref.read(ProviderList.boardProvider); - var item = prov.board.lists[listIndex].items[itemIndex]; - item.child = Column( - children: [ - // AnimatedOpacity(opacity: opacity, duration: duration) - item.placeHolderAt != PlaceHolderAt.bottom - ? TweenAnimationBuilder( - duration: const Duration(milliseconds: 3000), - curve: Curves.ease, - tween: Tween(begin: 0, end: 1), - builder: (context, value, child) { - return Opacity( - opacity: value, - child: child, - ); - }, - child: Container( - // decoration: BoxDecoration( - // border: Border.all(color: Colors.grey.shade200), - // borderRadius: BorderRadius.circular(4), - // color: item.backgroundColor ?? Colors.white, - // ), - margin: const EdgeInsets.only( - bottom: 10, - ), - width: prov.draggedItemState!.width, - height: prov.draggedItemState!.height, - child: DottedBorder( - child: const Center( - child: Text( - "Drop your task here ", - style: - TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - )), - ), - ), - ) - : Container(), - // Container( - // decoration: BoxDecoration( - // borderRadius: BorderRadius.circular(4), - // color: item.backgroundColor ?? Colors.white, - // ), - // width: item.actualSize!.width, - // child: item.prevChild, - // ), - TweenAnimationBuilder( - duration: const Duration(milliseconds: 500), - curve: Curves.easeOut, - tween: Tween(begin: item.actualSize!.height, end: 0), - builder: (context, value, child) { - return Transform.translate( - offset: Offset( - 0, - item.placeHolderAt == PlaceHolderAt.bottom - ? value - : (-value)), - child: child, - ); - }, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), - color: item.backgroundColor ?? Colors.white, - ), - width: item.actualSize!.width, - child: item.prevChild, - ), - ), - item.placeHolderAt == PlaceHolderAt.bottom - ? TweenAnimationBuilder( - duration: const Duration(milliseconds: 3000), - curve: Curves.ease, - tween: Tween(begin: 0, end: 1), - builder: (context, value, child) { - return Opacity( - opacity: value, - child: child, - ); - }, - child: Container( - // decoration: BoxDecoration( - // border: Border.all(color: Colors.grey.shade200), - // borderRadius: BorderRadius.circular(4), - // color: item.backgroundColor ?? Colors.white, - // ), - margin: const EdgeInsets.only(top: 10), - width: prov.draggedItemState!.width, - height: prov.draggedItemState!.height, - child: DottedBorder( - child: const Center( - child: Text( - "Drop your task here ", - style: - TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - )), - ), - ), - ) - : Container(), - ], - ); - } - - bool isPrevSystemCard({required int listIndex, required int itemIndex}) { - var prov = ref.read(ProviderList.boardProvider); - var item = prov.board.lists[listIndex].items[itemIndex]; - var isItemHidden = itemIndex - 1 >= 0 && - prov.draggedItemState!.itemIndex == itemIndex - 1 && - prov.draggedItemState!.listIndex == listIndex; - if (prov.board.lists[prov.board.dragItemOfListIndex!] - .items[prov.board.dragItemIndex!].addedBySystem == - true) { - prov.board.lists[prov.board.dragItemOfListIndex!].items.removeAt(0); - log("ITEM REMOVED"); - - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - prov.board.lists[prov.board.dragItemOfListIndex!].setState!(); - if (isItemHidden) { - // print("ITEM HIDDEN"); - prov.move = "DOWN"; - } - prov.board.dragItemIndex = itemIndex; - prov.board.dragItemOfListIndex = listIndex; - - // log("UPDATED | ITEM= ${prov.board.dragItemIndex} | LIST= $listIndex"); - item.setState!(); - }); - - return true; - } - return false; - } - - void checkForYAxisMovement({required int listIndex, required int itemIndex}) { - var prov = ref.read(ProviderList.boardProvider); - var item = prov.board.lists[listIndex].items[itemIndex]; - - bool willPlaceHolderAtBottom = - _bottomPlaceHolderPossibility(listIndex, itemIndex); - // willPlaceHolderAtBottom = ((itemIndex == - // prov.board.lists[listIndex].items.length - 1) && - // ((prov.draggedItemState!.height * 0.6) + prov.valueNotifier.value.dy > - // item.y! + item.height!) && - // item.placeHolderAt != PlaceHolderAt.bottom && - // prov.board.lists[listIndex].items[itemIndex].addedBySystem != true); - - // willPlaceHolderAtTop = - // ((prov.valueNotifier.value.dy < item.y! + (item.height! * 0.5)) && - // (prov.draggedItemState!.height + prov.valueNotifier.value.dy > - // item.y! + (item.height! * 0.5))) && - // prov.board.lists[listIndex].items[itemIndex].addedBySystem != true; - - // print(willPlaceHolderAtTop); - // if (((willPlaceHolderAtTop || willPlaceHolderAtBottom) && - // prov.board.dragItemOfListIndex! == listIndex) && - // (prov.board.dragItemIndex != itemIndex || - // (willPlaceHolderAtBottom && - // item.placeHolderAt != PlaceHolderAt.bottom) || - // (item.placeHolderAt == PlaceHolderAt.bottom && - // (itemIndex == prov.board.lists[listIndex].items.length - 1)))) - if (getYAxisCondition(listIndex: listIndex, itemIndex: itemIndex)) { - // log("UP/DOWNN"); - // print("BOTTOM PLACEHOLDER => ${willPlaceHolderAtBottom}"); - if (willPlaceHolderAtBottom && - item.placeHolderAt == PlaceHolderAt.bottom) { - return; - } - - if (prov.board.dragItemIndex! < itemIndex && prov.move != 'other') { - prov.move = "DOWN"; - } - - resetCardWidget(); - - item.placeHolderAt = - willPlaceHolderAtBottom ? PlaceHolderAt.bottom : PlaceHolderAt.top; - - if (willPlaceHolderAtBottom) { - prov.move = "LAST"; - } - var isItemHidden = itemIndex - 1 >= 0 && - prov.draggedItemState!.itemIndex == itemIndex - 1 && - prov.draggedItemState!.listIndex == listIndex; - - if ((item.addedBySystem == null || !item.addedBySystem!)) { - addPlaceHolder(listIndex: listIndex, itemIndex: itemIndex); - // log("${item.placeHolderAt.name}=>${item.height}"); - } - if (isPrevSystemCard(listIndex: listIndex, itemIndex: itemIndex)) return; - - var temp = prov.board.dragItemIndex; - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - // log("PREVIOUS |${prov.board.dragItemOfListIndex}| LIST= ${prov.board.dragItemIndex}"); - - if (!prov.board.lists[prov.board.dragItemOfListIndex!].items[temp!] - .context!.mounted) return; - - if (isItemHidden) { - prov.move = "DOWN"; - } - if (itemIndex != prov.board.dragItemIndex && - prov.board.dragItemOfListIndex != listIndex) { - prov.board.lists[prov.board.dragItemOfListIndex!].items[temp] - .placeHolderAt = PlaceHolderAt.none; - } - - prov.board.dragItemIndex = itemIndex; - prov.board.dragItemOfListIndex = listIndex; - prov.board.lists[prov.board.dragItemOfListIndex!].items[temp] - .setState!(); - // log("UP/DOWN $listIndex $itemIndex"); - item.setState!(); - }); - } - } - - bool isLastItemDragged({required int listIndex, required int itemIndex}) { - var prov = ref.read(ProviderList.boardProvider); - var item = prov.board.lists[listIndex].items[itemIndex]; - if (prov.draggedItemState!.itemIndex == itemIndex && - prov.draggedItemState!.listIndex == listIndex && - prov.board.lists[listIndex].items.length - 1 == itemIndex && - prov.board.dragItemIndex == itemIndex && - prov.board.dragItemOfListIndex == listIndex) { - return true; - } - - if ((prov.draggedItemState!.itemIndex == itemIndex && - prov.draggedItemState!.listIndex == listIndex && - prov.board.dragItemOfListIndex == listIndex && - prov.board.lists[listIndex].items.length - 1 == itemIndex && - ((prov.draggedItemState!.height * 0.6) + prov.valueNotifier.value.dy > - item.y! + item.height!))) { - WidgetsBinding.instance.addPostFrameCallback((timeStamp) {}); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - // log("PREVIOUS |${prov.board.dragItemOfListIndex}| LIST= ${prov.board.dragItemIndex}"); - - // if (isItemHidden) { - // prov.move = "DOWN"; - // } - prov.board.lists[prov.board.dragItemOfListIndex!] - .items[prov.board.dragItemIndex!].child = - prov.board.lists[prov.board.dragItemOfListIndex!] - .items[prov.board.dragItemIndex!].prevChild; - prov.board.lists[prov.board.dragItemOfListIndex!] - .items[prov.board.dragItemIndex!].setState!(); - prov.board.dragItemIndex = itemIndex; - prov.board.dragItemOfListIndex = listIndex; - - // log("UP/DOWN $listIndex $itemIndex"); - item.setState!(); - }); - return true; - } - return false; - } - - bool _topPlaceHolderPossibility(int listIndex, int itemIndex) { - var prov = ref.read(ProviderList.boardProvider); - var item = prov.board.lists[listIndex].items[itemIndex]; - - var willPlaceHolderAtTop = item.placeHolderAt == PlaceHolderAt.bottom - ? (prov.valueNotifier.value.dy < - item.y! + (item.actualSize!.height * 0.65)) - : ((prov.valueNotifier.value.dy <= - item.y! + (item.actualSize!.height * 0.65)) && - (prov.draggedItemState!.height + prov.valueNotifier.value.dy > - item.y! + (item.height!))); - - bool x = (item.placeHolderAt == PlaceHolderAt.bottom - ? item.height != item.actualSize!.height - : true); - // if (item.placeHolderAt == PlaceHolderAt.bottom) { - // print("BOTTOM TRUE"); - // } - - return willPlaceHolderAtTop && - prov.delta.dy < 0 && - item.placeHolderAt != PlaceHolderAt.top && - x && - prov.board.dragItemOfListIndex! == listIndex && - item.addedBySystem != true; - } - - bool _bottomPlaceHolderPossibility(int listIndex, int itemIndex) { - var prov = ref.read(ProviderList.boardProvider); - var item = prov.board.lists[listIndex].items[itemIndex]; - var willPlaceHolderAtBottom = item.placeHolderAt == PlaceHolderAt.top - ? (prov.draggedItemState!.height + prov.valueNotifier.value.dy >= - item.y! + (item.height! * 0.8)) - : ((prov.draggedItemState!.height + prov.valueNotifier.value.dy >= - item.y! + (item.height! * 0.5)) && - (prov.valueNotifier.value.dy < item.y!)); - - bool x = (item.placeHolderAt == PlaceHolderAt.top - ? item.height != item.actualSize!.height - : true); - - return - // false&& - willPlaceHolderAtBottom && - item.placeHolderAt != PlaceHolderAt.bottom && - prov.board.dragItemOfListIndex! == listIndex && - item.addedBySystem != true && - x; - } - - bool getYAxisCondition({required int listIndex, required int itemIndex}) { - var prov = ref.read(ProviderList.boardProvider); - var item = prov.board.lists[listIndex].items[itemIndex]; - bool value = (_topPlaceHolderPossibility(listIndex, itemIndex) || - _bottomPlaceHolderPossibility(listIndex, itemIndex)) && - prov.board.dragItemOfListIndex! == listIndex; - return value && item.addedBySystem != true; - } - - bool getXAxisCondition({required int listIndex, required int itemIndex}) { - var prov = ref.read(ProviderList.boardProvider); - - var right = ((prov.draggedItemState!.width * 0.6) + - prov.valueNotifier.value.dx > - prov.board.lists[listIndex].x!) && - ((prov.board.lists[listIndex].x! + prov.board.lists[listIndex].width! > - prov.draggedItemState!.width + prov.valueNotifier.value.dx)) && - (prov.board.dragItemOfListIndex != listIndex); - var left = (((prov.draggedItemState!.width) + prov.valueNotifier.value.dx > - prov.board.lists[listIndex].x! + - prov.board.lists[listIndex].width!) && - ((prov.draggedItemState!.width * 0.6) + prov.valueNotifier.value.dx < - prov.board.lists[listIndex].x! + - prov.board.lists[listIndex].width!) && - (prov.board.dragItemOfListIndex != listIndex)); - - return (left || right); - } - - void checkForXAxisMovement({required int listIndex, required int itemIndex}) { - var prov = ref.read(ProviderList.boardProvider); - var item = prov.board.lists[listIndex].items[itemIndex]; - - var canReplaceCurrent = ((prov.valueNotifier.value.dy >= item.y!) && - (item.height! + item.y! > - prov.valueNotifier.value.dy + (prov.draggedItemState!.height / 2))); - var willPlaceHolderAtBottom = (itemIndex == - prov.board.lists[listIndex].items.length - 1 && - ((prov.draggedItemState!.height * 0.6) + prov.valueNotifier.value.dy > - item.y! + item.height!)); - - if (canReplaceCurrent || willPlaceHolderAtBottom) { - // log("X AXIS"); - prov.move = "other"; - - resetCardWidget(); - - item.placeHolderAt = - willPlaceHolderAtBottom ? PlaceHolderAt.bottom : PlaceHolderAt.top; - if (willPlaceHolderAtBottom) { - prov.move = "LAST"; - // log("BOTTOM PLACEHOLDER X AXIS"); - } - - var isItemHidden = itemIndex - 1 >= 0 && - prov.draggedItemState!.itemIndex == itemIndex - 1 && - prov.draggedItemState!.listIndex == listIndex; - - // if (!isItemHidden) { - addPlaceHolder(listIndex: listIndex, itemIndex: itemIndex); - // } - if (isPrevSystemCard(listIndex: listIndex, itemIndex: itemIndex)) return; - - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - prov.board.lists[prov.board.dragItemOfListIndex!] - .items[prov.board.dragItemIndex!].setState!(); - if (isItemHidden) { - prov.move = "DOWN"; - } - prov.board.dragItemIndex = itemIndex; - prov.board.dragItemOfListIndex = listIndex; - // log("UPDATED | ITEM= $listIndex | LIST= $itemIndex"); - item.setState!(); - }); - } - } - - void onLongpressCard( - {required int listIndex, - required int itemIndex, - required BuildContext context, - required VoidCallback setsate}) { - var prov = ref.read(ProviderList.boardProvider); - final draggableProv = ref.read(ProviderList.draggableNotifier.notifier); - var box = context.findRenderObject() as RenderBox; - var location = box.localToGlobal(Offset.zero); - prov.board.lists[listIndex].items[itemIndex].x = - location.dx - prov.board.displacementX!; - prov.board.lists[listIndex].items[itemIndex].y = - location.dy - prov.board.displacementY!; - // prov.board.lists[listIndex].items[itemIndex].width = box.size.width; - // prov.board.lists[listIndex].items[itemIndex].height = box.size.height; - prov.updateValue( - dx: location.dx - prov.board.displacementX!, - dy: location.dy - prov.board.displacementY!); - prov.board.dragItemIndex = itemIndex; - prov.board.dragItemOfListIndex = listIndex; - draggableProv.setDraggableType(DraggableType.card); - prov.draggedItemState = DraggedItemState( - child: SizedBox( - width: - prov.board.lists[listIndex].items[itemIndex].context!.size!.width, - child: prov.board.lists[listIndex].items[itemIndex].child, - ), - listIndex: listIndex, - itemIndex: itemIndex, - height: box.size.height - 10, - width: box.size.width, - x: location.dx, - y: location.dy); - prov.draggedItemState!.setState = setsate; - // log("${listIndex} ${itemIndex}"); - setsate(); - } - - bool isCurrentElementDragged( - {required int listIndex, required int itemIndex}) { - var prov = ref.read(ProviderList.boardProvider); - final draggableProv = ref.read(ProviderList.draggableNotifier); - - return draggableProv.isCardDragged && - prov.draggedItemState!.itemIndex == itemIndex && - prov.draggedItemState!.listIndex == listIndex; - } - - void saveNewCard() { - var boardProv = ref.read(ProviderList.boardProvider); - final cardState = boardProv.newCardState; - boardProv.board.lists[cardState.listIndex!].items[cardState.cardIndex!] - .child = Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.grey.shade200), - borderRadius: BorderRadius.circular(4), - color: Colors.white, - ), - margin: const EdgeInsets.only(bottom: 10), - padding: const EdgeInsets.all(12), - child: Text(boardProv.newCardState.textController.text, - style: boardProv.board.textStyle), - ); - boardProv.board.lists[cardState.listIndex!].items[cardState.cardIndex!] - .prevChild = - boardProv.board.lists[cardState.listIndex!].items[cardState.cardIndex!] - .child; - cardState.isFocused = false; - boardProv.board.lists[cardState.listIndex!].items[cardState.listIndex!] - .isNew = false; - boardProv.newCardState.textController.clear(); - boardProv.board.lists[cardState.listIndex!].items[cardState.listIndex!] - .setState!(); - cardState.cardIndex = null; - cardState.listIndex = null; - log("TAPPED"); - } - - void reorderCard() { - var boardProv = ref.read(ProviderList.boardProvider); - BoardList list = - boardProv.board.lists[boardProv.board.dragItemOfListIndex!]; - ListItem card = list.items[boardProv.board.dragItemIndex!]; - card.child = card.prevChild; - - if (boardProv.draggedItemState!.listIndex == - boardProv.board.dragItemOfListIndex!) { - list.items.insert( - boardProv.board.dragItemIndex!, - boardProv.board.lists[boardProv.draggedItemState!.listIndex!].items - .removeAt(boardProv.draggedItemState!.itemIndex!)); - } else { - if (card.placeHolderAt == PlaceHolderAt.bottom) { - list.items.insert( - boardProv.board.dragItemIndex! + 1, - boardProv.board.lists[boardProv.draggedItemState!.listIndex!].items - .removeAt(boardProv.draggedItemState!.itemIndex!)); - } else { - list.items.insert( - boardProv.board.dragItemIndex!, - boardProv.board.lists[boardProv.draggedItemState!.listIndex!].items - .removeAt(boardProv.draggedItemState!.itemIndex!)); - } - } - - card.placeHolderAt = PlaceHolderAt.none; - } -} diff --git a/lib/Provider/provider_list.dart b/lib/Provider/provider_list.dart deleted file mode 100755 index ce177b9..0000000 --- a/lib/Provider/provider_list.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:kanban_board/Provider/draggable_provider.dart'; - -import 'list_item_provider.dart'; - -import 'board_list_provider.dart'; -import 'board_provider.dart'; - -class ProviderList { - static final boardProvider = ChangeNotifierProvider( - (ref) => BoardProvider(ref), - ); - static final cardProvider = ChangeNotifierProvider( - (ref) => ListItemProvider(ref), - ); - static final boardListProvider = ChangeNotifierProvider( - (ref) => BoardListProvider(ref), - ); - - static final draggableNotifier = - StateNotifierProvider( - (ref) => DraggableNotfier(ref)); -} diff --git a/lib/constants.dart b/lib/constants.dart deleted file mode 100644 index 15ba122..0000000 --- a/lib/constants.dart +++ /dev/null @@ -1,4 +0,0 @@ -// ignore_for_file: constant_identifier_names - -const double BOARD_PADDING = 20; -const double LIST_GAP = 10; diff --git a/lib/custom/board.dart b/lib/custom/board.dart deleted file mode 100755 index a484d6b..0000000 --- a/lib/custom/board.dart +++ /dev/null @@ -1,480 +0,0 @@ -import 'package:dotted_border/dotted_border.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:kanban_board/constants.dart'; -import 'package:kanban_board/draggable/draggable_state.dart'; -import 'package:kanban_board/draggable/presentation/dragged_card.dart'; -import '../Provider/provider_list.dart'; -import '../models/board_list.dart' as board_list; -import '../models/inputs.dart'; -import 'board_list.dart'; -import 'text_field.dart'; - -class KanbanBoard extends StatefulWidget { - const KanbanBoard( - this.list, { - this.backgroundColor = Colors.white, - this.cardPlaceHolderColor, - this.boardScrollConfig, - this.listScrollConfig, - this.listPlaceHolderColor, - this.boardDecoration, - this.cardTransitionBuilder, - this.listTransitionBuilder, - this.cardTransitionDuration = const Duration(milliseconds: 150), - this.listTransitionDuration = const Duration(milliseconds: 150), - this.listDecoration, - this.textStyle, - this.onItemTap, - this.displacementX = 0.0, - this.displacementY = 0.0, - this.onItemReorder, - this.onListReorder, - this.onListRename, - this.onNewCardInsert, - this.onItemLongPress, - this.onListTap, - this.onListLongPress, - super.key, - }); - final List list; - final Color backgroundColor; - final ScrollConfig? boardScrollConfig; - final ScrollConfig? listScrollConfig; - final Color? cardPlaceHolderColor; - final Color? listPlaceHolderColor; - final TextStyle? textStyle; - final Decoration? listDecoration; - final Decoration? boardDecoration; - final void Function(int? cardIndex, int? listIndex)? onItemTap; - final void Function(int? cardIndex, int? listIndex)? onItemLongPress; - final void Function(int? listIndex)? onListTap; - final void Function(int? listIndex)? onListLongPress; - final void Function(int? oldCardIndex, int? newCardIndex, int? oldListIndex, - int? newListIndex)? onItemReorder; - final void Function(int? oldListIndex, int? newListIndex)? onListReorder; - final void Function(String? oldName, String? newName)? onListRename; - final void Function(String? cardIndex, String? listIndex, String? text)? - onNewCardInsert; - final Widget Function(Widget child, Animation animation)? - cardTransitionBuilder; - final Widget Function(Widget child, Animation animation)? - listTransitionBuilder; - final double displacementX; - final double displacementY; - final Duration cardTransitionDuration; - final Duration listTransitionDuration; - - @override - State createState() => _KanbanBoardState(); -} - -class _KanbanBoardState extends State { - @override - Widget build(BuildContext context) { - return ProviderScope( - child: MaterialApp( - debugShowCheckedModeBanner: false, - home: Board(widget.list, - displacementX: widget.displacementX, - displacementY: widget.displacementY, - backgroundColor: widget.backgroundColor, - boardDecoration: widget.boardDecoration, - cardPlaceHolderColor: widget.cardPlaceHolderColor, - listPlaceHolderColor: widget.listPlaceHolderColor, - listDecoration: widget.listDecoration, - boardScrollConfig: widget.boardScrollConfig, - listScrollConfig: widget.listScrollConfig, - textStyle: widget.textStyle, - onItemTap: widget.onItemTap, - onItemLongPress: widget.onItemLongPress, - onListTap: widget.onListTap, - onListLongPress: widget.onListLongPress, - onItemReorder: widget.onItemReorder, - onListReorder: widget.onListReorder, - onListRename: widget.onListRename, - onNewCardInsert: widget.onNewCardInsert, - cardTransitionBuilder: widget.cardTransitionBuilder, - listTransitionBuilder: widget.listTransitionBuilder, - cardTransitionDuration: widget.cardTransitionDuration, - listTransitionDuration: widget.listTransitionDuration), - )); - } -} - -class Board extends ConsumerStatefulWidget { - const Board( - this.list, { - this.backgroundColor = Colors.white, - this.cardPlaceHolderColor, - this.listPlaceHolderColor, - this.boardDecoration, - this.boardScrollConfig, - this.listScrollConfig, - this.cardTransitionBuilder, - this.listTransitionBuilder, - this.cardTransitionDuration = const Duration(milliseconds: 150), - this.listTransitionDuration = const Duration(milliseconds: 150), - this.listDecoration, - this.textStyle, - this.onItemTap, - this.displacementX = 0.0, - this.displacementY = 0.0, - this.onItemReorder, - this.onListReorder, - this.onListRename, - this.onNewCardInsert, - this.onItemLongPress, - this.onListTap, - this.onListLongPress, - super.key, - }); - final List list; - final Color backgroundColor; - final Color? cardPlaceHolderColor; - final Color? listPlaceHolderColor; - final TextStyle? textStyle; - final Decoration? listDecoration; - final Decoration? boardDecoration; - final ScrollConfig? boardScrollConfig; - final ScrollConfig? listScrollConfig; - final void Function(int? cardIndex, int? listIndex)? onItemTap; - final void Function(int? cardIndex, int? listIndex)? onItemLongPress; - final void Function(int? listIndex)? onListTap; - final void Function(int? listIndex)? onListLongPress; - final void Function(int? oldCardIndex, int? newCardIndex, int? oldListIndex, - int? newListIndex)? onItemReorder; - final void Function(int? oldListIndex, int? newListIndex)? onListReorder; - final void Function(String? oldName, String? newName)? onListRename; - final void Function(String? cardIndex, String? listIndex, String? text)? - onNewCardInsert; - final Widget Function(Widget child, Animation animation)? - cardTransitionBuilder; - final Widget Function(Widget child, Animation animation)? - listTransitionBuilder; - final double displacementX; - final double displacementY; - final Duration cardTransitionDuration; - final Duration listTransitionDuration; - - @override - ConsumerState createState() => _BoardState(); -} - -class _BoardState extends ConsumerState { - @override - void initState() { - var boardProv = ref.read(ProviderList.boardProvider); - final draggableProv = ref.read(ProviderList.draggableNotifier); - var boardListProv = ref.read(ProviderList.boardListProvider); - boardProv.initializeBoard( - data: widget.list, - boardScrollConfig: widget.boardScrollConfig, - listScrollConfig: widget.listScrollConfig, - displacementX: widget.displacementX, - displacementY: widget.displacementY, - backgroundColor: widget.backgroundColor, - boardDecoration: widget.boardDecoration, - cardPlaceHolderColor: widget.cardPlaceHolderColor, - listPlaceHolderColor: widget.listPlaceHolderColor, - listDecoration: widget.listDecoration, - textStyle: widget.textStyle, - onItemTap: widget.onItemTap, - onItemLongPress: widget.onItemLongPress, - onListTap: widget.onListTap, - onListLongPress: widget.onListLongPress, - onItemReorder: widget.onItemReorder, - onListReorder: widget.onListReorder, - onListRename: widget.onListRename, - onNewCardInsert: widget.onNewCardInsert, - cardTransitionBuilder: widget.cardTransitionBuilder, - listTransitionBuilder: widget.listTransitionBuilder, - cardTransitionDuration: widget.cardTransitionDuration, - listTransitionDuration: widget.listTransitionDuration); - - for (var element in boardProv.board.lists) { - // List Scroll Listener - element.scrollController.addListener(() { - if (boardListProv.scrolling) { - if (boardListProv.scrollingDown) { - boardProv.valueNotifier.value = Offset( - boardProv.valueNotifier.value.dx, - boardProv.valueNotifier.value.dy + 0.00001); - } else { - boardProv.valueNotifier.value = Offset( - boardProv.valueNotifier.value.dx, - boardProv.valueNotifier.value.dy + 0.00001); - } - } - }); - } - - // Board Scroll Listener - boardProv.board.controller.addListener(() { - if (boardProv.scrolling) { - if (boardProv.scrollingLeft && draggableProv.isListDragged) { - for (var element in boardProv.board.lists) { - if (element.context == null) break; - var of = (element.context!.findRenderObject() as RenderBox) - .localToGlobal(Offset.zero); - element.x = of.dx - boardProv.board.displacementX! - 10; - element.width = element.context!.size!.width - 30; - element.y = of.dy - widget.displacementY + 24; - } - - boardListProv.moveListLeft(); - } else if (boardProv.scrollingRight && draggableProv.isListDragged) { - for (var element in boardProv.board.lists) { - if (element.context == null) break; - var of = (element.context!.findRenderObject() as RenderBox) - .localToGlobal(Offset.zero); - element.x = of.dx - boardProv.board.displacementX! - 10; - element.width = element.context!.size!.width - 30; - element.y = of.dy - widget.displacementY + 24; - } - boardListProv.moveListRight(); - } - } - }); - - super.initState(); - } - - @override - Widget build(BuildContext context) { - var boardProv = ref.read(ProviderList.boardProvider); - var boardListProv = ref.read(ProviderList.boardListProvider); - final draggableProv = ref.watch(ProviderList.draggableNotifier); - final draggableNotifier = ref.read(ProviderList.draggableNotifier.notifier); - double statusBarHeight = MediaQuery.of(context).padding.top; - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - boardProv.board.setstate = () => setState(() {}); - var box = context.findRenderObject() as RenderBox; - boardProv.board.displacementX = - box.localToGlobal(Offset.zero).dx + BOARD_PADDING; //- margin - boardProv.board.displacementY = box.localToGlobal(Offset.zero).dy - - statusBarHeight + - BOARD_PADDING; // statusbar - }); - return Listener( - onPointerUp: (event) { - if (draggableProv.draggableType != DraggableType.none) { - if (draggableProv.isCardDragged) { - ref.read(ProviderList.cardProvider).reorderCard(); - } - boardProv.move = ""; - draggableNotifier.stopDragging(); - setState(() {}); - } - }, - onPointerMove: (event) { - if (draggableProv.isCardDragged) { - if (event.delta.dx > 0) { - boardProv.boardScroll(); - } else { - boardProv.boardScroll(); - } - } else if (draggableProv.isListDragged) { - if (event.delta.dx > 0) { - boardProv.boardScroll(); - boardListProv.moveListRight(); - } else { - boardProv.boardScroll(); - boardListProv.moveListLeft(); - } - } - boardProv.delta = event.delta; - boardProv.valueNotifier.value = Offset( - event.delta.dx + boardProv.valueNotifier.value.dx, - event.delta.dy + boardProv.valueNotifier.value.dy); - }, - child: GestureDetector( - onTap: () { - if (boardProv.newCardState.isFocused == true) { - ref.read(ProviderList.cardProvider).saveNewCard(); - } - }, - child: Scaffold( - backgroundColor: Colors.white, - body: Container( - padding: - const EdgeInsets.only(top: BOARD_PADDING, left: BOARD_PADDING), - decoration: widget.boardDecoration ?? - BoxDecoration(color: widget.backgroundColor), - child: Stack( - fit: StackFit.passthrough, - clipBehavior: Clip.none, - children: [ - Row( - children: [ - Expanded( - child: SizedBox( - height: 1200, - child: ScrollConfiguration( - behavior: ScrollConfiguration.of(context).copyWith( - dragDevices: { - PointerDeviceKind.mouse, - PointerDeviceKind.touch, - }, - ), - child: SingleChildScrollView( - controller: boardProv.board.controller, - scrollDirection: Axis.horizontal, - child: Row( - children: boardProv.board.lists - .map( - (e) => - boardProv.board.lists.indexOf(e) != - boardProv.board.lists - .length - - 1 - ? BoardList( - index: boardProv.board.lists - .indexOf(e), - ) - : Row( - crossAxisAlignment: - CrossAxisAlignment - .start, - children: [ - BoardList( - index: boardProv - .board.lists - .indexOf(e), - ), - boardListProv.newList - ? Container( - margin: - const EdgeInsets - .only( - top: 20, - left: 30, - ), - padding: - const EdgeInsets - .only( - bottom: 20, - ), - width: 300, - color: const Color - .fromARGB( - 255, - 247, - 248, - 252, - ), - child: Wrap( - children: [ - SizedBox( - height: 50, - width: 300, - child: Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - IconButton( - onPressed: - () { - setState(() { - boardListProv.newList = false; - boardProv.newCardState.textController.clear(); - }); - }, - icon: - const Icon(Icons.close), - ), - IconButton( - onPressed: - () { - setState(() { - boardListProv.newList = false; - boardProv.board.lists.add(board_list.BoardList( - width: 300, - scrollController: ScrollController(), - items: [], - title: boardProv.newCardState.textController.text, - )); - boardProv.newCardState.textController.clear(); - }); - }, - icon: - const Icon(Icons.done)) - ], - ), - ), - Container( - width: - 300, - color: Colors - .white, - margin: const EdgeInsets - .only( - top: - 20, - right: - 10, - left: - 10), - child: - const TField()), - ], - ), - ) - : GestureDetector( - onTap: () { - if (boardProv - .newCardState - .isFocused == - true) { - ref - .read(ProviderList - .cardProvider) - .saveNewCard(); - } - boardListProv - .newList = - true; - setState(() {}); - }, - child: Container( - height: 50, - width: 300, - margin: const EdgeInsets - .only( - top: 10, - left: 20), - decoration: BoxDecoration( - color: Colors - .transparent, - borderRadius: - BorderRadius.circular( - 6)), - child: - DottedBorder( - child: Center( - child: Text( - "Add List", - style: - widget.textStyle)), - )), - ) - ], - )) - .toList()), - ), - ), - ), - ), - ], - ), - const DraggedCard() - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/custom/board_list.dart b/lib/custom/board_list.dart deleted file mode 100644 index 69d4631..0000000 --- a/lib/custom/board_list.dart +++ /dev/null @@ -1,378 +0,0 @@ -import 'dart:developer'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:kanban_board/constants.dart'; -import 'package:kanban_board/draggable/draggable_state.dart'; -import '../../Provider/provider_list.dart'; -import '../models/item_state.dart'; -import 'list_item.dart'; - -class BoardList extends ConsumerStatefulWidget { - const BoardList({super.key, required this.index}); - final int index; - - @override - ConsumerState createState() => _BoardListState(); -} - -class _BoardListState extends ConsumerState { - Offset location = Offset.zero; - - @override - Widget build(BuildContext context) { - var prov = ref.read(ProviderList.boardProvider); - var listProv = ref.read(ProviderList.boardListProvider); - final draggableNotfier = ref.read(ProviderList.draggableNotifier); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - listProv.calculateSizePosition( - listIndex: widget.index, - context: context, - setstate: () => setState(() {})); - }); - return ValueListenableBuilder( - valueListenable: prov.valueNotifier, - builder: (context, a, b) { - if (draggableNotfier.draggableType == DraggableType.card) { - var draggedItemIndex = prov.board.dragItemIndex; - var draggedItemListIndex = prov.board.dragItemOfListIndex; - var list = prov.board.lists[widget.index]; - var box = context.findRenderObject(); - var listBox = list.context!.findRenderObject(); - if (box == null || listBox == null) return b!; - - box = box as RenderBox; - listBox = listBox as RenderBox; - location = box.localToGlobal(Offset.zero); - - list.x = listBox.localToGlobal(Offset.zero).dx - - prov.board.displacementX!; - list.y = listBox.localToGlobal(Offset.zero).dy - - prov.board.displacementY!; - - if (((prov.draggedItemState!.width * 0.6) + - prov.valueNotifier.value.dx > - prov.board.lists[widget.index].x!) && - ((prov.board.lists[widget.index].x! + - prov.board.lists[widget.index].width! > - prov.draggedItemState!.width + - prov.valueNotifier.value.dx)) && - (prov.board.dragItemOfListIndex! != widget.index)) { - // print("RIGHT ->"); - // print(prov.board.lists[widget.index].items.length); - // CASE: WHEN ELEMENT IS DRAGGED RIGHT SIDE AND LIST HAVE NO ELEMENT IN IT // - if (prov.board.lists[widget.index].items.isEmpty) { - log("LIST 0 RIGHT"); - prov.move = "REPLACE"; - prov.board.lists[widget.index].items.add(ListItem( - child: Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.grey.shade200), - borderRadius: BorderRadius.circular(6), - color: prov.board.cardPlaceholderColor ?? Colors.white, - ), - margin: const EdgeInsets.only(top: 5), - width: prov.draggedItemState!.width, - height: prov.draggedItemState!.height, - ), - prevChild: - Container(), // WE HAVE ADDED THIS IN EMPTY LIST JUST TO SHOW PLACEHOLDER, so it should be hiddent - listIndex: widget.index, - index: 0, - addedBySystem: true)); - - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - prov.board.lists[prov.board.dragItemOfListIndex!] - .items[prov.board.dragItemIndex!].child = - prov.board.lists[prov.board.dragItemOfListIndex!] - .items[prov.board.dragItemIndex!].prevChild; - prov.board.lists[prov.board.dragItemOfListIndex!] - .items[prov.board.dragItemIndex!].setState!(); - if (prov.board.lists[prov.board.dragItemOfListIndex!] - .items[prov.board.dragItemIndex!].addedBySystem == - true) { - prov.board.lists[prov.board.dragItemOfListIndex!].items - .removeAt(0); - log("ITEM REMOVED"); - prov.board.lists[prov.board.dragItemOfListIndex!] - .setState!(); - } - prov.board.dragItemIndex = 0; - prov.board.dragItemOfListIndex = widget.index; - setState(() {}); - }); - } - // CASE WHEN LIST HAVE ONLY ONE ITEM AND IT IS PICKED, SO NOW IT IS HIDDEN, ITS SIZE IS 0 , SO WE NEED TO HANDLE IT EXPLICITLY // - else if (prov.board.lists[widget.index].items.length == 1 && - prov.draggedItemState!.itemIndex == 0 && - prov.draggedItemState!.listIndex == widget.index) { - // print("RIGHT LENGTH == 1"); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - if (prov.board.lists[draggedItemListIndex!] - .items[draggedItemIndex!].addedBySystem == - true) { - prov.board.lists[draggedItemListIndex].items - .removeAt(draggedItemIndex); - prov.board.lists[draggedItemListIndex].setState!(); - } else { - prov - .board - .lists[prov.board.dragItemOfListIndex!] - .items[prov.board.dragItemIndex!] - .placeHolderAt = PlaceHolderAt.none; - - prov.board.lists[prov.board.dragItemOfListIndex!] - .items[prov.board.dragItemIndex!].child = - prov.board.lists[prov.board.dragItemOfListIndex!] - .items[prov.board.dragItemIndex!].prevChild; - - prov.board.lists[prov.board.dragItemOfListIndex!] - .items[prov.board.dragItemIndex!].setState!(); - } - prov.board.dragItemIndex = 0; - prov.board.dragItemOfListIndex = widget.index; - // log("UPDATED | ITEM= ${widget.itemIndex} | LIST= ${widget.listIndex}"); - prov.board.lists[prov.board.dragItemOfListIndex!] - .items[prov.board.dragItemIndex!].setState!(); - }); - } - } else if (((prov.draggedItemState!.width * 0.6) + - prov.valueNotifier.value.dx < - prov.board.lists[widget.index].x! + - prov.board.lists[widget.index].width!) && - ((prov.board.lists[widget.index].x! + - prov.board.lists[widget.index].width! < - prov.draggedItemState!.width + - prov.valueNotifier.value.dx)) && - (prov.board.dragItemOfListIndex! != widget.index)) { - if (prov.board.lists[widget.index].items.isEmpty) { - prov.move = "REPLACE"; - // print("LIST 0 LEFT"); - - prov.board.lists[widget.index].items.add(ListItem( - child: Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.grey.shade200), - borderRadius: BorderRadius.circular(6), - color: prov.board.cardPlaceholderColor ?? Colors.white, - ), - margin: const EdgeInsets.only(top: 5), - width: prov.draggedItemState!.width, - height: prov.draggedItemState!.height, - ), - prevChild: - Container(), // WE HAVE ADDED THIS IN EMPTY LIST JUST TO SHOW PLACEHOLDER, so it should be hiddent - listIndex: widget.index, - index: 0, - addedBySystem: true)); - - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - prov.board.lists[prov.board.dragItemOfListIndex!] - .items[prov.board.dragItemIndex!].child = - prov.board.lists[prov.board.dragItemOfListIndex!] - .items[prov.board.dragItemIndex!].prevChild; - prov.board.lists[prov.board.dragItemOfListIndex!] - .items[prov.board.dragItemIndex!].setState!(); - - if (prov.board.lists[prov.board.dragItemOfListIndex!] - .items[prov.board.dragItemIndex!].addedBySystem == - true) { - prov.board.lists[prov.board.dragItemOfListIndex!].items - .removeAt(0); - log("ITEM REMOVED"); - prov.board.lists[prov.board.dragItemOfListIndex!] - .setState!(); - } - prov.board.dragItemIndex = 0; - prov.board.dragItemOfListIndex = widget.index; - setState(() {}); - }); - } - // CASE: WHEN LIST CONTAINS ONLY ONE ITEM, AND WHICH IS THE FIRST ITEM DRAGGED DURING A PARTICULAR SESSION, WHICH IS CURRENTLY HIDDEN // - - else if (prov.board.lists[widget.index].items.length == 1 && - prov.draggedItemState!.itemIndex == 0 && - prov.draggedItemState!.listIndex == widget.index) { - // print("LEFT LENGTH == 1"); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - // CASE : IF PREVIOUSLY PLACEHOLDER IS ADDED IN EMPTY LIST THEN EXPLICITLY REMOVE THAT PLACEHOLDER, AND MAKE THAT LIST EMPTY AGAIN // - if (prov.board.lists[draggedItemListIndex!] - .items[draggedItemIndex!].addedBySystem == - true) { - prov.board.lists[draggedItemListIndex].items - .removeAt(draggedItemIndex); - prov.board.lists[draggedItemListIndex].setState!(); - } else { - prov - .board - .lists[draggedItemListIndex] - .items[draggedItemIndex] - .placeHolderAt = PlaceHolderAt.none; - - prov.board.lists[draggedItemListIndex] - .items[draggedItemIndex].child = - prov.board.lists[draggedItemListIndex] - .items[draggedItemIndex].prevChild; - - prov.board.lists[draggedItemListIndex] - .items[draggedItemIndex].setState!(); - } - - // Placeholder is updated at current position // - prov.board.dragItemIndex = 0; - prov.board.dragItemOfListIndex = widget.index; - // log("UPDATED | ITEM= ${widget.itemIndex} | LIST= ${widget.listIndex}"); - prov.board.lists[prov.board.dragItemOfListIndex!] - .items[prov.board.dragItemIndex!].setState!(); - }); - } - } - } - return b!; - }, - child: Container( - margin: const EdgeInsets.only(bottom: 15, right: LIST_GAP), - width: prov.board.lists[widget.index].width!, - decoration: prov.board.listDecoration ?? - BoxDecoration( - color: prov.board.lists[widget.index].backgroundColor, - ), - child: draggableNotfier.draggableType == DraggableType.list && - prov.draggedItemState!.listIndex == widget.index - ? TweenAnimationBuilder( - duration: const Duration(milliseconds: 500), - curve: Curves.ease, - tween: Tween(begin: 0, end: 1), - builder: (context, value, child) { - return Opacity( - opacity: value, - child: child, - ); - }, - child: Opacity( - opacity: 0.4, child: prov.draggedItemState!.child)) - : Column(children: [ - GestureDetector( - onLongPress: () { - listProv.onListLongpress( - listIndex: widget.index, - context: context, - setstate: () => setState(() {})); - }, - child: prov.board.lists[widget.index].header ?? - Container( - width: prov.board.lists[widget.index].width, - color: prov - .board.lists[widget.index].headerBackgroundColor, - padding: const EdgeInsets.only( - left: 15, bottom: 10, top: 10, right: 0), - alignment: Alignment.centerLeft, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - prov.board.lists[widget.index].title, - style: prov.board.textStyle ?? - const TextStyle( - fontSize: 17, - color: Colors.black, - fontWeight: FontWeight.w500), - ), - SizedBox( - // padding: const EdgeInsets.all(5), - child: PopupMenuButton( - constraints: BoxConstraints( - minWidth: prov.board.lists[widget.index] - .width! * - 0.7, - maxWidth: prov.board.lists[widget.index] - .width! * - 0.7, - ), - itemBuilder: (ctx) { - return [ - PopupMenuItem( - value: 1, - child: Text( - "Add card", - style: prov.board.textStyle, - ), - ), - PopupMenuItem( - value: 2, - child: Text( - "Delete List", - style: prov.board.textStyle, - ), - ), - ]; - }, - onSelected: (value) async { - if (value == 1) { - listProv.addNewCard( - position: "TOP", - listIndex: widget.index); - } else if (value == 2) { - prov.board.lists - .removeAt(widget.index); - prov.board.setstate!(); - } - })), - ], - ), - ), - ), - Flexible( - child: ListView.builder( - // physics: const ClampingScrollPhysics() - controller: - prov.board.lists[widget.index].scrollController, - itemCount: prov.board.lists[widget.index].items.length, - shrinkWrap: true, - itemBuilder: (ctx, index) { - return Item( - itemIndex: index, - listIndex: widget.index, - ); - }, - - // itemCount: prov.items.length, - ), - ), - prov.board.lists[widget.index].footer ?? - Container( - padding: const EdgeInsets.only(left: 15), - height: 45, - width: prov.board.lists[widget.index].width, - color: prov - .board.lists[widget.index].footerBackgroundColor, - child: GestureDetector( - onTap: () async { - listProv.addNewCard( - position: "LAST", listIndex: widget.index); - }, - child: Row( - children: [ - const Icon( - Icons.add, - color: Colors.black, - size: 22, - ), - const SizedBox( - width: 5, - ), - Text( - 'NEW', - style: prov.board.textStyle ?? - const TextStyle( - color: Colors.black, - fontSize: 17, - fontWeight: FontWeight.w500), - ), - ], - ), - ), - ) - ]), - )); - } -} diff --git a/lib/custom/list_item.dart b/lib/custom/list_item.dart deleted file mode 100755 index 4ee9686..0000000 --- a/lib/custom/list_item.dart +++ /dev/null @@ -1,135 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../Provider/provider_list.dart'; - -class Item extends ConsumerStatefulWidget { - const Item({ - super.key, - required this.itemIndex, - this.color = Colors.pink, - required this.listIndex, - }); - final int itemIndex; - final int listIndex; - final Color color; - @override - ConsumerState createState() => _ItemState(); -} - -class _ItemState extends ConsumerState { - Offset location = Offset.zero; - bool newAdded = false; - var node = FocusNode(); - bool temp = false; - - @override - Widget build(BuildContext context) { - // log("BUILDED ${widget.itemIndex}"); - var prov = ref.read(ProviderList.boardProvider); - var cardProv = ref.read(ProviderList.cardProvider); - var draggableProv = ref.read(ProviderList.draggableNotifier); - - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - cardProv.calculateCardPositionSize( - listIndex: widget.listIndex, - itemIndex: widget.itemIndex, - context: context, - setsate: () => {setState(() {})}); - }); - return ValueListenableBuilder( - valueListenable: prov.valueNotifier, - builder: (ctx, a, b) { - if (draggableProv.isCardDragged) { - // item added by system in empty list, its widget/UI should not be manipulated on movements // - if (prov.board.lists[widget.listIndex].items.isEmpty) return b!; - - // CALCULATE SIZE AND POSITION OF ITEM // - if (cardProv.calculateSizePosition( - listIndex: widget.listIndex, itemIndex: widget.itemIndex)) { - return b!; - } - - // IF ITEM IS LAST ITEM OF LIST, DIFFERENT APPROACH IS USED // - - // if (cardProv.isLastItemDragged( - // listIndex: widget.listIndex, itemIndex: widget.itemIndex)) { - // // log("LAST ELEMENT DRAGGED"); - // return b!; - // } - - // DO NOT COMPARE ANYTHING WITH DRAGGED ITEM, IT WILL CAUSE ERRORS BECUSE ITS HIDDEN // - if ((prov.draggedItemState!.itemIndex == widget.itemIndex && - prov.draggedItemState!.listIndex == widget.listIndex)) { - // log("HERE"); - return b!; - } - - // if (widget.itemIndex - 1 >= 0 && - // prov.board.lists[widget.listIndex].items[widget.itemIndex - 1] - // .containsPlaceholder == - // true) { - // // log("FOR ${widget.listIndex} ${widget.itemIndex}"); - // prov.board.lists[widget.listIndex].items[widget.itemIndex].y = - // prov.board.lists[widget.listIndex].items[widget.itemIndex] - // .y! - - // prov.board.lists[widget.listIndex].items[widget.itemIndex-1].actualSize!.height; - // } - - if (cardProv.getYAxisCondition( - listIndex: widget.listIndex, itemIndex: widget.itemIndex)) { - // log("Y AXIS CONDITION"); - cardProv.checkForYAxisMovement( - listIndex: widget.listIndex, itemIndex: widget.itemIndex); - } else if (cardProv.getXAxisCondition( - listIndex: widget.listIndex, itemIndex: widget.itemIndex)) { - cardProv.checkForXAxisMovement( - listIndex: widget.listIndex, itemIndex: widget.itemIndex); - } - } - return b!; - }, - child: GestureDetector( - onLongPress: () { - cardProv.onLongpressCard( - listIndex: widget.listIndex, - itemIndex: widget.itemIndex, - context: context, - setsate: () => {setState(() {})}); - }, - child: draggableProv.isCardDragged && - prov.board.dragItemIndex == widget.itemIndex && - prov.draggedItemState!.itemIndex == widget.itemIndex && - prov.draggedItemState!.listIndex == widget.listIndex && - prov.board.dragItemOfListIndex! == widget.listIndex - ? Container( - margin: const EdgeInsets.only(bottom: 10), - decoration: BoxDecoration( - border: Border.all(color: Colors.grey.shade200), - borderRadius: BorderRadius.circular(4), - color: prov.board.lists[widget.listIndex] - .items[widget.itemIndex].backgroundColor ?? - Colors.white, - ), - width: prov.draggedItemState!.width, - height: prov.draggedItemState!.height, - ) - : cardProv.isCurrentElementDragged( - listIndex: widget.listIndex, - itemIndex: widget.itemIndex) - ? Container() - : GestureDetector( - onTap: () { - setState(() { - temp = !temp; - }); - }, - child: Container( - margin: const EdgeInsets.only(bottom: 5), - width: prov.board.lists[widget.listIndex] - .items[widget.itemIndex].width, - child: prov.board.lists[widget.listIndex] - .items[widget.itemIndex].child, - ), - ))); - } -} diff --git a/lib/custom/text_field.dart b/lib/custom/text_field.dart deleted file mode 100644 index 5354bc6..0000000 --- a/lib/custom/text_field.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -import '../Provider/provider_list.dart'; - -class TField extends ConsumerStatefulWidget { - const TField({super.key}); - - @override - ConsumerState createState() => _TFieldState(); -} - -class _TFieldState extends ConsumerState { - var node = FocusNode(); - @override - Widget build(BuildContext context) { - var prov = ref.read(ProviderList.boardProvider); - return TextFormField( - decoration: const InputDecoration( - enabledBorder: OutlineInputBorder(borderSide: BorderSide.none), - focusedBorder: OutlineInputBorder(borderSide: BorderSide.none), - disabledBorder: OutlineInputBorder(borderSide: BorderSide.none)), - autofocus: true, - enabled: true, - maxLines: null, - keyboardType: TextInputType.multiline, - style: prov.board.textStyle, - controller: prov.newCardState.textController, - focusNode: node, - ); - } -} diff --git a/lib/draggable/card_draggable.dart b/lib/draggable/card_draggable.dart deleted file mode 100644 index a93510e..0000000 --- a/lib/draggable/card_draggable.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:kanban_board/draggable/draggable_state.dart'; - -class CardDraggable extends DraggableState { - late int _index; - late int _listIndex; - - CardDraggable( - {required int index, - required listIndex, - required super.height, - required super.width, - required super.context, - required super.widget, - required super.currPos}) { - _index = index; - _listIndex = listIndex; - } - - int get index => _index; - int get listIndex => _listIndex; - - void setDataPosition({required int index, required int listIndex}) { - _index = index; - _listIndex = listIndex; - } -} diff --git a/lib/draggable/draggable_state.dart b/lib/draggable/draggable_state.dart deleted file mode 100644 index 64a6e0f..0000000 --- a/lib/draggable/draggable_state.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:flutter/material.dart'; - -enum DraggableType { card, list, none } - -abstract class DraggableState { - DraggableState( - {required this.height, - required this.width, - required this.context, - required this.widget, - required this.currPos}); - final double height; - final double width; - Offset currPos; - final BuildContext context; - final Widget widget; - - void setViewPosition(Offset currPos) { - this.currPos = currPos; - } -} diff --git a/lib/draggable/list_draggable.dart b/lib/draggable/list_draggable.dart deleted file mode 100644 index 0fcdf57..0000000 --- a/lib/draggable/list_draggable.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:kanban_board/draggable/draggable_state.dart'; - -class ListDraggable extends DraggableState { - ListDraggable( - {required int index, - required listIndex, - required super.height, - required super.width, - required super.context, - required super.widget, - required super.currPos}) { - _index = index; - } - late int _index; - - int get index => _index; - void setDataPosition(int index) { - _index = index; - } -} diff --git a/lib/draggable/presentation/dragged_card.dart b/lib/draggable/presentation/dragged_card.dart deleted file mode 100644 index c6825dd..0000000 --- a/lib/draggable/presentation/dragged_card.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:kanban_board/Provider/provider_list.dart'; -import 'package:kanban_board/draggable/draggable_state.dart'; - -class DraggedCard extends ConsumerStatefulWidget { - const DraggedCard({super.key}); - - @override - ConsumerState createState() => _DraggedCardState(); -} - -class _DraggedCardState extends ConsumerState { - @override - Widget build(BuildContext context) { - final boardProv = ref.read(ProviderList.boardProvider); - final boardListProv = ref.read(ProviderList.boardListProvider); - final draggableProv = ref.watch(ProviderList.draggableNotifier); - - return ValueListenableBuilder( - valueListenable: boardProv.valueNotifier, - builder: (ctx, Offset value, child) { - if (draggableProv.isCardDragged) { - boardListProv.maybeListScroll(); - } - return draggableProv.draggableType != DraggableType.none - ? Positioned( - left: value.dx, - top: value.dy, - child: Opacity( - opacity: 0.4, - child: boardProv.draggedItemState!.child, - ), - ) - : Container(); - }, - ); - } -} diff --git a/lib/kanban_board.dart b/lib/kanban_board.dart new file mode 100644 index 0000000..766285f --- /dev/null +++ b/lib/kanban_board.dart @@ -0,0 +1,6 @@ +library kanban_board; + +export 'src/board.dart'; +export 'src/board_inputs.dart'; +export 'src/widgets/widgets.dart'; +export 'src/controllers/board_controller.dart'; diff --git a/lib/models/board.dart b/lib/models/board.dart deleted file mode 100644 index bf5b6cf..0000000 --- a/lib/models/board.dart +++ /dev/null @@ -1,104 +0,0 @@ -import 'package:flutter/material.dart'; -import 'board_list.dart'; -import 'inputs.dart'; - -abstract class BaseDecoration { - Decoration decoration; - BaseDecoration(this.decoration); -} - -class BoardState { - List lists = []; - ScrollController controller; - VoidCallback? setstate; - int? dragListIndex = 0; - int? dragItemIndex = 0; - int? previousDragListIndex = 0; - int? dragItemOfListIndex = 0; - int? prevItemOfListIndex = 0; - double? displacementX; - double? displacementY; - Function(int? itemIndex, int? listIndex)? onItemTap; - Function(int? itemIndex, int? listIndex)? onItemLongPress; - Function(int? listIndex)? onListTap; - Function(int? listIndex)? onListLongPress; - final void Function(int? oldCardIndex, int? newCardIndex, int? oldListIndex, - int? newListIndex)? onItemReorder; - final void Function(int? oldListIndex, int? newListIndex)? onListReorder; - final void Function(String? oldName, String? newName)? onListRename; - final void Function(String? cardIndex, String? listIndex, String? text)? - onNewCardInsert; - Color? backgroundColor; - Color? cardPlaceholderColor; - Color? listPlaceholderColor; - final ScrollConfig? boardScrollConfig; - final ScrollConfig? listScrollConfig; - TextStyle? textStyle; - Decoration? listDecoration; - Decoration? boardDecoration; - TransitionHandler transitionHandler; - - BoardState({ - required this.lists, - required this.controller, - this.dragListIndex, - this.onItemTap, - this.onItemLongPress, - this.boardScrollConfig, - this.listScrollConfig, - this.setstate, - this.onListTap, - this.onItemReorder, - this.onListReorder, - this.onListRename, - this.onNewCardInsert, - this.displacementX, - this.displacementY, - this.onListLongPress, - this.dragItemIndex, - this.textStyle, - this.backgroundColor = Colors.white, - this.cardPlaceholderColor, - this.listPlaceholderColor, - this.listDecoration, - this.boardDecoration, - this.dragItemOfListIndex, - required this.transitionHandler, - }) { - textStyle = textStyle ?? - TextStyle( - color: Colors.grey.shade800, - fontSize: 19, - fontWeight: FontWeight.w400); - } -} - -class TransitionHandler { - final Widget Function(Widget child, Animation animation) - cardTransitionBuilder; - final Widget Function(Widget child, Animation animation) - listTransitionBuilder; - final Duration cardTransitionDuration; - final Duration listTransitionDuration; - - TransitionHandler( - {required this.cardTransitionBuilder, - required this.listTransitionBuilder, - required this.cardTransitionDuration, - required this.listTransitionDuration}); -} - -class NewCardState { - bool? isFocused; - int? listIndex; - int? cardIndex; - late TextEditingController textController; - - NewCardState({ - this.isFocused, - this.listIndex, - this.cardIndex, - }) { - textController = TextEditingController(); - } -} diff --git a/lib/models/board_list.dart b/lib/models/board_list.dart deleted file mode 100644 index 8663964..0000000 --- a/lib/models/board_list.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:flutter/material.dart'; -import 'item_state.dart'; - -class BoardList { - BuildContext? context; - double? x; - double? y; - double? height; - double? width; - Widget? child; - Widget? header; - Widget? footer; - Color? headerBackgroundColor; - Color? footerBackgroundColor; - Color? backgroundColor; - VoidCallback? setState; - List items = []; - TextEditingController nameController = TextEditingController(); - ScrollController scrollController; - - String title; - BoardList( - {required this.items, - this.context, - this.height, - this.width, - this.header, - this.footer, - this.setState, - this.headerBackgroundColor = const Color.fromARGB( - 255, - 247, - 248, - 252, - ), - this.footerBackgroundColor = const Color.fromARGB( - 255, - 247, - 248, - 252, - ), - this.x, - this.child, - this.backgroundColor = const Color.fromARGB( - 255, - 247, - 248, - 252, - ), - this.y, - required this.scrollController, - required this.title}) { - headerBackgroundColor = headerBackgroundColor ?? Colors.grey.shade300; - footerBackgroundColor = footerBackgroundColor ?? Colors.grey.shade300; - } -} diff --git a/lib/models/inputs.dart b/lib/models/inputs.dart deleted file mode 100644 index 031b415..0000000 --- a/lib/models/inputs.dart +++ /dev/null @@ -1,90 +0,0 @@ -import 'package:flutter/material.dart'; - -class BoardListsData { - final String? title; - Widget? header; - Color? headerBackgroundColor; - Color? footerBackgroundColor; - Widget? footer; - final List items; - Color? backgroundColor; - double width; - BoardListsData({ - this.title, - this.header, - this.footer, - required this.items, - this.footerBackgroundColor = const Color.fromRGBO(247, 248, 252, 1), - this.headerBackgroundColor = const Color.fromARGB(255, 247, 248, 252), - this.backgroundColor = const Color.fromARGB( - 255, - 247, - 248, - 252, - ), - this.width = 300, - }) { - footer = footer ?? - Container( - padding: const EdgeInsets.only(left: 15), - height: 45, - width: 300, - color: footerBackgroundColor, - child: const Row( - children: [ - Icon( - Icons.add, - color: Colors.black, - size: 22, - ), - SizedBox( - width: 10, - ), - Text( - 'NEW', - style: TextStyle( - color: Colors.black, - fontSize: 19, - fontWeight: FontWeight.bold), - ), - ], - ), - ); - header = header ?? - Container( - width: 250, - color: headerBackgroundColor, - padding: const EdgeInsets.only(left: 0, bottom: 12, top: 12), - alignment: Alignment.centerLeft, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - title ?? '', - style: const TextStyle( - fontSize: 20, - color: Colors.black, - fontWeight: FontWeight.bold), - ), - GestureDetector( - child: const SizedBox( - // padding: const EdgeInsets.all(5), - child: Icon(Icons.more_vert)), - ), - ], - ), - ); - } -} - -class ScrollConfig { - double offset; - Duration duration; - Curve curve; - - ScrollConfig({ - required this.offset, - required this.duration, - required this.curve, - }); -} diff --git a/lib/models/item_state.dart b/lib/models/item_state.dart deleted file mode 100755 index 52ca83e..0000000 --- a/lib/models/item_state.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:flutter/material.dart'; - -enum PlaceHolderAt { top, bottom, none } - -class ListItem { - BuildContext? context; - double? height; - double? width; - int listIndex; - VoidCallback? setState; - bool? isNew; - Color? backgroundColor = Colors.white; - int index; - double? x; - double? y; - PlaceHolderAt placeHolderAt; - Widget child; - bool? addedBySystem = false; - Size? actualSize; - Widget prevChild; - ListItem({ - this.context, - required this.child, - required this.prevChild, - required this.listIndex, - required this.index, - this.actualSize, - this.height, - this.setState, - this.addedBySystem, - this.placeHolderAt = PlaceHolderAt.none, - this.backgroundColor, - this.width, - this.x, - this.y, - this.isNew = false, - }); -} - -class DraggedItemState { - BuildContext? context; - double height; - double width; - int? listIndex; - int? itemIndex; - double x; - VoidCallback? setState; - double y; - Widget child; - DraggedItemState({ - this.context, - required this.child, - this.setState, - required this.listIndex, - required this.itemIndex, - required this.height, - required this.width, - required this.x, - required this.y, - }); -} diff --git a/lib/src/board.dart b/lib/src/board.dart new file mode 100644 index 0000000..376d162 --- /dev/null +++ b/lib/src/board.dart @@ -0,0 +1,359 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:kanban_board/src/controllers/states/scroll_state.dart'; +import 'package:kanban_board/src/widgets/board-group/board_groups_root.dart'; +import 'package:kanban_board/src/widgets/draggable/draggable_overlay.dart'; +import 'package:kanban_board/src/widgets/kanban_gesture_listener.dart'; +import 'board_inputs.dart'; +import 'constants/constants.dart'; +import 'controllers/controllers.dart'; +import 'helpers/board_state_controller_storage.dart'; + +typedef CardTransitionBuilder = Widget Function( + Widget child, Animation animation); + +typedef ListTransitionBuilder = Widget Function( + Widget child, Animation animation); + +typedef OnGroupItemMove = void Function( + int? oldCardIndex, int? newCardIndex, int? oldListIndex, int? newListIndex); + +typedef OnGroupMove = void Function(int? oldListIndex, int? newListIndex); + +typedef GroupItemBuilder = Widget Function( + BuildContext context, String groupId, int itemIndex); + +typedef GroupHeaderBuilder = Widget Function( + BuildContext context, String groupId); + +typedef GroupFooterBuilder = Widget Function( + BuildContext context, String groupId); + +class KanbanBoard extends StatefulWidget { + const KanbanBoard({ + this.boardScrollConfig = const BoardScrollConfig(), + this.boardDecoration, + required this.controller, + this.groups = const [], + required this.groupItemBuilder, + this.groupDecoration, + this.groupHeaderBuilder, + this.groupFooterBuilder, + this.groupGhost, + this.itemGhost, + this.onGroupItemMove, + this.onGroupMove, + this.groupScrollConfig = const GroupScrollConfig(), + this.trailing, + this.leading, + this.groupConstraints = const BoxConstraints(maxWidth: 300), + this.newCardWidget, + super.key, + }); + + /// It is the controller for the board. + /// It can be used to perform operations like adding a new group, adding a new item to a group, etc. + final KanbanBoardController controller; + + /// It is the list of groups for the board. + /// Each group contains a list of group-items. + /// Each group-item is a card in the board. + final List groups; + + /// This is called when a group-item is moved from one place to another. + /// It takes [oldCardIndex], [newCardIndex], [oldListIndex], [newListIndex] as input. + final OnGroupItemMove? onGroupItemMove; + + /// This is called when a group is moved from one place to another. + /// It takes [oldListIndex], [newListIndex] as input. + final OnGroupMove? onGroupMove; + + /// This is the configuration for the board scroll. + /// It is used to customize the scroll [speed], [curve], and [duration]. + /// Takes [Offset], [Duration], [Curve] as input. + final ScrollConfig boardScrollConfig; + + /// This is the configuration for the list scroll. + /// It is used to customize the scroll [speed], [curve], and [duration]. + /// Takes [Offset], [Duration], [Curve] as input. + final ScrollConfig groupScrollConfig; + + /// This is the decoration for the board. + final Decoration? boardDecoration; + + /// This is the decoration for the group. + final Decoration? groupDecoration; + + /// This is the builder for the group-header. + /// pass the [context], [groupId] to the builder. + /// it is called for each group. + final GroupHeaderBuilder? groupHeaderBuilder; + + /// This is the builder for the group-footer. + /// pass the [context], [groupId] to the builder. + /// it is called for each group. + final GroupFooterBuilder? groupFooterBuilder; + + /// This is the widget which is shown after the last group. + final Widget? trailing; + + /// This is the widget which is shown before the first group. + final Widget? leading; + + /// This is the builder for the group-item. + /// pass the [context], [groupId], and [itemIndex] to the builder. + final GroupItemBuilder groupItemBuilder; + + /// This is the constraints for the group. + final BoxConstraints groupConstraints; + + // It is the ghost widget for the group-item. + final Widget? itemGhost; + + // It is the ghost widget for the group. + final Widget? groupGhost; + + /// It's a builder for the new item added in + final Widget Function(BuildContext, String, String)? newCardWidget; + + @override + State createState() => _KanbanBoardState(); +} + +class _KanbanBoardState extends State { + @override + Widget build(BuildContext context) { + return ProviderScope( + child: MaterialApp( + debugShowCheckedModeBanner: false, + home: Board( + groups: widget.groups, + groupItemBuilder: widget.groupItemBuilder, + controller: widget.controller, + onGroupItemMove: widget.onGroupItemMove, + onGroupMove: widget.onGroupMove, + boardScrollConfig: widget.boardScrollConfig, + groupScrollConfig: widget.groupScrollConfig, + boardDecoration: widget.boardDecoration, + trailing: widget.trailing, + leading: widget.leading, + groupConstraints: widget.groupConstraints, + groupDecoration: widget.groupDecoration, + groupHeaderBuilder: widget.groupHeaderBuilder, + groupFooterBuilder: widget.groupFooterBuilder, + groupGhost: widget.groupGhost, + itemGhost: widget.itemGhost, + ), + )); + } +} + +class Board extends ConsumerStatefulWidget { + const Board({ + this.groups = const [], + required this.controller, + required this.groupItemBuilder, + this.onGroupItemMove, + this.onGroupMove, + this.boardScrollConfig = const BoardScrollConfig(), + this.groupScrollConfig = const GroupScrollConfig(), + this.boardDecoration, + this.groupDecoration, + this.trailing, + this.leading, + this.groupConstraints = const BoxConstraints(maxWidth: 300), + this.groupHeaderBuilder, + this.groupFooterBuilder, + this.groupGhost, + this.itemGhost, + this.newCardWidget, + super.key, + }); + final List groups; + final KanbanBoardController controller; + final GroupItemBuilder groupItemBuilder; + final OnGroupItemMove? onGroupItemMove; + final OnGroupMove? onGroupMove; + final ScrollConfig boardScrollConfig; + final ScrollConfig groupScrollConfig; + final Decoration? boardDecoration; + final Decoration? groupDecoration; + final Widget? trailing; + final Widget? leading; + final BoxConstraints groupConstraints; + final GroupHeaderBuilder? groupHeaderBuilder; + final GroupFooterBuilder? groupFooterBuilder; + final Widget? groupGhost; + final Widget? itemGhost; + final Widget? newCardWidget; + + @override + ConsumerState createState() => _BoardState(); +} + +class _BoardState extends ConsumerState { + /// [_boardScrollController] is the controller for the board scroll. + final ScrollController _boardScrollController = ScrollController(); + + /// [_boardStateController] is the controller to manage the state of the board. + late ChangeNotifierProvider _boardStateController; + late ChangeNotifierProvider + _groupItemStateController; + late ChangeNotifierProvider _groupStateController; + + /// [_getBoardOffset] is used to compute the offset of the board. + void _getBoardOffset() { + final double statusBarHeight = MediaQuery.of(context).padding.top; + var box = context.findRenderObject() as RenderBox; + ref.read(_boardStateController).boardOffset = Offset( + box.localToGlobal(Offset.zero).dx + BOARD_PADDING, + box.localToGlobal(Offset.zero).dy - statusBarHeight + BOARD_PADDING); + } + + /// [_activateBoardScrollListeners] activates the board scroll listeners. + /// only when drgaggable offset is updated, it notifies the newly group-items, groups came into view. + void _activateBoardScrollListeners() { + final boardState = ref.read(_boardStateController); + // Group Scroll Listener + _boardScrollController.addListener( + () { + if (boardState.isScrolling) { + /// This is to notify newly group-items came into view. + /// about the dragging widget position & calculate their position & size to show placeholder. + boardState.draggingState.feedbackOffset.value = Offset( + boardState.draggingState.feedbackOffset.value.dx + 0.00001, + boardState.draggingState.feedbackOffset.value.dy); + } + }, + ); + } + + /// [_initializeBoardGroups] is used to initialize the board groups. + List _initializeBoardGroups() { + List groups = []; + for (int index = 0; index < widget.groups.length; index++) { + final group = widget.groups[index]; + List items = []; + for (int itemIndex = 0; itemIndex < group.items.length; itemIndex++) { + items.add( + IKanbanBoardGroupItem( + groupIndex: index, + id: group.items[itemIndex].id, + key: GlobalKey(), + itemWidget: widget.groupItemBuilder(context, group.id, itemIndex), + ghost: widget.groupItemBuilder(context, group.id, itemIndex), + index: itemIndex, + setState: () => {}, + ), + ); + } + groups.add( + IKanbanBoardGroup( + scrollController: ScrollController(), + id: group.id, + name: group.name, + items: items, + customData: group.customData, + index: index, + setState: () => {}, + key: GlobalKey(), + ), + ); + } + return groups; + } + + @override + void initState() { + ///Initializing the [BoardStateController] provider. + _boardStateController = ChangeNotifierProvider( + (ref) => BoardStateController( + groups: _initializeBoardGroups(), + controller: widget.controller, + ), + ); + + //saving [_boardStateController] to the controller storage + BoardStateControllerStorage.I.addStateController( + widget.controller.boardId, + ref.read(_boardStateController), + ); + + ///Setting the ghost widgets for the group-item and item. + ref.read(_boardStateController) + ..itemGhost = widget.itemGhost + ..groupGhost = widget.groupGhost; + + ///Initializing the [ListItemProvider] provider. + ///It is used to manage the state of the group-item. + _groupItemStateController = + ChangeNotifierProvider( + (ref) => GroupItemStateController(ref.read(_boardStateController))); + + ///Initializing the [BoardListProvider] provider. + ///It is used to manage the state of the group. + _groupStateController = ChangeNotifierProvider( + (ref) => GroupStateController(ref.read(_boardStateController))); + + _activateBoardScrollListeners(); + super.initState(); + } + + @override + void didUpdateWidget(covariant Board oldWidget) { + if (oldWidget.groups != widget.groups) { + //TODO: Fix needed scrollcontroller breaking + // ref.read(_boardStateController).groups = _initializeBoardGroups(); + } + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) { + WidgetsBinding.instance.addPostFrameCallback((_) => _getBoardOffset()); + // TODO: check i've commented ignore: unused_local_variable + // final boardState = ref.watch(_boardStateController); + return Scaffold( + backgroundColor: Colors.white, + body: KanbanGestureListener( + boardgroupController: _groupStateController, + boardStateController: _boardStateController, + groupItemController: _groupItemStateController, + groupScrollConfig: widget.groupScrollConfig, + boardScrollConfig: widget.boardScrollConfig, + boardScrollController: _boardScrollController, + child: Container( + padding: + const EdgeInsets.only(top: BOARD_PADDING, left: BOARD_PADDING), + decoration: widget.boardDecoration, + child: Stack( + fit: StackFit.passthrough, + clipBehavior: Clip.none, + children: [ + BoardGroupsRoot( + groupItemBuilder: widget.groupItemBuilder, + leading: widget.leading, + trailing: widget.trailing, + header: widget.groupHeaderBuilder, + footer: widget.groupFooterBuilder, + groupDecoration: widget.groupDecoration, + groupConstraints: widget.groupConstraints, + boardStateController: _boardStateController, + groupItemStateController: _groupItemStateController, + groupStateController: _groupStateController, + boardScrollController: _boardScrollController, + ), + DraggableOverlay( + boardState: _boardStateController, + groupState: _groupStateController, + boardScrollController: _boardScrollController, + groupScrollConfig: widget.groupScrollConfig, + boardScrollConfig: widget.boardScrollConfig, + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/board_inputs.dart b/lib/src/board_inputs.dart new file mode 100644 index 0000000..407ddfa --- /dev/null +++ b/lib/src/board_inputs.dart @@ -0,0 +1,31 @@ +/// [KanbanBoardGroup] is used for Board's group input. +class KanbanBoardGroup { + /// It is the [id] of the group. + /// Every group should have a unique id. + String id; + + /// It is the [name] of the group. + /// If headerWidget is not provided, this will be used to display the name of the group. + String name; + + /// This is a [customData] that can be used to store any data. + T? customData; + + /// This contains the list of [items] in the group. + List items = const []; + + KanbanBoardGroup({ + this.customData, + required this.id, + required this.name, + this.items = const [], + }); +} + +/// [KanbanBoardGroupItem] is used for Board's group item input. +abstract class KanbanBoardGroupItem { + String get id; + + @override + String toString() => 'KanbanBoardListItem(id: $id)'; +} diff --git a/lib/src/constants/constants.dart b/lib/src/constants/constants.dart new file mode 100644 index 0000000..9be37aa --- /dev/null +++ b/lib/src/constants/constants.dart @@ -0,0 +1,30 @@ +// ignore_for_file: constant_identifier_names, non_constant_identifier_names + +const double BOARD_PADDING = 20; +const double LIST_GAP = 10; +const double CARD_GAP = 7; +const double DEFAULT_GROUP_WIDTH = 350; +// Group Scroll Boundary Threshold +const double GROUP_NEAR_SCROLL_BOUNDARY = 0; +const double GROUP_MID_SCROLL_BOUNDARY = 30; +const double GROUP_FAR_SCROLL_BOUNDARY = 70; +// Group Scroll speeds +const double GROUP_NEAR_SCROLL_MOVE = 300; +const double GROUP_MID_SCROLL_MOVE = 100; +const double GROUP_FAR_SCROLL_MOVE = 50; +// Group Scroll durations +const Duration GROUP_NEAR_SCROLL_DURATION = Duration(milliseconds: 100); +const Duration GROUP_MID_SCROLL_DURATION = Duration(milliseconds: 150); +const Duration GROUP_FAR_SCROLL_DURATION = Duration(milliseconds: 250); +// Board Scroll Boundary Threshold +const double BOARD_NEAR_SCROLL_BOUNDARY = 0; +const double BOARD_MID_SCROLL_BOUNDARY = 30; +const double BOARD_FAR_SCROLL_BOUNDARY = 70; +// Board Scroll speeds +const double BOARD_NEAR_SCROLL_MOVE = 120; +const double BOARD_MID_SCROLL_MOVE = 80; +const double BOARD_FAR_SCROLL_MOVE = 50; +// Board Scroll durations +const Duration BOARD_NEAR_SCROLL_DURATION = Duration(milliseconds: 100); +const Duration BOARD_MID_SCROLL_DURATION = Duration(milliseconds: 200); +const Duration BOARD_FAR_SCROLL_DURATION = Duration(milliseconds: 250); diff --git a/lib/src/constants/widget_styles.dart b/lib/src/constants/widget_styles.dart new file mode 100644 index 0000000..42c58ee --- /dev/null +++ b/lib/src/constants/widget_styles.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; +import 'package:kanban_board/src/constants/constants.dart'; + +import '../controllers/controllers.dart'; + +class DefaultStyles { + static TextStyle textStyle({ + Color? color, + double? fontSize, + FontWeight? fontWeight, + }) { + return TextStyle( + fontFamily: "Raleway", + fontSize: fontSize ?? 16, + color: color ?? Colors.black, + fontWeight: fontWeight ?? FontWeight.normal); + } + + static BoxDecoration groupDecoration() { + return const BoxDecoration( + color: Color.fromRGBO(247, 248, 249, 1), + borderRadius: BorderRadius.all(Radius.circular(5)), + ); + } + + static Widget groupHeader({ + required IKanbanBoardGroup group, + required Function(GroupOperationType type) onOperationSelect, + }) { + return Container( + width: DEFAULT_GROUP_WIDTH, + decoration: groupDecoration(), + padding: const EdgeInsets.only(left: 15, bottom: 10, top: 10, right: 0), + alignment: Alignment.centerLeft, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + group.name, + style: textStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + SizedBox( + // padding: const EdgeInsets.all(5), + child: PopupMenuButton( + color: Colors.white, + icon: const Icon( + Icons.more_horiz, + color: Colors.black, + size: 18, + ), + itemBuilder: (ctx) { + return [ + PopupMenuItem( + value: GroupOperationType.addItem, + child: Text( + "Add item", + style: textStyle(), + ), + ), + PopupMenuItem( + value: GroupOperationType.delete, + child: Text( + "Delete Group", + style: textStyle(), + ), + ), + ]; + }, + onSelected: (value) => onOperationSelect(value))), + ], + ), + ); + } + + static Widget groupFooter({ + required Function() onAddNewGroup, + }) { + return Container( + width: DEFAULT_GROUP_WIDTH, + decoration: groupDecoration(), + padding: const EdgeInsets.only( + bottom: 10, + ), + alignment: Alignment.centerLeft, + child: Row( + children: [ + IconButton( + onPressed: onAddNewGroup, + icon: const Icon( + Icons.add, + color: Colors.black, + ), + ), + Text( + "New", + style: textStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } + + static groupItemGhost() { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(5), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + spreadRadius: 1, + blurRadius: 2, + offset: const Offset(0, 2), // changes position of shadow + ), + ], + ), + ); + } +} diff --git a/lib/src/controllers/board_controller.dart b/lib/src/controllers/board_controller.dart new file mode 100644 index 0000000..18c8d8c --- /dev/null +++ b/lib/src/controllers/board_controller.dart @@ -0,0 +1,119 @@ +import 'package:flutter/material.dart'; +import 'package:kanban_board/src/board_inputs.dart'; +import 'package:kanban_board/src/helpers/board_state_controller_storage.dart'; +import 'package:kanban_board/src/widgets/textfield.dart'; +import 'package:uuid/uuid.dart'; + +import 'controllers.dart'; + +class KanbanBoardController { + late final String boardId; + + /// [KanbanBoardController] controls the behavior of some operations in [KanbanBoard]. + /// operations include adding a [group], adding a [group-item], etc. + KanbanBoardController() : boardId = const Uuid().v4(); + + /// [addGroup] adds a group to the board. + void addGroup(String id, dynamic groupData) {} + + /// [addGroupItem] adds a group-item to the board at a specific index. If the index is not provided, it will be added at the end. + void addGroupItem({ + required String groupId, + required KanbanBoardGroupItem groupItem, + int? index, + }) { + final boardController = + BoardStateControllerStorage.I.getStateController(boardId); + if (boardController == null) { + throw Exception('Board controller not found'); + } + final group = boardController.groups.firstWhere( + (element) => element.id == groupId, + orElse: (() => throw Exception('Group not found'))); + + group.items.insert( + index ?? group.items.length, + IKanbanBoardGroupItem( + groupIndex: group.index, + id: const Uuid().v4(), + index: index ?? 0, + key: GlobalKey(), + addedBySystem: true, + setState: () {}, + ), + ); + + group.setState(); + } + + /// [removeGroup] removes a group from the board. + void removeGroup(String groupId) { + final boardController = + BoardStateControllerStorage.I.getStateController(boardId); + if (boardController == null) { + throw Exception('Board controller not found'); + } + boardController.groups.removeWhere((element) => element.id == groupId); + boardController.notify(); + } + + /// Removes a item from the group in the board. + void removeGroupItem(String groupId, String itemId) { + final boardController = + BoardStateControllerStorage.I.getStateController(boardId); + if (boardController == null) { + throw Exception('Board controller not found'); + } + final group = boardController.groups.firstWhere( + (element) => element.id == groupId, + orElse: (() => throw Exception('Group not found with id: $groupId'))); + + group.items.removeWhere((element) => element.id == itemId); + group.setState(); + } + + /// [showNewCard] shows a new card in the group. + void showNewCard({ + required final String groupId, + final int? position, + final void Function(String)? onCardAdded, + }) { + final boardController = + BoardStateControllerStorage.I.getStateController(boardId); + if (boardController == null) { + throw Exception('Board controller not found'); + } + final group = boardController.groups.firstWhere( + (element) => element.id == groupId, + orElse: (() => throw Exception('Group not found'))); + + group.items.insert( + position ?? group.items.length, + IKanbanBoardGroupItem( + groupIndex: group.index, + id: const Uuid().v4(), + index: position ?? 0, + key: GlobalKey(), + addedBySystem: true, + setState: () {}, + //TODO: Why ghost is used? + ghost: + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(5), + ), + height: 120, + child: CardWithTextField( + onCompleteEditing: (value){ + group.items.removeAt(position ?? group.items.length); + onCardAdded?.call(value); + } + ), + ), + ), + ); + + group.setState(); + } +} diff --git a/lib/src/controllers/board_state_controller.dart b/lib/src/controllers/board_state_controller.dart new file mode 100644 index 0000000..b7df21e --- /dev/null +++ b/lib/src/controllers/board_state_controller.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'controllers.dart'; + +class BoardStateController extends ChangeNotifier { + BoardStateController({ + required this.controller, + this.groups = const [], + }); + + /// These are the list of internal group-state of the board. + /// Each group-state holds the state of group and its items. + List groups = []; + + /// The controller for the board. + KanbanBoardController controller; + + /// It holds whether the board is currently scrolling or not. + bool isScrolling = false; + + /// It holds the state of widget currently being dragged. + DraggableState draggingState = DraggableState.initial(); + + /// It holds the board position + Offset boardOffset = Offset.zero; + + // It holds the user's input ghost widget for the item. + Widget? itemGhost; + + // It holds the user's input ghost widget for the group. + Widget? groupGhost; + + void notify() { + notifyListeners(); + } +} diff --git a/lib/src/controllers/controllers.dart b/lib/src/controllers/controllers.dart new file mode 100644 index 0000000..d4e83d2 --- /dev/null +++ b/lib/src/controllers/controllers.dart @@ -0,0 +1,11 @@ +library controllers; + +/// All the controllers are exported here +export './board_state_controller.dart'; +export './group_state_controller.dart'; +export './group_item_state_controller.dart'; +export './board_controller.dart'; + +/// All the states are exported here +export './states/board_internal_states.dart'; +export './states/draggable_state.dart'; diff --git a/lib/src/controllers/group_item_state_controller.dart b/lib/src/controllers/group_item_state_controller.dart new file mode 100644 index 0000000..e568335 --- /dev/null +++ b/lib/src/controllers/group_item_state_controller.dart @@ -0,0 +1,576 @@ +import 'package:flutter/material.dart'; +import 'package:kanban_board/src/constants/constants.dart'; +import 'controllers.dart'; + +class GroupItemStateController extends ChangeNotifier { + GroupItemStateController(this.boardState); + final BoardStateController boardState; + TextEditingController newCardTextController = TextEditingController(); + + /// [computeItemPositionSize] is used to compute the position and size of the groupItem. + /// On each build of the groupItem, the position and size of the groupItem is computed. + void computeItemPositionSize( + {required int groupIndex, + required int itemIndex, + required BuildContext context, + required VoidCallback setstate}) { + final groupItem = boardState.groups[groupIndex].items[itemIndex]; + if (!context.mounted) return; + final itemRenderBox = context.findRenderObject() as RenderBox; + final location = itemRenderBox.localToGlobal(Offset.zero); + groupItem.updateWith( + setState: setstate, + position: Offset(location.dx - boardState.boardOffset.dx, + location.dy - boardState.boardOffset.dy), + size: groupItem.size == Size.zero + ? Size(itemRenderBox.size.width, itemRenderBox.size.height - CARD_GAP) + : null, + // actual size should not be updated, as groupItem might contain placeholder widget. + ); + + /// usecase: when item is not in view, and on scrolling it comes in view, then rebuild the widget to update the position and size. + /// to ensure key.currentContext is not null, for any widget that is in view. + if (groupItem.key.currentContext == null || + !groupItem.key.currentContext!.mounted) { + setstate(); + } + } + + /// This method is called on the [LongPress] event of the group-groupItem widget. + /// It is responsible for setting the state of the dragged groupItem with updated position and size. + void onLongPressItem({ + required int groupIndex, + required int itemIndex, + required BuildContext context, + required VoidCallback setState, + }) { + final groups = boardState.groups; + final groupItem = groups[groupIndex].items[itemIndex]; + final draggingState = boardState.draggingState; + final box = context.findRenderObject() as RenderBox; + final location = box.localToGlobal(Offset.zero); + groupItem.updateWith( + position: Offset(location.dx - boardState.boardOffset.dx, + location.dy - boardState.boardOffset.dy), + size: Size(box.size.width, box.size.height - CARD_GAP), + actualSize: Size(box.size.width, box.size.height - CARD_GAP), + setState: setState, + ); + + //If the groupItem is added by the system, then restrict the drag operation. + if (groupItem.addedBySystem) return; + + draggingState.feedbackOffset.value = groupItem.position!; + draggingState.updateWith( + feedbackSize: groupItem.size, + draggableType: DraggableType.item, + dragStartIndex: itemIndex, + dragStartGroupIndex: groupIndex, + currentIndex: itemIndex, + currentGroupIndex: groupIndex, + draggingWidget: Opacity( + opacity: 0.5, + child: SizedBox( + height: groupItem.size.height, + width: groupItem.size.width, + child: groupItem.itemWidget, + ), + ), + ); + setState(); + } + + /// [resetItemWidget] is used to reset the placeholder widget of the groupItem. + /// This is called whenever the groupItem is moved to a different position, to remove the placeholder from the last position. + void resetItemWidget() { + wrapWithPlaceHolder( + groupIndex: boardState.draggingState.currentGroupIndex, + itemIndex: boardState.draggingState.currentIndex, + reset: true, + ); + final groupItem = boardState + .groups[boardState.draggingState.currentGroupIndex] + .items[boardState.draggingState.currentIndex]; + groupItem.placeHolderAt = PlaceHolderAt.none; + } + + /// [calculateSizePosition] is used to calculate the size and position of the groupItem. + /// This is called on every move of dragging widget, to compute the position and size of the groupItem & group. + /// This helps in further computation of the placeholder widget. + bool calculateSizePosition({ + required int groupIndex, + required int itemIndex, + }) { + final group = boardState.groups[groupIndex]; + var groupItem = group.items[itemIndex]; + + /// This is used to check if the groupItem is in view or not. + if (groupItem.key.currentContext == null || + group.key.currentContext == null || + !groupItem.key.currentContext!.mounted) { + return true; + } + var itemRenderBox = groupItem.key.currentContext!.findRenderObject(); + var groupRenderBox = group.key.currentContext!.findRenderObject(); + if (itemRenderBox == null || groupRenderBox == null) return true; + + itemRenderBox = itemRenderBox as RenderBox; + groupRenderBox = groupRenderBox as RenderBox; + final position = itemRenderBox.localToGlobal(Offset.zero); + + // Update the size and position of the groupItem and group. + groupItem.updateWith( + size: itemRenderBox.size, + actualSize: groupItem.actualSize == Size.zero ? itemRenderBox.size : null, + position: Offset(position.dx - boardState.boardOffset.dx, + position.dy - boardState.boardOffset.dy), + ); + group.updateWith( + position: Offset( + groupRenderBox.localToGlobal(Offset.zero).dx - + boardState.boardOffset.dx, + groupRenderBox.localToGlobal(Offset.zero).dy - + boardState.boardOffset.dy), + size: Size( + groupRenderBox.size.width - LIST_GAP, groupRenderBox.size.height)); + return false; + } + + /// [wrapWithPlaceHolder] add/remove the placeholder widget to the groupItem. + /// It wraps placeholder/actual-widget with respective animation. + void wrapWithPlaceHolder( + {required int groupIndex, required int itemIndex, bool reset = false}) { + final groups = boardState.groups; + final groupItem = groups[groupIndex].items[itemIndex]; + final draggingState = boardState.draggingState; + + // update the ghost widget with placeholder wrapped groupItem. + groupItem.ghost = Column( + children: [ + groupItem.placeHolderAt == PlaceHolderAt.top && !reset + ? TweenAnimationBuilder( + duration: const Duration(milliseconds: 2000), + curve: Curves.ease, + tween: Tween(begin: 0, end: 1), + builder: (context, value, child) { + return Opacity( + opacity: value, + child: child, + ); + }, + child: Container( + margin: const EdgeInsets.only(bottom: CARD_GAP), + width: draggingState.feedbackSize.width, + height: draggingState.feedbackSize.height, + child: boardState.itemGhost), + ) + : Container(), + SlideTransition( + position: Tween( + begin: Offset( + 0, + groupItem.placeHolderAt == PlaceHolderAt.bottom + ? 1 + : groupItem.placeHolderAt == PlaceHolderAt.top + ? -1 + : 0), + end: const Offset(0, 0)) + .animate( + CurvedAnimation( + parent: groupItem.animationController!, + curve: Curves.easeInOut, + ), + ), + child: Container( + margin: const EdgeInsets.only(top: 0), + width: draggingState.feedbackSize.width, + height: draggingState.feedbackSize.height, + child: groupItem.itemWidget)), + groupItem.placeHolderAt == PlaceHolderAt.bottom && !reset + ? TweenAnimationBuilder( + duration: const Duration(milliseconds: 2000), + curve: Curves.ease, + tween: Tween(begin: 0, end: 1), + builder: (context, value, child) { + return Opacity( + opacity: value, + child: child, + ); + }, + child: Container( + margin: const EdgeInsets.only(top: CARD_GAP), + width: draggingState.feedbackSize.width, + height: draggingState.feedbackSize.height, + child: boardState.itemGhost), + ) + : Container(), + ], + ); + } + + /// [isCurrentSystemItem] check's, if the current placeholder is added by the system. + /// Placeholder is added by the system, when dragging widget drag over the empty group. + /// It also removes the placeholder from the last position. + /// [Returns] true, if the current item is added by the system, else false. + bool isCurrentSystemItem({required int groupIndex, required int itemIndex}) { + final draggingState = boardState.draggingState; + final groups = boardState.groups; + var groupItem = groups[groupIndex].items[itemIndex]; + if (groups[draggingState.currentGroupIndex] + .items[draggingState.currentIndex] + .addedBySystem) { + groups[draggingState.currentGroupIndex].items = []; + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + groups[draggingState.currentGroupIndex].setState(); + draggingState.currentIndex = itemIndex; + draggingState.currentGroupIndex = groupIndex; + groupItem.setState(); + }); + + return true; + } + return false; + } + + void handleSameGroupMove({required int groupIndex, required int itemIndex}) { + final groups = boardState.groups; + final groupItem = groups[groupIndex].items[itemIndex]; + final draggingState = boardState.draggingState; + bool willPlaceHolderAtBottom = + _bottomPlaceHolderPossibility(groupIndex, itemIndex); + if (getYAxisCondition(groupIndex: groupIndex, itemIndex: itemIndex)) { + // Reset the placeholder from the last position. + resetItemWidget(); + + // Set the placeholder at the top/bottom of the groupItem. + groupItem.placeHolderAt = + willPlaceHolderAtBottom ? PlaceHolderAt.bottom : PlaceHolderAt.top; + + // Wrap the groupItem with placeholder, to show the placeholder at the top/bottom of the groupItem. + if ((!groupItem.addedBySystem)) { + wrapWithPlaceHolder( + groupIndex: groupIndex, + itemIndex: itemIndex, + ); + } + // Check if the last placeholder is added by the system, so there should not be any manipulation with the same column y-axis widgets. + if (isCurrentSystemItem(groupIndex: groupIndex, itemIndex: itemIndex)) { + return; + } + + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + /// Check if the groupItem is in view or not. + if (groups[draggingState.currentGroupIndex] + .items[draggingState.currentIndex] + .key + .currentContext == + null || + !groups[draggingState.currentGroupIndex] + .items[draggingState.currentIndex] + .key + .currentContext! + .mounted) return; + + // Reset the placeholder from the last position. + if (itemIndex != draggingState.currentIndex && + draggingState.currentGroupIndex != groupIndex) { + resetItemWidget(); + } + // If the current item is animating, fasten the animation. + if (groups[draggingState.currentGroupIndex] + .items[draggingState.currentIndex] + .animationController! + .isAnimating) { + groups[draggingState.currentGroupIndex] + .items[draggingState.currentIndex] + .animationController! + .fling(); + } + // rebuild the last groupItem with removed placeholder. + groups[draggingState.currentGroupIndex] + .items[draggingState.currentIndex] + .setState(); + + // Update the current index and current group index. + draggingState.currentIndex = itemIndex; + draggingState.currentGroupIndex = groupIndex; + // rebuild the current groupItem with added placeholder. + groupItem.setState(); + + // Start the animation of the groupItem. + if (groupItem.animationController!.isCompleted) { + groupItem.animationController?.forward(from: -1.0); + } else { + groupItem.animationController?.forward(); + } + }); + } + } + + bool _topPlaceHolderPossibility(int groupIndex, int itemIndex) { + final groups = boardState.groups; + final groupItem = groups[groupIndex].items[itemIndex]; + final draggingState = boardState.draggingState; + + var willPlaceHolderAtTop = groupItem.placeHolderAt == PlaceHolderAt.bottom + ? (draggingState.feedbackOffset.value.dy < + groupItem.position!.dy + (groupItem.actualSize.height * 0.5)) + : ((draggingState.feedbackOffset.value.dy <= + groupItem.position!.dy + (groupItem.actualSize.height * 0.5)) && + (draggingState.feedbackSize.height + + draggingState.feedbackOffset.value.dy > + groupItem.position!.dy + (groupItem.actualSize.height))); + + return willPlaceHolderAtTop && + draggingState.axisDirection == AxisDirection.up && + groupItem.placeHolderAt != PlaceHolderAt.top && + draggingState.currentGroupIndex == groupIndex && + groupItem.addedBySystem != true; + } + + /// This method checks if the current groupItem can be combined with placeholder at bottom of it. + bool _bottomPlaceHolderPossibility(int groupIndex, int itemIndex) { + final groups = boardState.groups; + var groupItem = groups[groupIndex].items[itemIndex]; + final draggingState = boardState.draggingState; + + /// There are two cases for the bottom placeholder possibility: + + var willPlaceHolderAtBottom = + + /// 1. If there already exists a placeholder at the top of the groupItem: + /// In this case, the bottom placeholder will be placed only, if the draggable have crossed 80% height of the current item. + groupItem.placeHolderAt == PlaceHolderAt.top + ? (draggingState.feedbackSize.height + + draggingState.feedbackOffset.value.dy >= + groupItem.position!.dy + (groupItem.size.height * 0.75)) + + /// 2. If there is no placeholder attached to the groupItem: + /// In this case, the bottom placeholder will be placed only, if the draggable have crossed 50% height of the current item. + /// and the [draggable-top] is above the [groupItem-top]. + /// This is to ensure that the placeholder should be attached to current item only, cause all validation is done on the basis of current item [size] [position]. + : ((draggingState.feedbackSize.height + + draggingState.feedbackOffset.value.dy >= + groupItem.position!.dy + + (groupItem.actualSize.height * 0.5)) && + (draggingState.feedbackOffset.value.dy < + groupItem.position!.dy)); + + // if (willPlaceHolderAtBottom) { + // log("${draggingState.feedbackSize.height} + ${draggingState.feedbackOffset.value.dy} >= ${groupItem.position!.dy} + ${groupItem.actualSize.height * 0.8}"); + // } + + /// This is the last validation to check if the placeholder should be attached to the current item. + return willPlaceHolderAtBottom && + draggingState.axisDirection == AxisDirection.down && + + /// There should not exist bottom placeholder to the current item, if the current item is added by the system. + groupItem.placeHolderAt != PlaceHolderAt.bottom && + + /// The selected item for placeholder should be from the same group that is being dragged. + draggingState.currentGroupIndex == groupIndex && + + /// The current item should not be added by the system. + groupItem.addedBySystem != true; + } + + bool getYAxisCondition({required int groupIndex, required int itemIndex}) { + final groups = boardState.groups; + final groupItem = groups[groupIndex].items[itemIndex]; + final draggingState = boardState.draggingState; + + var right = (draggingState.feedbackOffset.value.dx <= + groups[groupIndex].position!.dx + + (groups[groupIndex].size.width * 0.4)) && + ((groups[groupIndex].position!.dx + groups[groupIndex].size.width <= + draggingState.feedbackSize.width + + draggingState.feedbackOffset.value.dx)); + + var left = ((draggingState.feedbackOffset.value.dx <= + groups[groupIndex].position!.dx) && + (draggingState.feedbackSize.width + + draggingState.feedbackOffset.value.dx >= + groups[groupIndex].position!.dx + + (groups[groupIndex].size.width * 0.6))); + + bool value = (_topPlaceHolderPossibility(groupIndex, itemIndex) || + _bottomPlaceHolderPossibility(groupIndex, itemIndex)) && + (right || left) && + draggingState.currentGroupIndex == groupIndex; + return value && groupItem.addedBySystem != true; + } + + bool getXAxisCondition({required int groupIndex, required int itemIndex}) { + final groups = boardState.groups; + final draggingState = boardState.draggingState; + var right = (draggingState.feedbackOffset.value.dx <= + groups[groupIndex].position!.dx + + (groups[groupIndex].size.width * 0.4)) && + ((groups[groupIndex].position!.dx + groups[groupIndex].size.width < + draggingState.feedbackSize.width + + draggingState.feedbackOffset.value.dx)); + + var left = ((draggingState.feedbackOffset.value.dx < + groups[groupIndex].position!.dx) && + (draggingState.feedbackSize.width + + draggingState.feedbackOffset.value.dx >= + groups[groupIndex].position!.dx + + (groups[groupIndex].size.width * 0.6))); + return (left || right) && draggingState.currentGroupIndex != groupIndex; + } + + void handleDiffGroupMove({required int groupIndex, required int itemIndex}) { + final groups = boardState.groups; + final groupItem = groups[groupIndex].items[itemIndex]; + final draggingState = boardState.draggingState; + + var canReplaceCurrent = + ((draggingState.feedbackOffset.value.dy >= groupItem.position!.dy) && + (groupItem.size.height + groupItem.position!.dy > + draggingState.feedbackOffset.value.dy + + (draggingState.feedbackSize.height / 2))); + var willPlaceHolderAtBottom = + (itemIndex == groups[groupIndex].items.length - 1 && + ((draggingState.feedbackSize.height * 0.6) + + draggingState.feedbackOffset.value.dy > + groupItem.position!.dy + groupItem.size.height)); + + if (canReplaceCurrent || willPlaceHolderAtBottom) { + resetItemWidget(); + groupItem.placeHolderAt = + willPlaceHolderAtBottom ? PlaceHolderAt.bottom : PlaceHolderAt.top; + + wrapWithPlaceHolder(groupIndex: groupIndex, itemIndex: itemIndex); + if (isCurrentSystemItem(groupIndex: groupIndex, itemIndex: itemIndex)) { + return; + } + + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + if (itemIndex != draggingState.currentIndex && + draggingState.currentGroupIndex != groupIndex) { + resetItemWidget(); + } + final animationController = groups[draggingState.currentGroupIndex] + .items[draggingState.currentIndex] + .animationController; + if (animationController?.isAnimating == true) { + animationController!.fling(); + } + groups[draggingState.currentGroupIndex] + .items[draggingState.currentIndex] + .setState(); + + draggingState.currentIndex = itemIndex; + draggingState.currentGroupIndex = groupIndex; + + groupItem.setState(); + if (groupItem.animationController!.isCompleted) { + groupItem.animationController?.forward(from: -1.0); + } else { + groupItem.animationController?.forward(); + } + }); + } + } + + bool isCurrentElementDragged( + {required int groupIndex, required int itemIndex}) { + final draggingState = boardState.draggingState; + + return draggingState.draggableType == DraggableType.item && + draggingState.dragStartIndex == itemIndex && + draggingState.dragStartGroupIndex == groupIndex; + } + + void updateCardPlaceholder({ + required int groupIndex, + required int itemIndex, + bool update = false, + }) { + resetItemWidget(); + final groupItem = boardState.groups[groupIndex].items[itemIndex]; + if (update) groupItem.setState(); + } + + /// This method is called on the onDragEnd event of the draggable widget + /// It is responsible for reordering the card in the board + void onDragEnd() { + final groups = boardState.groups; + final draggedState = boardState.draggingState; + + IKanbanBoardGroup group = boardState.groups[draggedState.currentGroupIndex]; + IKanbanBoardGroupItem groupItem = group.items[draggedState.currentIndex]; + + /// If the card is dropped in the same group. + if (draggedState.dragStartGroupIndex == draggedState.currentGroupIndex) { + /// Remove the groupItem from the index from where it was dragged, and insert it at the current index. + updateCardPlaceholder( + groupIndex: draggedState.currentGroupIndex, + itemIndex: draggedState.currentIndex, + update: true, + ); + group.items.insert( + draggedState.currentIndex, + groups[draggedState.dragStartGroupIndex] + .items + .removeAt(draggedState.dragStartIndex), + ); + } + + /// If the card is dropped in a different group. + else { + if (groupItem.addedBySystem) { + group.items.removeAt(draggedState.currentIndex); + group.items.insert( + draggedState.currentIndex, + groups[draggedState.dragStartGroupIndex] + .items + .removeAt(draggedState.dragStartIndex)); + } + + /// If the placeholder is at the bottom of any groupItem, insert the groupItem at the current index + 1. + /// This is because the groupItem at the current index will be shifted to the previous index. + else if (groupItem.placeHolderAt == PlaceHolderAt.bottom) { + updateCardPlaceholder( + groupIndex: draggedState.currentGroupIndex, + itemIndex: draggedState.currentIndex, + update: true, + ); + group.items.insert( + draggedState.currentIndex + 1, + groups[draggedState.dragStartGroupIndex] + .items + .removeAt(draggedState.dragStartIndex)); + } else { + updateCardPlaceholder( + groupIndex: draggedState.currentGroupIndex, + itemIndex: draggedState.currentIndex, + update: true, + ); + + /// If the placeholder is at the top of any groupItem, insert the groupItem at the current index. + /// This is because the groupItem at the current index will be shifted to the next index. + group.items.insert( + draggedState.currentIndex, + groups[draggedState.dragStartGroupIndex] + .items + .removeAt(draggedState.dragStartIndex)); + } + } + + /// Reset the placeholder of the groupItem. + /// This is done to remove the placeholder from the groupItem. + + /// Rebuild the current group to which the groupItem is dropped. + groups[draggedState.currentGroupIndex].setState(); + // Rebuild the group from which the groupItem was dragged. + groups[draggedState.dragStartGroupIndex].setState(); + + /// Reset the dragging state to its initial state. + boardState.draggingState = DraggableState.initial( + feedbackOffset: boardState.draggingState.feedbackOffset, + ); + + boardState.notify(); + } +} diff --git a/lib/src/controllers/group_state_controller.dart b/lib/src/controllers/group_state_controller.dart new file mode 100644 index 0000000..3b938ad --- /dev/null +++ b/lib/src/controllers/group_state_controller.dart @@ -0,0 +1,299 @@ +import 'dart:math'; + +import 'package:dotted_border/dotted_border.dart'; +import 'package:flutter/material.dart'; +import 'package:kanban_board/src/board.dart'; +import 'package:kanban_board/src/controllers/board_state_controller.dart'; +import 'package:kanban_board/src/constants/constants.dart'; +import 'package:kanban_board/src/helpers/widget_detail.helper.dart'; +import 'states/board_internal_states.dart'; +import 'states/draggable_state.dart'; + +class GroupStateController extends ChangeNotifier { + GroupStateController(this.boardState); + final BoardStateController boardState; + bool isScrolling = false; + + void calculateSizePosition( + {required BoardStateController boardState, + required int groupIndex, + required BuildContext context, + required VoidCallback setstate}) { + final group = boardState.groups[groupIndex]; + var groupRenderbox = context.findRenderObject() as RenderBox; + var position = groupRenderbox.localToGlobal(Offset.zero); + group + ..position = Offset(position.dx - boardState.boardOffset.dx, + position.dy - boardState.boardOffset.dy) + ..setState = setstate + ..size = + Size(groupRenderbox.size.width - LIST_GAP, groupRenderbox.size.height) + ..actualSize = group.actualSize == Size.zero + ? Size( + groupRenderbox.size.width - LIST_GAP, groupRenderbox.size.height) + : group.actualSize; + } + + /// This method is called on the onDragEnd event of the draggable widget + /// It is responsible for reordering the card in the board + void onDragEnd() { + final groups = boardState.groups; + final draggedState = boardState.draggingState; + + final pickedGroupCopy = groups[draggedState.dragStartGroupIndex].deepCopy(); + final currentGroup = groups[draggedState.currentGroupIndex]; + + final placeholderAt = currentGroup.placeHolderAt; + groups.removeAt(draggedState.dragStartGroupIndex); + groups.insert( + placeholderAt == PlaceHolderAt.left + ? draggedState.dragStartGroupIndex < draggedState.currentGroupIndex + ? max(draggedState.currentGroupIndex - 1, 0) + : draggedState.currentGroupIndex + : draggedState.dragStartGroupIndex > draggedState.currentGroupIndex + ? min(draggedState.currentGroupIndex + 1, groups.length) + : draggedState.currentGroupIndex, + pickedGroupCopy.updateWith(key: GlobalKey()), + ); + + /// Remove the groupItem from the index from where it was dragged, and insert it at the current index. + + /// Reset the placeholder of the groupItem. + /// This is done to remove the placeholder from the groupItem. + currentGroup.placeHolderAt = PlaceHolderAt.none; + + /// Reset the dragging state to its initial state. + boardState.draggingState = DraggableState.initial( + feedbackOffset: boardState.draggingState.feedbackOffset, + ); + + boardState.notify(); + } + + void onGroupLongpress( + {required BoardStateController boardState, + required int groupIndex, + required BuildContext context, + required VoidCallback setstate, + GroupFooterBuilder? footer, + GroupHeaderBuilder? header}) { + for (final group in boardState.groups) { + if (group.key.currentState == null || !group.key.currentState!.mounted) { + break; + } + final itemRenderBox = + group.key.currentState!.context.findRenderObject() as RenderBox; + final position = itemRenderBox.localToGlobal(Offset.zero); + group + ..position = Offset(position.dx - boardState.boardOffset.dx, + position.dy - boardState.boardOffset.dy) + ..size = Size( + itemRenderBox.size.width - LIST_GAP, itemRenderBox.size.height); + } + boardState.groups[groupIndex].setState = setstate; + boardState.draggingState.feedbackOffset.value = + boardState.groups[groupIndex].position!; + final group = boardState.groups[groupIndex]; + boardState.draggingState.updateWith( + draggingWidget: WidgetHelper.getDraggingGroup( + context: context, + group: group, + groupIndex: groupIndex, + footer: footer, + header: header), + draggableType: DraggableType.group, + feedbackSize: boardState.groups[groupIndex].size, + dragStartGroupIndex: groupIndex, + currentIndex: -1, + currentGroupIndex: groupIndex, + dragStartIndex: -1); + setstate(); + } + + /// [handleItemDragOverGroup] handles the placement of the dragged item, when it is dragged over a group. + /// It only handles two cases: + /// 1. When the item is dragged over a empty group. + /// 2. When the group from which the item is dragged gets empty || basically the only item in the group is dragged. + void handleItemDragOverGroup( + int groupIndex, + ) { + final draggingState = boardState.draggingState; + final group = boardState.groups[groupIndex]; + if (!canItemDropOverGroup(groupIndex)) return; + if (group.items.isNotEmpty && + !(group.items.length == 1 && + draggingState.dragStartGroupIndex == groupIndex && + draggingState.dragStartIndex == 0)) return; + + // print("HREE ${draggingState.dragStartGroupIndex} ${groupIndex} ${draggingState.dragStartGroupIndex == groupIndex}"); + if (group.items.isEmpty) { + /// Add the placeholder directly to the group as item. + group.items.add(IKanbanBoardGroupItem( + key: GlobalKey(), + id: 'system-added-placeholder', + index: 0, + setState: () => {}, + placeHolderAt: PlaceHolderAt.none, + itemWidget: Container( + margin: const EdgeInsets.only( + bottom: 10, + ), + width: draggingState.feedbackSize.width, + height: draggingState.feedbackSize.height, + child: DottedBorder( + child: const Center( + child: Text( + "Drop your task here", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + )), + ), + ), + addedBySystem: true, + groupIndex: groupIndex)); + } + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + /// Remove the placeholder from the previous group-item. + boardState.groups[draggingState.currentGroupIndex] + .items[draggingState.currentIndex].placeHolderAt = PlaceHolderAt.none; + + /// If the last placeholder was added by the system as item in the group, then remove it. + if (boardState.groups[draggingState.currentGroupIndex] + .items[draggingState.currentIndex].addedBySystem == + true) { + boardState.groups[draggingState.currentGroupIndex].items.removeAt(0); + boardState.groups[draggingState.currentGroupIndex].setState(); + } + + /// Update the dragging state. + draggingState.currentIndex = 0; + draggingState.currentGroupIndex = groupIndex; + + /// Rebuild the group. + group.setState(); + }); + } + + void handleGroupDragOverGroup(int groupIndex) { + final draggingState = boardState.draggingState; + final groups = boardState.groups; + final group = boardState.groups[groupIndex]; + final placeholderAt = canGroupDropOverGroup(groupIndex); + if (placeholderAt == PlaceHolderAt.none || + group.animationController!.isAnimating) return; + + /// Remove the placeholder from the previous group-item. + // wrapWithPlaceHolder( + // groupIndex: draggingState.currentGroupIndex, reset: true); + groups[draggingState.currentGroupIndex].animationOffset = Offset( + groups[draggingState.currentGroupIndex].placeHolderAt == + PlaceHolderAt.left + ? -1 + : groups[draggingState.currentGroupIndex].placeHolderAt == + PlaceHolderAt.right + ? 1 + : 0, + 0); + groups[draggingState.currentGroupIndex].placeHolderAt = PlaceHolderAt.none; + + // wrapWithPlaceHolder(groupIndex: groupIndex); + groups[groupIndex].placeHolderAt = placeholderAt; + + groups[groupIndex].animationOffset = Offset( + groups[groupIndex].placeHolderAt == PlaceHolderAt.left + ? -1 + : groups[groupIndex].placeHolderAt == PlaceHolderAt.right + ? 1 + : 0, + 0); + + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + if (groups[draggingState.currentGroupIndex] + .animationController! + .isAnimating) { + groups[draggingState.currentGroupIndex].animationController!.fling(); + } + groups[draggingState.currentGroupIndex].setState(); + draggingState.currentGroupIndex = groupIndex; + groups[groupIndex].setState(); + if (group.animationController!.isCompleted) { + group.animationController?.forward(from: -1.0); + } else { + group.animationController?.forward(); + } + }); + } + + /// [canItemDropOverGroup] checks if the dragged item can be dropped on the group or not. + /// It checks if the item is entering the group from the left or right side. + PlaceHolderAt canGroupDropOverGroup(int groupIndex) { + final draggingState = boardState.draggingState; + final group = boardState.groups[groupIndex]; + final draggableOffset = draggingState.feedbackOffset.value; + // if (groupIndex == 2) { + // print( + // "DRAGGABLE OFFSET =${draggableOffset.dx} ${group.position!.dx} ${group.size.width} ${draggingState.feedbackSize.width}"); + // } + bool canDrop = false; + PlaceHolderAt placeHolderAt = PlaceHolderAt.none; + if (group.placeHolderAt == PlaceHolderAt.left) { + final entringFromLeft = + (draggingState.feedbackSize.width + draggableOffset.dx > + group.position!.dx + (group.size.width * 0.75)) && + (group.position!.dx < draggableOffset.dx); + + canDrop = entringFromLeft; + placeHolderAt = PlaceHolderAt.right; + } else if (group.placeHolderAt == PlaceHolderAt.right) { + final entringFromRight = (draggableOffset.dx < + group.position!.dx + (group.actualSize.width * 0.5)) && + (group.position!.dx + group.size.width > + draggingState.feedbackSize.width + draggableOffset.dx); + canDrop = entringFromRight; + placeHolderAt = PlaceHolderAt.left; + } else { + final bool entringFromRight = (draggableOffset.dx < + group.position!.dx + (group.size.width * 0.4)) && + ((group.position!.dx + group.actualSize.width < + draggingState.feedbackSize.width + draggableOffset.dx)); + final entringFromLeft = + (draggingState.feedbackSize.width + draggableOffset.dx >= + group.position!.dx + (group.size.width * 0.5)) && + (draggableOffset.dx < group.position!.dx); + canDrop = entringFromLeft || entringFromRight; + placeHolderAt = entringFromLeft + ? PlaceHolderAt.right + : entringFromRight + ? PlaceHolderAt.left + : PlaceHolderAt.none; + } + if (canDrop && draggingState.dragStartGroupIndex != groupIndex) { + return placeHolderAt; + } + return PlaceHolderAt.none; + } + + /// [canItemDropOverGroup] checks if the dragged item can be dropped on the group or not. + /// It checks if the item is entering the group from the left or right side. + bool canItemDropOverGroup(int groupIndex) { + final draggingState = boardState.draggingState; + final group = boardState.groups[groupIndex]; + final draggableOffset = draggingState.feedbackOffset.value; + + /// Check if the item is entering the group from the left side. + final bool entringFromLeft = + (draggableOffset.dx < group.position!.dx + (group.size.width * 0.4)) && + ((group.position!.dx + group.size.width < + draggingState.feedbackSize.width + draggableOffset.dx)); + + /// Check if the item is entering the group from the right side. + final entringFromRight = + (draggingState.feedbackSize.width + draggableOffset.dx > + group.position!.dx + (group.size.width * 0.6)) && + (group.position!.dx + group.size.width > + draggingState.feedbackSize.width + draggableOffset.dx); + + /// For item to change group, it should not be in the same group. + return (entringFromLeft || entringFromRight) && + draggingState.currentGroupIndex != groupIndex; + } +} diff --git a/lib/src/controllers/scroll_handler.dart b/lib/src/controllers/scroll_handler.dart new file mode 100644 index 0000000..db34bdc --- /dev/null +++ b/lib/src/controllers/scroll_handler.dart @@ -0,0 +1,353 @@ +import 'package:flutter/material.dart'; +import 'board_state_controller.dart'; +import 'states/board_internal_states.dart'; +import 'states/draggable_state.dart'; +import 'states/scroll_state.dart'; + +/// [ScrollHandler] is used when the user is dragging a group-item near the edge of the group-end. +/// It can variate the velocity of the scroll based on the distance of the draggable to the edge of the group. +class ScrollHandler { + static Duration getDuration( + ScrollVelocity velocity, ScrollConfig scrollConfig) { + return velocity == ScrollVelocity.slow + ? scrollConfig.farBoundary.duration + : velocity == ScrollVelocity.medium + ? scrollConfig.midBoundary.duration + : scrollConfig.nearBoundary.duration; + } + + static double getOffsetToMove( + ScrollVelocity velocity, ScrollConfig scrollConfig) { + return velocity == ScrollVelocity.fast + ? scrollConfig.nearBoundary.offset + : velocity == ScrollVelocity.medium + ? scrollConfig.midBoundary.offset + : scrollConfig.farBoundary.offset; + } + + static ScrollVelocity getVelocity( + AxisDirection axisDirection, + double draggingWidgetPosition, + double maxViewport, + ScrollConfig scrollConfig) { + if (axisDirection == AxisDirection.down || + axisDirection == AxisDirection.right) { + return draggingWidgetPosition > + maxViewport - scrollConfig.farBoundary.boundary + ? ScrollVelocity.fast + : draggingWidgetPosition > + maxViewport - scrollConfig.midBoundary.boundary + ? ScrollVelocity.medium + : ScrollVelocity.slow; + } else { + return draggingWidgetPosition < scrollConfig.farBoundary.boundary + ? ScrollVelocity.fast + : draggingWidgetPosition < scrollConfig.midBoundary.boundary + ? ScrollVelocity.medium + : ScrollVelocity.slow; + } + } + + static Future _downsideScroll({ + required BoardStateController boardState, + required ScrollConfig scrollConfig, + required ScrollController scrollController, + required bool isScrolling, + required void Function(bool value) setScrolling, + }) async { + final draggingState = boardState.draggingState; + + /// If the draggable is not a group-item or the group is already scrolling, [return]. + /// This is to prevent the group from scrolling while the group is already scrolling. + if (draggingState.draggableType != DraggableType.item || isScrolling) { + return; + } + final draggingWidgetBottomPosition = draggingState.feedbackOffset.value.dy + + draggingState.feedbackSize.height; + + /// The maximum viewport of the scroll controller. + final scrollContext = scrollController.position.context.storageContext; + final scrollRenderBox = scrollContext.findRenderObject() as RenderBox; + final scrollStartPos = scrollRenderBox.localToGlobal(Offset.zero); + final maxViewport = scrollController.position.viewportDimension + + scrollStartPos.dy - + boardState.boardOffset.dy; + + /// If the scroll controller is not at the end and the dragging widget is near the end of the viewport, scroll the controller. + if (scrollController.offset < scrollController.position.maxScrollExtent && + draggingWidgetBottomPosition > + maxViewport - scrollConfig.farBoundary.boundary) { + setScrolling(true); + + /// [velocity] is used to determine the speed of the scroll. + /// It is based on the distance of the dragging widget to the end of the viewport. + final velocity = draggingWidgetBottomPosition > + maxViewport - scrollConfig.nearBoundary.boundary + ? ScrollVelocity.fast + : draggingWidgetBottomPosition > + maxViewport - scrollConfig.midBoundary.boundary + ? ScrollVelocity.medium + : ScrollVelocity.slow; + + await scrollController.animateTo( + scrollController.offset + + (velocity == ScrollVelocity.fast + ? scrollConfig.nearBoundary.offset + : velocity == ScrollVelocity.medium + ? scrollConfig.midBoundary.offset + : scrollConfig.farBoundary.offset), + duration: velocity == ScrollVelocity.slow + ? scrollConfig.farBoundary.duration + : velocity == ScrollVelocity.medium + ? scrollConfig.midBoundary.duration + : scrollConfig.nearBoundary.duration, + curve: scrollConfig.curve); + setScrolling(false); + _downsideScroll( + boardState: boardState, + scrollConfig: scrollConfig, + scrollController: scrollController, + isScrolling: false, + setScrolling: setScrolling); + } + return; + } + + static Future _upsideScroll({ + required BoardStateController boardState, + required ScrollConfig scrollConfig, + required ScrollController scrollController, + required bool isScrolling, + required void Function(bool value) setScrolling, + }) async { + final draggingState = boardState.draggingState; + + /// If the draggable is not a group-item or the group is already scrolling, [return]. + /// This is to prevent the group from scrolling while the group is already scrolling. + if (draggingState.draggableType != DraggableType.item || isScrolling) { + return; + } + final draggingWidgetTopPosition = draggingState.feedbackOffset.value.dy; + + /// The minimum viewport of the scroll controller. + final scrollContext = scrollController.position.context.storageContext; + final scrollRenderBox = scrollContext.findRenderObject() as RenderBox; + final scrollStartPos = scrollRenderBox.localToGlobal(Offset.zero); + final minViewport = scrollStartPos.dy - boardState.boardOffset.dy; + + /// If the scroll controller is not at the start and the dragging widget is near the start of the viewport, scroll the controller. + if (scrollController.offset > scrollController.position.minScrollExtent && + draggingWidgetTopPosition < + minViewport + scrollConfig.farBoundary.boundary) { + setScrolling(true); + + /// [velocity] is used to determine the speed of the scroll. + /// It is based on the distance of the dragging widget to the end of the viewport. + final velocity = draggingWidgetTopPosition < + minViewport + scrollConfig.nearBoundary.boundary + ? ScrollVelocity.fast + : draggingWidgetTopPosition < + minViewport + scrollConfig.midBoundary.boundary + ? ScrollVelocity.medium + : ScrollVelocity.slow; + + await scrollController.animateTo( + scrollController.offset - + (velocity == ScrollVelocity.fast + ? scrollConfig.nearBoundary.offset + : velocity == ScrollVelocity.medium + ? scrollConfig.midBoundary.offset + : scrollConfig.farBoundary.offset), + duration: velocity == ScrollVelocity.slow + ? scrollConfig.farBoundary.duration + : velocity == ScrollVelocity.medium + ? scrollConfig.midBoundary.duration + : scrollConfig.nearBoundary.duration, + curve: scrollConfig.curve); + + setScrolling(false); + _upsideScroll( + boardState: boardState, + scrollConfig: scrollConfig, + scrollController: scrollController, + isScrolling: false, + setScrolling: setScrolling); + } + return; + } + + static Future _rightsideScroll({ + required BoardStateController boardState, + required ScrollConfig scrollConfig, + required ScrollController scrollController, + required bool isScrolling, + required void Function(bool value) setScrolling, + }) async { + final draggingState = boardState.draggingState; + + /// If the draggable is not a group-item or the group is already scrolling, [return]. + /// This is to prevent the group from scrolling while the group is already scrolling. + if (draggingState.draggableType == DraggableType.none || isScrolling) { + return; + } + final draggingWidgetRightPosition = draggingState.feedbackOffset.value.dx + + draggingState.feedbackSize.width; + + /// The maximum viewport of the scroll controller. + final scrollContext = scrollController.position.context.storageContext; + final scrollRenderBox = scrollContext.findRenderObject() as RenderBox; + final scrollStartPos = scrollRenderBox.localToGlobal(Offset.zero); + final maxViewport = scrollController.position.viewportDimension + + scrollStartPos.dx - + boardState.boardOffset.dx; + + /// If the scroll controller is not at the end and the dragging widget is near the end of the viewport, scroll the controller. + if (scrollController.offset < scrollController.position.maxScrollExtent && + draggingWidgetRightPosition > + maxViewport - scrollConfig.farBoundary.boundary) { + setScrolling(true); + + /// [velocity] is used to determine the speed of the scroll. + /// It is based on the distance of the dragging widget to the end of the viewport. + final velocity = draggingWidgetRightPosition > + maxViewport - scrollConfig.nearBoundary.boundary + ? ScrollVelocity.fast + : draggingWidgetRightPosition > + maxViewport - scrollConfig.midBoundary.boundary + ? ScrollVelocity.medium + : ScrollVelocity.slow; + + await scrollController.animateTo( + scrollController.offset + + (velocity == ScrollVelocity.fast + ? scrollConfig.nearBoundary.offset + : velocity == ScrollVelocity.medium + ? scrollConfig.midBoundary.offset + : scrollConfig.farBoundary.offset), + duration: velocity == ScrollVelocity.slow + ? scrollConfig.farBoundary.duration + : velocity == ScrollVelocity.medium + ? scrollConfig.midBoundary.duration + : scrollConfig.nearBoundary.duration, + curve: scrollConfig.curve); + setScrolling(false); + _rightsideScroll( + boardState: boardState, + scrollConfig: scrollConfig, + scrollController: scrollController, + isScrolling: false, + setScrolling: setScrolling); + } + return; + } + + static Future _leftsideScroll({ + required BoardStateController boardState, + required ScrollConfig scrollConfig, + required ScrollController scrollController, + required bool isScrolling, + required void Function(bool value) setScrolling, + }) async { + final draggingState = boardState.draggingState; + + /// If the draggable is not a group-item or the group is already scrolling, [return]. + /// This is to prevent the group from scrolling while the group is already scrolling. + if (draggingState.draggableType == DraggableType.none || isScrolling) { + return; + } + final draggingWidgetLeftPosition = draggingState.feedbackOffset.value.dx; + + /// The minimum viewport of the scroll controller. + final minViewport = boardState.boardOffset.dx; + + /// If the scroll controller is not at the start and the dragging widget is near the start of the viewport, scroll the controller. + if (scrollController.offset > scrollController.position.minScrollExtent && + draggingWidgetLeftPosition < + minViewport + scrollConfig.farBoundary.boundary) { + setScrolling(true); + + /// [velocity] is used to determine the speed of the scroll. + /// It is based on the distance of the dragging widget to the end of the viewport. + final velocity = draggingWidgetLeftPosition < + minViewport + scrollConfig.nearBoundary.boundary + ? ScrollVelocity.fast + : draggingWidgetLeftPosition < + minViewport + scrollConfig.midBoundary.boundary + ? ScrollVelocity.medium + : ScrollVelocity.slow; + + await scrollController.animateTo( + scrollController.offset - + (velocity == ScrollVelocity.fast + ? scrollConfig.nearBoundary.offset + : velocity == ScrollVelocity.medium + ? scrollConfig.midBoundary.offset + : scrollConfig.farBoundary.offset), + duration: velocity == ScrollVelocity.slow + ? scrollConfig.farBoundary.duration + : velocity == ScrollVelocity.medium + ? scrollConfig.midBoundary.duration + : scrollConfig.nearBoundary.duration, + curve: scrollConfig.curve); + + setScrolling(false); + _leftsideScroll( + boardState: boardState, + scrollConfig: scrollConfig, + scrollController: scrollController, + isScrolling: false, + setScrolling: setScrolling); + } + return; + } +} + +class GroupScrollHandler { + static Future checkGroupScroll({ + required BoardStateController boardState, + required ScrollConfig scrollConfig, + required ScrollController scrollController, + required bool isScrolling, + required void Function(bool value) setScrolling, + }) async { + await ScrollHandler._downsideScroll( + boardState: boardState, + scrollConfig: scrollConfig, + scrollController: scrollController, + isScrolling: isScrolling, + setScrolling: setScrolling, + ); + await ScrollHandler._upsideScroll( + boardState: boardState, + scrollConfig: scrollConfig, + scrollController: scrollController, + isScrolling: isScrolling, + setScrolling: setScrolling, + ); + } +} + +class BoardScrollHandler { + static Future checkBoardScroll({ + required BoardStateController boardState, + required ScrollConfig scrollConfig, + required ScrollController scrollController, + required bool isScrolling, + required void Function(bool value) setScrolling, + }) async { + await ScrollHandler._rightsideScroll( + boardState: boardState, + scrollConfig: scrollConfig, + scrollController: scrollController, + isScrolling: isScrolling, + setScrolling: setScrolling, + ); + await ScrollHandler._leftsideScroll( + boardState: boardState, + scrollConfig: scrollConfig, + scrollController: scrollController, + isScrolling: isScrolling, + setScrolling: setScrolling, + ); + } +} diff --git a/lib/src/controllers/states/board_internal_states.dart b/lib/src/controllers/states/board_internal_states.dart new file mode 100644 index 0000000..b16a618 --- /dev/null +++ b/lib/src/controllers/states/board_internal_states.dart @@ -0,0 +1,221 @@ +import 'package:flutter/material.dart'; +import 'draggable_state.dart'; + +/// [IKanbanBoardGroup] is used to manage the internal state of the [Groups]. +class IKanbanBoardGroup { + /// The [id] of the group. + String id; + + /// This stores the [name] of the group. + String name; + + /// This is a [customData] that can be used to store any data. + dynamic customData; + + /// It is the index of the group in the board. + int index; + + /// It is the [key] of the group. + /// This is used to identify the group and compute its position offset. + GlobalKey key; + + /// This is the [ghost] widget that is used to show the dragging widget. + Widget? ghost; + + /// [placeHolderAt] determines if the placeholder is attached to group or not, and its position. + PlaceHolderAt placeHolderAt = PlaceHolderAt.none; + + /// It handles the group animation when dragging widget is near the group. + AnimationController? animationController; + + late Offset animationOffset; + + /// This holds the last computed [position] of the group. + Offset? position; + + /// This holds the last computed [size] of the group. + Size size = Size.zero; + + /// This holds the last computed [actualSize] of the group. + Size actualSize = Size.zero; + + /// [setState] is used to update the state of the group. + /// It is often used to invoke rebuild of the widget. + VoidCallback setState; + + /// This is the [scrollController] that is used to control the scroll of the group. + /// It is used to scroll the group when the dragging widget is near the edge of the group. + ScrollController scrollController = ScrollController(); + + /// This contains the list of [items] in the group. + List items = const []; + + IKanbanBoardGroup({ + required this.id, + required this.key, + required this.name, + required this.index, + required this.setState, + required this.scrollController, + this.animationController, + this.customData, + this.ghost, + this.items = const [], + this.placeHolderAt = PlaceHolderAt.none, + this.position, + this.size = const Size(0, 0), + this.actualSize = const Size(0, 0), + this.animationOffset = Offset.zero, + }); + + IKanbanBoardGroup updateWith({ + String? id, + GlobalKey? key, + String? name, + int? index, + VoidCallback? setState, + dynamic customData, + Widget? ghost, + Offset? position, + Size? size, + Size? actualSize, + List? items, + Offset? animationOffset, + PlaceHolderAt? placeHolderAt, + ScrollController? scrollController, + AnimationController? animationController, + }) { + this.id = id ?? this.id; + this.key = key ?? this.key; + this.name = name ?? this.name; + this.index = index ?? this.index; + this.setState = setState ?? this.setState; + this.customData = customData ?? this.customData; + this.ghost = ghost ?? this.ghost; + this.position = position ?? this.position; + this.size = size ?? this.size; + this.actualSize = actualSize ?? this.actualSize; + this.items = items ?? this.items; + this.animationOffset = animationOffset ?? this.animationOffset; + this.placeHolderAt = placeHolderAt ?? this.placeHolderAt; + this.scrollController = scrollController ?? this.scrollController; + this.animationController = animationController ?? this.animationController; + return this; + } + + // get deepcopy + IKanbanBoardGroup deepCopy() { + return IKanbanBoardGroup( + id: id, + key: key, + name: name, + index: index, + setState: setState, + scrollController: scrollController, + animationController: animationController, + customData: customData, + ghost: ghost, + items: items, + placeHolderAt: placeHolderAt, + position: position, + size: size, + actualSize: actualSize, + animationOffset: animationOffset, + ); + } +} + +/// [IKanbanBoardGroupItem] is used to manage the internal state of the [GroupItems]. +class IKanbanBoardGroupItem { + /// The [id] of the item. + String id; + + /// It is the [index] of the item in the group. + int index; + + /// It is the index of the group this item is associated with in the board. + int groupIndex; + + /// This is the [key] of the item. + /// This is used to identify the item and compute its position offset. + GlobalKey key; + + /// This is the [ghost] widget that is used to show the dragging widget. + Widget? ghost; + + /// This is the [itemWidget] that is used to show the item. + Widget? itemWidget; + + /// This holds the last computed [position] of the item. + Offset? position; + + /// [setState] is used to update the state of the group-item. + /// It is often used to invoke rebuild of the widget. + VoidCallback setState; + + /// This is used to determine if the item was added by the system. + /// This is used when group is empty, and the system adds an item to it, to show the placeholder. + bool addedBySystem = false; + + /// [placeHolderAt] used to determine if the placeholder is attached to item or not. + /// This also defines the position of the placeholder. It can be at the start or at the end of the item. + PlaceHolderAt placeHolderAt = PlaceHolderAt.none; + + /// This holds the last computed [size] of the item. + /// This will be affected by the placeholder. + Size size = Size.zero; + + /// This holds the last computed [actualSize] of the item. + /// This will not be affected by the placeholder. + Size actualSize = Size.zero; + + // This is used to animate the item when dragging widget is near the item. + late AnimationController? animationController; + + IKanbanBoardGroupItem({ + required this.key, + required this.id, + required this.index, + required this.setState, + required this.groupIndex, + this.ghost, + this.itemWidget, + this.position, + this.size = const Size(0, 0), + this.addedBySystem = false, + this.placeHolderAt = PlaceHolderAt.none, + this.animationController, + }); + + IKanbanBoardGroupItem updateWith({ + GlobalKey? key, + String? id, + int? index, + int? groupIndex, + VoidCallback? setState, + Widget? ghost, + Widget? itemWidget, + PlaceHolderAt? placeHolderAt, + Size? size, + Size? actualSize, + Offset? position, + }) { + this.key = key ?? this.key; + this.id = id ?? this.id; + this.index = index ?? this.index; + this.groupIndex = groupIndex ?? this.groupIndex; + this.setState = setState ?? this.setState; + this.ghost = ghost ?? this.ghost; + this.itemWidget = itemWidget ?? this.itemWidget; + this.placeHolderAt = placeHolderAt ?? this.placeHolderAt; + this.size = size ?? this.size; + this.actualSize = actualSize ?? this.actualSize; + this.position = position ?? this.position; + return this; + } +} + +/// [GroupOperationType] is used to determine the type of operation to be performed on the group. +enum GroupOperationType { addItem, delete } + +enum ScrollVelocity { slow, medium, fast } diff --git a/lib/src/controllers/states/draggable_state.dart b/lib/src/controllers/states/draggable_state.dart new file mode 100644 index 0000000..bc7df07 --- /dev/null +++ b/lib/src/controllers/states/draggable_state.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; + +/// [PlaceHolderAt] is used to determine the location of the placeholder widget. +enum PlaceHolderAt { top, bottom, left, right, none } + +/// [DraggableType] is used to determine the type of the widget that is being dragged. +enum DraggableType { item, group, none } + +/// It is used to manage the internal state of the [dragging-widget] widget. +class DraggableState { + DraggableState._({ + required this.feedbackOffset, + this.draggingWidget, + this.draggableType = DraggableType.none, + this.feedbackSize = Size.zero, + this.dragStartIndex = -1, + this.currentIndex = -1, + this.dragStartGroupIndex = -1, + this.currentGroupIndex = -1, + }); + + /// The widget currently being dragged. + Widget? draggingWidget; + + /// The type [DraggableType] of the widget currently being dragged. + DraggableType draggableType = DraggableType.none; + + /// The last computed size of the feedback widget being dragged. + Size feedbackSize = Size.zero; + + /// The last computed offset of the feedback widget being dragged. + ValueNotifier feedbackOffset; + + /// This is the last computed direction of the feedback widget being dragged. + /// This value is updated on every pixel movement of the feedback widget. + /// This value is used to determine the direction of the feedback widget, includes [AxisDirection.up], [AxisDirection.down], [AxisDirection.left], [AxisDirection.right]. + AxisDirection? axisDirection; + + /// The location that the dragging widget occupied before it started to drag. + int dragStartIndex = -1; + + /// The index that the dragging widget currently occupies. + int currentIndex = -1; + + /// The index of the group that the dragging widget started to drag from. + int dragStartGroupIndex = -1; + + /// The index of the group that the dragging widget currently occupies. + int currentGroupIndex = -1; + + bool hidePlaceholder = false; + + /// It is set [DraggableState] to its initial state. + factory DraggableState.initial({ + /// It's reference should not be changed, as it's being listened by the [ValueListenableBuilder]. + /// so, if [DraggableState] is being updated with initial values, after initialization of the board, then it must be passed as a parameter. + ValueNotifier? feedbackOffset, + }) { + return DraggableState._( + feedbackOffset: feedbackOffset ?? ValueNotifier(Offset.zero), + draggingWidget: null, + draggableType: DraggableType.none, + feedbackSize: Size.zero, + dragStartIndex: -1, + currentIndex: -1, + dragStartGroupIndex: -1, + currentGroupIndex: -1, + ); + } + + /// It is used to update the state of the [DraggableState], with the new values. + DraggableState updateWith({ + Widget? draggingWidget, + DraggableType? draggableType, + Size? feedbackSize, + int? dragStartIndex, + int? currentIndex, + int? dragStartGroupIndex, + int? currentGroupIndex, + bool? hidePlaceholder, + }) { + this.draggingWidget = draggingWidget ?? this.draggingWidget; + this.draggableType = draggableType ?? this.draggableType; + this.feedbackSize = feedbackSize ?? this.feedbackSize; + this.dragStartIndex = dragStartIndex ?? this.dragStartIndex; + this.currentIndex = currentIndex ?? this.currentIndex; + this.dragStartGroupIndex = dragStartGroupIndex ?? this.dragStartGroupIndex; + this.currentGroupIndex = currentGroupIndex ?? this.currentGroupIndex; + this.hidePlaceholder = hidePlaceholder ?? this.hidePlaceholder; + return this; + } +} diff --git a/lib/src/controllers/states/scroll_state.dart b/lib/src/controllers/states/scroll_state.dart new file mode 100644 index 0000000..bbb3b84 --- /dev/null +++ b/lib/src/controllers/states/scroll_state.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:kanban_board/src/constants/constants.dart'; + +/// [ScrollConfig] is used to configure the scroll behaviour. +class ScrollConfig { + final Boundary nearBoundary; + final Boundary midBoundary; + final Boundary farBoundary; + final Curve curve; + + const ScrollConfig({ + required this.nearBoundary, + required this.midBoundary, + required this.farBoundary, + required this.curve, + }); +} + +class GroupScrollConfig extends ScrollConfig { + const GroupScrollConfig():super( + nearBoundary: const _GroupNearBoundary(), + midBoundary: const _GroupMidBoundary(), + farBoundary: const _GroupFarBoundary(), + curve: Curves.linear, + ); +} + +class BoardScrollConfig extends ScrollConfig { + const BoardScrollConfig():super( + nearBoundary: const _BoardNearBoundary(), + midBoundary: const _BoardMidBoundary(), + farBoundary: const _BoardFarBoundary(), + curve: Curves.linear, + ); +} + +/// [Boundary] is used to define the boundary of the scroll, with its respective offset and duration. +class Boundary { + /// [boundary] is the distance from the edge of the group. + final double boundary; + + /// [offset] is the distance to be scrolled. + final double offset; + + /// [duration] is the duration of the scroll. + final Duration duration; + + const Boundary({ + required this.boundary, + required this.offset, + required this.duration, + }); +} + +// Group Scroll Boundaries +class _GroupFarBoundary extends Boundary { + const _GroupFarBoundary({ + double boundary = GROUP_FAR_SCROLL_BOUNDARY, + double offset = GROUP_FAR_SCROLL_MOVE, + Duration duration = GROUP_FAR_SCROLL_DURATION, + }) : super(boundary: boundary, offset: offset, duration: duration); +} + +class _GroupMidBoundary extends Boundary { + const _GroupMidBoundary({ + double boundary = GROUP_MID_SCROLL_BOUNDARY, + double offset = GROUP_MID_SCROLL_MOVE, + Duration duration = GROUP_MID_SCROLL_DURATION, + }) : super(boundary: boundary, offset: offset, duration: duration); +} + +class _GroupNearBoundary extends Boundary { + const _GroupNearBoundary({ + double boundary = GROUP_NEAR_SCROLL_BOUNDARY, + double offset = GROUP_NEAR_SCROLL_MOVE, + Duration duration = GROUP_NEAR_SCROLL_DURATION, + }) : super(boundary: boundary, offset: offset, duration: duration); +} + +// Board Scroll Boundaries + +class _BoardFarBoundary extends Boundary { + const _BoardFarBoundary({ + double boundary = BOARD_FAR_SCROLL_BOUNDARY, + double offset = BOARD_FAR_SCROLL_MOVE, + Duration duration = BOARD_FAR_SCROLL_DURATION, + }) : super(boundary: boundary, offset: offset, duration: duration); +} + +class _BoardMidBoundary extends Boundary { + const _BoardMidBoundary({ + double boundary = BOARD_MID_SCROLL_BOUNDARY, + double offset = BOARD_MID_SCROLL_MOVE, + Duration duration = BOARD_MID_SCROLL_DURATION, + }) : super(boundary: boundary, offset: offset, duration: duration); +} + +class _BoardNearBoundary extends Boundary { + const _BoardNearBoundary({ + double boundary = BOARD_NEAR_SCROLL_BOUNDARY, + double offset = BOARD_NEAR_SCROLL_MOVE, + Duration duration = BOARD_NEAR_SCROLL_DURATION, + }) : super(boundary: boundary, offset: offset, duration: duration); +} diff --git a/lib/src/helpers/board_state_controller_storage.dart b/lib/src/helpers/board_state_controller_storage.dart new file mode 100644 index 0000000..6a72e01 --- /dev/null +++ b/lib/src/helpers/board_state_controller_storage.dart @@ -0,0 +1,23 @@ + +import 'package:kanban_board/src/controllers/controllers.dart'; + +class BoardStateControllerStorage{ + BoardStateControllerStorage._(); + static final BoardStateControllerStorage _instance = BoardStateControllerStorage._(); + static BoardStateControllerStorage get I => _instance; + + + final Map _controllers = {}; + + void addStateController(String key, BoardStateController controller){ + _controllers[key] = controller; + } + + BoardStateController? getStateController(String key){ + return _controllers[key]; + } + + void removeStateController(String key){ + _controllers.remove(key); + } +} \ No newline at end of file diff --git a/lib/src/helpers/widget_detail.helper.dart b/lib/src/helpers/widget_detail.helper.dart new file mode 100644 index 0000000..17d560a --- /dev/null +++ b/lib/src/helpers/widget_detail.helper.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:kanban_board/src/board.dart'; +import 'package:kanban_board/src/constants/widget_styles.dart'; +import 'package:kanban_board/src/controllers/controllers.dart'; + +class WidgetHelper { + static Widget getDraggingGroup( + {required BuildContext context, + required IKanbanBoardGroup group, + required int groupIndex, + GroupHeaderBuilder? header, + GroupFooterBuilder? footer}) { + return SizedBox( + height: group.size.height, + width: group.size.width, + child: Column(mainAxisSize: MainAxisSize.min, children: [ + /// This builds the header of the group. + /// If the [GroupHeaderBuilder] is not provided, then it uses the default header. + header != null + ? header(context, group.id) + : DefaultStyles.groupHeader(group: group, onOperationSelect: (_) {}), + + /// This builds the body of the group. + /// This renders the list of items in the group. + Flexible( + child: ListView.builder( + itemCount: group.items.length, + shrinkWrap: true, + itemBuilder: (ctx, index) { + return group.items[index].itemWidget; + }, + ), + ), + + /// This builds the footer of the group. + /// If the [GroupFooterBuilder] is not provided, then it uses the default footer. + footer != null + ? footer(context, group.id) + : DefaultStyles.groupFooter(onAddNewGroup: () => {}) + ]), + ); + } +} diff --git a/lib/src/widgets/board-group/board_group.dart b/lib/src/widgets/board-group/board_group.dart new file mode 100644 index 0000000..b029aa7 --- /dev/null +++ b/lib/src/widgets/board-group/board_group.dart @@ -0,0 +1,240 @@ +import 'dart:developer'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:kanban_board/src/constants/constants.dart'; +import 'package:kanban_board/src/constants/widget_styles.dart'; +import 'package:kanban_board/src/board.dart'; +import 'package:kanban_board/src/controllers/controllers.dart'; +import 'package:kanban_board/src/widgets/widgets.dart'; + +class BoardGroup extends ConsumerStatefulWidget { + const BoardGroup( + {required this.boardStateController, + required this.groupIndex, + required this.groupConstraints, + required this.groupDecoration, + required this.itemBuilder, + required this.groupItemStateContoller, + required this.groupStateController, + this.footer, + this.header, + super.key}); + final int groupIndex; + final ChangeNotifierProvider boardStateController; + final ChangeNotifierProvider + groupItemStateContoller; + final ChangeNotifierProvider groupStateController; + final BoxConstraints groupConstraints; + final Decoration? groupDecoration; + final GroupFooterBuilder? footer; + final GroupHeaderBuilder? header; + final GroupItemBuilder? itemBuilder; + + @override + ConsumerState createState() => _BoardGroupState(); +} + +class _BoardGroupState extends ConsumerState { + /// [_scrollController] is used to control the scroll of the group. + late ScrollController _scrollController; + + bool resetPlaceholder = false; + + /// It is used to handle the operations on the group. + /// It is called when an operation is selected on the group through default header popup menu. + void onOperationSelect(GroupOperationType type) { + log("Operation selected $type"); + } + + void _activateBoardGroupScrollListeners() { + final boardState = ref.read(widget.boardStateController); + final groupState = ref.read(widget.groupStateController); + for (final group in boardState.groups) { + // Group Scroll Listener + //TODO: remove listener on dispose + group.scrollController.addListener(() { + if (groupState.isScrolling) { + /// This is to notify newly group-items came into view. + /// about the dragging widget position & calculate their position & size to show placeholder. + boardState.draggingState.feedbackOffset.value = Offset( + boardState.draggingState.feedbackOffset.value.dx, + boardState.draggingState.feedbackOffset.value.dy + 0.00001); + } + }); + } + } + + void _onDragUpdate(Offset draggableOffset, VoidCallback setstate) { + final boardState = ref.read(widget.boardStateController); + final draggingState = ref.read(widget.boardStateController).draggingState; + final group = boardState.groups[widget.groupIndex]; + group.setState = setstate; + if (draggingState.draggableType == DraggableType.none) return; + var box = context.findRenderObject(); + var groupRenderBox = group.key.currentContext!.findRenderObject(); + if (box == null || groupRenderBox == null) return; + box = box as RenderBox; + groupRenderBox = groupRenderBox as RenderBox; + final groupPosition = groupRenderBox.localToGlobal(Offset.zero); + group.position = Offset(groupPosition.dx - boardState.boardOffset.dx, + groupPosition.dy - boardState.boardOffset.dy); + group + ..size = + Size(groupRenderBox.size.width - LIST_GAP, groupRenderBox.size.height) + ..actualSize = + group.actualSize == Size.zero ? group.size : group.actualSize; + if (draggingState.draggableType == DraggableType.item) { + ref + .read(widget.groupStateController) + .handleItemDragOverGroup(widget.groupIndex); + } else { + ref + .read(widget.groupStateController) + .handleGroupDragOverGroup(widget.groupIndex); + } + } + + @override + void initState() { + _scrollController = ref + .read(widget.boardStateController) + .groups[widget.groupIndex] + .scrollController; + _activateBoardGroupScrollListeners(); + + super.initState(); + } + + @override + void dispose() { + _scrollController.removeListener(() {}); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final group = ref.watch(widget.boardStateController + .select((value) => value.groups[widget.groupIndex])); + final draggingState = ref.read(widget.boardStateController).draggingState; + + return ValueListenableBuilder( + key: group.key, + valueListenable: draggingState.feedbackOffset, + builder: (context, draggableOffset, b) { + _onDragUpdate(draggableOffset, () => setState(() {})); + return b!; + }, + child: draggingState.dragStartGroupIndex == widget.groupIndex && + draggingState.currentGroupIndex != widget.groupIndex + ? Container() + : GroupPlaceholderWrapper( + groupIndex: widget.groupIndex, + groupStateController: widget.groupStateController, + boardStateController: widget.boardStateController, + child: Align( + alignment: Alignment.topCenter, + child: Container( + margin: const EdgeInsets.only(bottom: 15, right: LIST_GAP), + width: widget.groupConstraints.maxWidth, + decoration: + widget.groupDecoration ?? DefaultStyles.groupDecoration(), + + ///If the current draggable is [this] group and it is being dragged over the current group, then animate. + child: GestureDetector( + onLongPress: () => + ref.read(widget.groupStateController).onGroupLongpress( + boardState: ref.read(widget.boardStateController), + groupIndex: widget.groupIndex, + context: context, + setstate: () => setState(() {}), + footer: widget.footer, + header: widget.header, + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + /// This builds the header of the group. + /// If the [GroupHeaderBuilder] is not provided, then it uses the default header. + _buildHeader(context, group), + + /// This builds the body of the group. + /// This renders the list of items in the group. + Flexible( + child: ListView.builder( + controller: _scrollController, + itemCount: group.items.length, + itemBuilder: (ctx, index) { + return GroupItem( + boardState: widget.boardStateController, + boardGroupState: widget.groupStateController, + groupItemState: widget.groupItemStateContoller, + itemIndex: index, + groupIndex: widget.groupIndex, + ); + }, + ), + ), + + /// This builds the footer of the group. + /// If the [GroupFooterBuilder] is not provided, then it uses the default footer. + _buildFooter(context, group), + ]), + ), + ), + ), + ), + ); + } + + Widget _buildHeader(BuildContext context, IKanbanBoardGroup group) { + return widget.header != null + ? widget.header!(context, group.id) + : DefaultStyles.groupHeader( + group: group, onOperationSelect: onOperationSelect); + } + + Widget _buildFooter(BuildContext context, IKanbanBoardGroup group) { + return widget.footer != null + ? widget.footer!(context, group.id) + : DefaultStyles.groupFooter( + onAddNewGroup: () => + {onOperationSelect(GroupOperationType.addItem)}); + } + + void _scrollToMax() async { + if (_scrollController.position.pixels == + _scrollController.position.maxScrollExtent) { + return; + } + await _scrollController.animateTo( + _scrollController.position.pixels + + _scrollController.position.extentAfter, + duration: Duration( + milliseconds: (int.parse(_scrollController.position.extentAfter + .toString() + .substring(0, 3) + .split('.') + .first))), + curve: Curves.linear, + ); + _scrollToMax(); + } + + void scrollToMin() async { + if (_scrollController.position.pixels == + _scrollController.position.minScrollExtent) { + return; + } + log(_scrollController.position.extentBefore.toString()); + await _scrollController.animateTo( + _scrollController.position.pixels - + _scrollController.position.extentBefore, + duration: Duration( + milliseconds: (int.parse(_scrollController.position.extentBefore + .toString() + .substring(0, 3) + .split('.') + .first))), + curve: Curves.linear, + ); + scrollToMin(); + } +} diff --git a/lib/src/widgets/board-group/board_groups_root.dart b/lib/src/widgets/board-group/board_groups_root.dart new file mode 100644 index 0000000..4c6e13e --- /dev/null +++ b/lib/src/widgets/board-group/board_groups_root.dart @@ -0,0 +1,88 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:kanban_board/src/controllers/group_state_controller.dart'; +import 'package:kanban_board/src/controllers/board_state_controller.dart'; +import 'package:kanban_board/src/controllers/group_item_state_controller.dart'; +import 'package:kanban_board/src/board.dart'; +import 'board_group.dart'; + +class BoardGroupsRoot extends ConsumerStatefulWidget { + const BoardGroupsRoot( + {required this.boardStateController, + required this.groupItemBuilder, + required this.leading, + required this.trailing, + required this.groupConstraints, + required this.groupDecoration, + required this.groupItemStateController, + required this.groupStateController, + required this.boardScrollController, + this.header, + this.footer, + super.key}); + final ChangeNotifierProvider boardStateController; + final ChangeNotifierProvider + groupItemStateController; + final ChangeNotifierProvider groupStateController; + final ScrollController boardScrollController; + final Widget? trailing; + final Widget? leading; + final BoxConstraints groupConstraints; + final Decoration? groupDecoration; + final GroupItemBuilder groupItemBuilder; + final GroupHeaderBuilder? header; + final GroupFooterBuilder? footer; + @override + ConsumerState createState() => _BoardGroupsRootState(); +} + +class _BoardGroupsRootState extends ConsumerState { + bool _showNewGroup = false; + void createNewGroup() { + setState(() { + _showNewGroup = true; + }); + } + + @override + Widget build(BuildContext context) { + final groups = + ref.watch(widget.boardStateController.select((value) => value.groups)); + return ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith( + dragDevices: { + PointerDeviceKind.mouse, + PointerDeviceKind.touch, + }, + ), + child: SingleChildScrollView( + controller: widget.boardScrollController, + scrollDirection: Axis.horizontal, + child: Row(children: [ + widget.leading ?? Container(), + ...[ + for (int index = 0; index < groups.length; index++) + BoardGroup( + groupItemStateContoller: widget.groupItemStateController, + groupStateController: widget.groupStateController, + header: widget.header, + footer: widget.footer, + itemBuilder: widget.groupItemBuilder, + groupDecoration: widget.groupDecoration, + boardStateController: widget.boardStateController, + groupIndex: index, + groupConstraints: widget.groupConstraints, + ), + ], + _showNewGroup + ? Container( + height: 300, + width: 300, + color: Colors.purple, + ) + : widget.trailing ?? Container(), + ]), + )); + } +} diff --git a/lib/src/widgets/board-group/group_placeholder_wrapper.dart b/lib/src/widgets/board-group/group_placeholder_wrapper.dart new file mode 100644 index 0000000..1cf4328 --- /dev/null +++ b/lib/src/widgets/board-group/group_placeholder_wrapper.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:kanban_board/src/constants/constants.dart'; +import 'package:kanban_board/src/controllers/controllers.dart'; + +class GroupPlaceholderWrapper extends ConsumerStatefulWidget { + const GroupPlaceholderWrapper( + {required this.groupIndex, + required this.groupStateController, + required this.boardStateController, + required this.child, + super.key}); + final int groupIndex; + final ChangeNotifierProvider groupStateController; + final ChangeNotifierProvider boardStateController; + final Widget child; + @override + ConsumerState createState() => + _GroupPlaceholderWrapperState(); +} + +class _GroupPlaceholderWrapperState + extends ConsumerState + with TickerProviderStateMixin { + late AnimationController? _animationController; + + @override + void initState() { + final group = + ref.read(widget.boardStateController).groups[widget.groupIndex]; + + group.animationController = AnimationController( + vsync: this, duration: const Duration(milliseconds: 700)); + + group.animationOffset = Offset( + group.placeHolderAt == PlaceHolderAt.left + ? -1 + : group.placeHolderAt == PlaceHolderAt.right + ? 1 + : 0, + 0); + _animationController = group.animationController; + + super.initState(); + } + + @override + void dispose() { + _animationController?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final boardState = ref.read(widget.boardStateController); + final draggingState = boardState.draggingState; + final group = boardState.groups[widget.groupIndex]; + + return Row( + children: [ + group.placeHolderAt == PlaceHolderAt.left + ? TweenAnimationBuilder( + duration: const Duration(milliseconds: 700), + curve: Curves.easeOutSine, + tween: Tween(begin: 0, end: 1), + builder: (context, value, child) { + return Opacity( + opacity: value, + child: child, + ); + }, + child: Opacity( + opacity: 0.6, + child: Container( + margin: const EdgeInsets.only(right: LIST_GAP), + child: draggingState.draggingWidget, + ), + )) + : Container(), + SlideTransition( + position: Tween( + begin: group.animationOffset, + end: const Offset(0, 0), + ).animate( + CurvedAnimation( + parent: _animationController!, + curve: _animationController!.isAnimating + ? Curves.easeOutSine + : Curves.ease, + ), + ), + child: widget.child, + ), + group.placeHolderAt == PlaceHolderAt.right + ? TweenAnimationBuilder( + duration: const Duration(milliseconds: 700), + curve: Curves.easeOutSine, + tween: Tween(begin: 0, end: 1), + builder: (context, value, child) { + return Opacity( + opacity: value, + child: child, + ); + }, + child: Opacity( + opacity: 0.6, + child: Container( + margin: const EdgeInsets.only(right: LIST_GAP), + child: draggingState.draggingWidget, + ), + ), + ) + : Container(), + ], + ); + } +} diff --git a/lib/src/widgets/board/widgets/new_list.dart b/lib/src/widgets/board/widgets/new_list.dart new file mode 100644 index 0000000..f2e832b --- /dev/null +++ b/lib/src/widgets/board/widgets/new_list.dart @@ -0,0 +1,77 @@ +// import 'package:flutter/material.dart'; +// import 'package:flutter_riverpod/flutter_riverpod.dart'; +// import 'package:kanban_board/models/board_list.dart'; +// import 'package:kanban_board/provider/provider_list.dart'; +// import 'package:kanban_board/src/widgets/textfield.dart'; + +// class NewList extends ConsumerStatefulWidget { +// const NewList({super.key}); + +// @override +// ConsumerState createState() => _NewListState(); +// } + +// class _NewListState extends ConsumerState { +// @override +// Widget build(BuildContext context) { +// final boardProv = ref.watch(ProviderList.boardProvider); +// final boardListProv = ref.watch(ProviderList.boardListProvider); +// return Container( +// margin: const EdgeInsets.only( +// top: 20, +// left: 30, +// ), +// padding: const EdgeInsets.only( +// bottom: 20, +// ), +// width: 300, +// color: const Color.fromARGB( +// 255, +// 247, +// 248, +// 252, +// ), +// child: Wrap( +// children: [ +// SizedBox( +// height: 50, +// width: 300, +// child: Row( +// mainAxisAlignment: MainAxisAlignment.spaceBetween, +// children: [ +// IconButton( +// onPressed: () { +// setState(() { +// boardListProv.showNewList = false; +// boardProv.newCardState.textController.clear(); +// }); +// }, +// icon: const Icon(Icons.close), +// ), +// IconButton( +// onPressed: () { +// setState(() { +// boardListProv.showNewList = false; +// boardProv.board.groups.add(BoardList( +// width: 300, +// scrollController: ScrollController(), +// items: [], +// title: boardProv.newCardState.textController.text, +// )); +// boardProv.newCardState.textController.clear(); +// }); +// }, +// icon: const Icon(Icons.done)) +// ], +// ), +// ), +// Container( +// width: 300, +// color: Colors.white, +// margin: const EdgeInsets.only(top: 20, right: 10, left: 10), +// child: const TField()), +// ], +// ), +// ); +// } +// } diff --git a/lib/src/widgets/draggable/draggable_overlay.dart b/lib/src/widgets/draggable/draggable_overlay.dart new file mode 100644 index 0000000..8aede4b --- /dev/null +++ b/lib/src/widgets/draggable/draggable_overlay.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:kanban_board/src/controllers/board_state_controller.dart'; +import 'package:kanban_board/src/controllers/group_state_controller.dart'; +import 'package:kanban_board/src/controllers/scroll_handler.dart'; +import 'package:kanban_board/src/controllers/states/draggable_state.dart'; +import 'package:kanban_board/src/controllers/states/scroll_state.dart'; + +/// [DraggableOverlay] is used to show the dragging widget. +/// It makes use of [ValueListenableBuilder] to listen to the [feedbackOffset] and update its position accordingly. +class DraggableOverlay extends ConsumerStatefulWidget { + const DraggableOverlay( + {required this.boardState, + required this.groupState, + required this.boardScrollController, + required this.groupScrollConfig, + required this.boardScrollConfig, + super.key}); + final ChangeNotifierProvider boardState; + final ChangeNotifierProvider groupState; + final ScrollController boardScrollController; + final ScrollConfig groupScrollConfig; + final ScrollConfig boardScrollConfig; + + @override + ConsumerState createState() => _DraggableOverlayState(); +} + +class _DraggableOverlayState extends ConsumerState { + /// This method is called when the dragging widget position is updated. + /// It makes use of [GroupScrollHandler] and [BoardScrollHandler] to check if the group or board should scroll. + Future _onDragUpdate() async { + final boardState = ref.read(widget.boardState); + final draggingState = boardState.draggingState; + final groupState = ref.read(widget.groupState); + + if (draggingState.draggableType == DraggableType.none) return; + + /// Check if the group should scroll. + await GroupScrollHandler.checkGroupScroll( + boardState: boardState, + scrollConfig: widget.groupScrollConfig, + scrollController: + boardState.groups[draggingState.currentGroupIndex].scrollController, + isScrolling: groupState.isScrolling, + setScrolling: (value) => groupState.isScrolling = value); + + /// Check if the board should scroll. + await BoardScrollHandler.checkBoardScroll( + boardState: boardState, + scrollConfig: widget.boardScrollConfig, + scrollController: widget.boardScrollController, + isScrolling: boardState.isScrolling, + setScrolling: (value) => boardState.isScrolling = value); + } + + @override + Widget build(BuildContext context) { + final draggingState = + ref.watch(widget.boardState.select((value) => value.draggingState)); + return ValueListenableBuilder( + valueListenable: draggingState.feedbackOffset, + builder: (ctx, Offset value, child) { + _onDragUpdate(); + return draggingState.draggableType != DraggableType.none + ? Positioned( + left: value.dx, + top: value.dy, + child: Opacity( + opacity: 1, + child: draggingState.draggingWidget, + ), + ) + : Container(); + }, + ); + } +} diff --git a/lib/src/widgets/group-item/group_item.dart b/lib/src/widgets/group-item/group_item.dart new file mode 100644 index 0000000..b2f71a4 --- /dev/null +++ b/lib/src/widgets/group-item/group_item.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:kanban_board/src/constants/constants.dart'; +import 'package:kanban_board/src/controllers/board_state_controller.dart'; +import 'package:kanban_board/src/controllers/group_item_state_controller.dart'; +import 'package:kanban_board/src/controllers/group_state_controller.dart'; +import 'package:kanban_board/src/controllers/states/draggable_state.dart'; + +class GroupItem extends ConsumerStatefulWidget { + const GroupItem({ + required this.itemIndex, + required this.groupIndex, + required this.boardState, + required this.boardGroupState, + required this.groupItemState, + super.key, + }); + final int itemIndex; + final int groupIndex; + final ChangeNotifierProvider boardState; + final ChangeNotifierProvider boardGroupState; + final ChangeNotifierProvider groupItemState; + @override + ConsumerState createState() => _GroupItemState(); +} + +class _GroupItemState extends ConsumerState + with TickerProviderStateMixin { + late AnimationController? _animationController; + + @override + void initState() { + final groupItem = ref + .read(widget.boardState) + .groups[widget.groupIndex] + .items[widget.itemIndex]; + + groupItem.animationController = AnimationController( + vsync: this, duration: const Duration(milliseconds: 500)); + _animationController = groupItem.animationController; + + super.initState(); + } + + @override + void dispose() { + _animationController?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final itemState = ref.read(widget.groupItemState); + final boardState = ref.read(widget.boardState); + final draggingState = boardState.draggingState; + final groupItem = + boardState.groups[widget.groupIndex].items[widget.itemIndex]; + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + itemState.computeItemPositionSize( + groupIndex: widget.groupIndex, + itemIndex: widget.itemIndex, + context: context, + setstate: () => {setState(() {})}); + }); + return ValueListenableBuilder( + key: boardState.groups[widget.groupIndex].items[widget.itemIndex].key, + valueListenable: boardState.draggingState.feedbackOffset, + builder: (ctx, offset, child) { + if (draggingState.draggableType == DraggableType.item) { + final groups = boardState.groups; + // item added by system in empty list, its widget/UI should not be manipulated on movements // + if (groups[widget.groupIndex].items.isEmpty) return child!; + + // CALCULATE SIZE AND POSITION OF ITEM // + if (itemState.calculateSizePosition( + groupIndex: widget.groupIndex, itemIndex: widget.itemIndex)) { + return child!; + } + + // DO NOT COMPARE ANYTHING WITH DRAGGED ITEM, IT WILL CAUSE ERRORS BECUSE ITS HIDDEN // + if ((draggingState.dragStartIndex == widget.itemIndex && + draggingState.dragStartGroupIndex == widget.groupIndex)) { + return child!; + } + + if (itemState.getYAxisCondition( + groupIndex: widget.groupIndex, itemIndex: widget.itemIndex)) { + itemState.handleSameGroupMove( + groupIndex: widget.groupIndex, itemIndex: widget.itemIndex); + } else if (itemState.getXAxisCondition( + groupIndex: widget.groupIndex, itemIndex: widget.itemIndex)) { + itemState.handleDiffGroupMove( + groupIndex: widget.groupIndex, itemIndex: widget.itemIndex); + } + } + return child!; + }, + child: GestureDetector( + onLongPress: () { + itemState.onLongPressItem( + groupIndex: widget.groupIndex, + itemIndex: widget.itemIndex, + context: context, + setState: () => {setState(() {})}); + }, + child: + // if item which is being dragged is same as current item, show feedback container + draggingState.draggableType == DraggableType.item && + draggingState.currentIndex == widget.itemIndex && + draggingState.dragStartIndex == widget.itemIndex && + draggingState.currentGroupIndex == widget.groupIndex && + draggingState.dragStartGroupIndex == widget.groupIndex + ? Container( + margin: const EdgeInsets.only(bottom: CARD_GAP), + width: draggingState.feedbackSize.width, + height: draggingState.feedbackSize.height, + child: boardState.itemGhost) + : itemState.isCurrentElementDragged( + groupIndex: widget.groupIndex, + itemIndex: widget.itemIndex) + ? Container() + : Container( + margin: const EdgeInsets.only(bottom: CARD_GAP), + child: groupItem.ghost), + ), + ); + } +} diff --git a/lib/src/widgets/kanban_gesture_listener.dart b/lib/src/widgets/kanban_gesture_listener.dart new file mode 100644 index 0000000..c2f10c8 --- /dev/null +++ b/lib/src/widgets/kanban_gesture_listener.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +// Riverpod: +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:kanban_board/src/controllers/states/scroll_state.dart'; +import 'package:kanban_board/src/controllers/controllers.dart' + show + BoardStateController, + DraggableType, + GroupStateController, + GroupItemStateController; + +/// [KanbanGestureListener] is used to listen to the pointer events and update the position of dragging-widget accordingly. +class KanbanGestureListener extends ConsumerStatefulWidget { + const KanbanGestureListener({ + required this.boardStateController, + required this.boardgroupController, + required this.groupItemController, + required this.boardScrollController, + required this.groupScrollConfig, + required this.boardScrollConfig, + this.child, + super.key, + }); + final ChangeNotifierProvider boardStateController; + final ChangeNotifierProvider boardgroupController; + final ChangeNotifierProvider groupItemController; + final ScrollController boardScrollController; + final ScrollConfig groupScrollConfig; + final ScrollConfig boardScrollConfig; + final Widget? child; + @override + ConsumerState createState() => + _KanbanGestureListenerState(); +} + +class _KanbanGestureListenerState extends ConsumerState { + /// This method is called when the pointer is up. + void _onPointerUp(PointerUpEvent event) { + final boardState = ref.read(widget.boardStateController); + + final draggingState = boardState.draggingState; + + if (draggingState.draggableType == DraggableType.item) { + ref.read(widget.groupItemController).onDragEnd(); + } else if (draggingState.draggableType == DraggableType.group) { + ref.read(widget.boardgroupController).onDragEnd(); + } + } + + /// This method is called when the pointer moves. + void _onPointerMove(PointerMoveEvent event) { + final boardState = ref.read(widget.boardStateController); + + final previousOffset = boardState.draggingState.feedbackOffset.value; + if ((event.position.dx - previousOffset.dx).abs() >= 100 || + (event.position.dy - previousOffset.dy).abs() >= 100) { + boardState.draggingState.axisDirection = + event.delta.dy > 0 ? AxisDirection.down : AxisDirection.up; + boardState.draggingState.feedbackOffset.value = Offset( + event.delta.dx + previousOffset.dx, + event.delta.dy + previousOffset.dy); + } + } + + @override + Widget build(BuildContext context) { + return Listener( + onPointerUp: _onPointerUp, + onPointerMove: _onPointerMove, + child: widget.child); + } +} diff --git a/lib/src/widgets/textfield.dart b/lib/src/widgets/textfield.dart new file mode 100644 index 0000000..2609628 --- /dev/null +++ b/lib/src/widgets/textfield.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class CardWithTextField extends ConsumerStatefulWidget { + const CardWithTextField({ + this.onCompleteEditing, + super.key, + }); + final void Function(String)? onCompleteEditing; + @override + ConsumerState createState() => _TFieldState(); +} + +class _TFieldState extends ConsumerState { + var node = FocusNode(); + final _controller = TextEditingController(); + @override + Widget build(BuildContext context) { + return TextFormField( + decoration: const InputDecoration( + hintText: 'Enter your text here', + enabledBorder: OutlineInputBorder(borderSide: BorderSide.none), + focusedBorder: OutlineInputBorder(borderSide: BorderSide.none), + disabledBorder: OutlineInputBorder(borderSide: BorderSide.none), + ), + autofocus: true, + enabled: true, + maxLines: 3, + keyboardType: TextInputType.text, + controller: _controller, + focusNode: node, + onFieldSubmitted: (text) => widget.onCompleteEditing?.call(text), + ); + } +} diff --git a/lib/src/widgets/widgets.dart b/lib/src/widgets/widgets.dart new file mode 100644 index 0000000..0099ea8 --- /dev/null +++ b/lib/src/widgets/widgets.dart @@ -0,0 +1,12 @@ +library widgets; + +// Board Group +export './board-group/board_groups_root.dart'; +export './board-group/board_group.dart'; +export './board-group/group_placeholder_wrapper.dart'; +// Board Item +export './group-item/group_item.dart'; +// Draggable +export './draggable/draggable_overlay.dart'; +// Gesture Listener +export 'kanban_gesture_listener.dart'; diff --git a/pubspec.lock b/pubspec.lock index f4878f5..49e6054 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -37,10 +37,18 @@ packages: dependency: transitive description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.18.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 + url: "https://pub.dev" + source: hosted + version: "3.0.5" dotted_border: dependency: "direct main" description: @@ -57,6 +65,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" flutter: dependency: "direct main" description: flutter @@ -83,14 +99,30 @@ packages: description: flutter source: sdk version: "0.0.0" - js: + leak_tracker: dependency: transitive description: - name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + name: leak_tracker + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "0.6.7" + version: "10.0.5" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" lints: dependency: transitive description: @@ -103,34 +135,34 @@ packages: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.15.0" path: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_drawing: dependency: transitive description: @@ -164,18 +196,26 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" state_notifier: dependency: transitive description: @@ -188,10 +228,10 @@ packages: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" string_scanner: dependency: transitive description: @@ -212,10 +252,26 @@ packages: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "1.3.2" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" vector_math: dependency: transitive description: @@ -224,6 +280,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + url: "https://pub.dev" + source: hosted + version: "14.2.5" sdks: - dart: ">=3.0.0 <4.0.0" - flutter: ">=3.0.0" + dart: ">=3.4.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/pubspec.yaml b/pubspec.yaml index a72d520..0573e26 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,6 +12,7 @@ dependencies: sdk: flutter flutter_riverpod: ^2.3.6 dotted_border: ^2.1.0 + uuid: ^4.5.1 dev_dependencies: flutter_test: