From 16fa80f8df4f5b2d0c6e94ad00259c76fd0ceef9 Mon Sep 17 00:00:00 2001 From: Benimautner Date: Sat, 22 Jul 2023 22:54:04 +0200 Subject: [PATCH 01/12] added basic project classes --- lib/api/project.dart | 41 ++++++++++++++++++++++++++++++++ lib/models/project.dart | 40 +++++++++++++++++++++++++++++++ lib/service/mocked_services.dart | 2 +- lib/service/services.dart | 11 +++++++++ 4 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 lib/api/project.dart create mode 100644 lib/models/project.dart diff --git a/lib/api/project.dart b/lib/api/project.dart new file mode 100644 index 0000000..04523e2 --- /dev/null +++ b/lib/api/project.dart @@ -0,0 +1,41 @@ +import 'package:vikunja_app/api/service.dart'; +import 'package:vikunja_app/models/project.dart'; +import 'package:vikunja_app/service/services.dart'; + +class ProjectAPIService extends APIService implements ProjectService { + ProjectAPIService(super.client); + + @override + Future create(Project p) { + // TODO: implement create + throw UnimplementedError(); + } + + @override + Future delete(int projectId) { + // TODO: implement delete + throw UnimplementedError(); + } + + @override + Future get(int projectId) { + // TODO: implement get + throw UnimplementedError(); + } + + @override + Future?> getAll() { + // TODO: implement getAll + return client.get('/projects').then((response) { + if (response == null) return null; + return convertList(response.body, (result) => Project.fromJson(result)); + }); + } + + @override + Future update(int projectId) { + // TODO: implement update + throw UnimplementedError(); + } + +} \ No newline at end of file diff --git a/lib/models/project.dart b/lib/models/project.dart new file mode 100644 index 0000000..484d911 --- /dev/null +++ b/lib/models/project.dart @@ -0,0 +1,40 @@ +import 'package:vikunja_app/models/user.dart'; + +class Project { + final int id; + final User? owner; + final int parentProjectId; + final String description; + final String title; + final DateTime created, updated; + + Project( + {this.id = 0, + this.owner, + this.parentProjectId = 0, + this.description = '', + required this.title, + created, + updated}) : + this.created = created ?? DateTime.now(), + this.updated = updated ?? DateTime.now(); + + Project.fromJson(Map json) + : title = json['title'], + description = json['description'], + id = json['id'], + parentProjectId = json['parent_project_id'], + created = DateTime.parse(json['created']), + updated = DateTime.parse(json['updated']), + owner = json['owner'] != null ? User.fromJson(json['owner']) : null; + + Map toJSON() => { + 'id': id, + 'created': created.toUtc().toIso8601String(), + 'updated': updated.toUtc().toIso8601String(), + 'title': title, + 'owner': owner?.toJSON(), + 'description': description, + 'parent_project_id': parentProjectId + }; +} \ No newline at end of file diff --git a/lib/service/mocked_services.dart b/lib/service/mocked_services.dart index 20b85fc..c5808fc 100644 --- a/lib/service/mocked_services.dart +++ b/lib/service/mocked_services.dart @@ -46,7 +46,7 @@ var _tasks = { created: DateTime.now(), description: 'A descriptive task', done: false, - listId: 1, + projectId: 1, ) }; diff --git a/lib/service/services.dart b/lib/service/services.dart index 2b436b9..38b29c8 100644 --- a/lib/service/services.dart +++ b/lib/service/services.dart @@ -12,6 +12,7 @@ import 'package:vikunja_app/models/task.dart'; import 'package:vikunja_app/models/user.dart'; import 'package:vikunja_app/models/bucket.dart'; +import '../models/project.dart'; import '../models/server.dart'; enum TaskServiceOptionSortBy { @@ -128,6 +129,16 @@ class TaskServiceOptions { } } +abstract class ProjectService { + Future?> getAll(); + + Future get(int projectId); + Future create(Project p); + Future update(int projectId); + Future delete(int projectId); +} + + abstract class NamespaceService { Future?> getAll(); From 2893a4e7f9700647d05c90cfd5c56ddf3c809e8a Mon Sep 17 00:00:00 2001 From: Benimautner Date: Sat, 22 Jul 2023 22:54:50 +0200 Subject: [PATCH 02/12] moved from list to projects --- lib/global.dart | 3 +++ lib/models/task.dart | 12 ++++++++---- lib/pages/landing_page.dart | 13 +++++-------- lib/stores/list_store.dart | 2 +- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/lib/global.dart b/lib/global.dart index 277b923..2df37a1 100644 --- a/lib/global.dart +++ b/lib/global.dart @@ -20,6 +20,7 @@ import 'package:vikunja_app/service/services.dart'; import 'package:timezone/data/latest_all.dart' as tz; import 'package:workmanager/workmanager.dart'; +import 'api/project.dart'; import 'main.dart'; @@ -68,6 +69,8 @@ class VikunjaGlobalState extends State { NamespaceService get namespaceService => new NamespaceAPIService(client); + ProjectService get projectService => new ProjectAPIService(client); + TaskService get taskService => new TaskAPIService(client); BucketService get bucketService => new BucketAPIService(client); diff --git a/lib/models/task.dart b/lib/models/task.dart index e5c0773..2b7a9cf 100644 --- a/lib/models/task.dart +++ b/lib/models/task.dart @@ -10,7 +10,8 @@ import 'package:vikunja_app/utils/checkboxes_in_text.dart'; class Task { final int id; final int? parentTaskId, priority, bucketId; - final int? listId; + //final int? listId; + final int? projectId; final DateTime created, updated; DateTime? dueDate, startDate, endDate; final List reminderDates; @@ -50,7 +51,8 @@ class Task { DateTime? created, DateTime? updated, required this.createdBy, - required this.listId, + //required this.listId, + required this.projectId, this.bucketId, }) : this.created = created ?? DateTime.now(), this.updated = updated ?? DateTime.now(); @@ -105,7 +107,8 @@ class Task { : [], updated = DateTime.parse(json['updated']), created = DateTime.parse(json['created']), - listId = json['list_id'], + //listId = json['list_id'], + projectId = json['project_id'], bucketId = json['bucket_id'], createdBy = User.fromJson(json['created_by']); @@ -163,7 +166,8 @@ class Task { id: id ?? this.id, parentTaskId: parentTaskId ?? this.parentTaskId, priority: priority ?? this.priority, - listId: listId ?? this.listId, + //listId: listId ?? this.listId, + projectId: projectId ?? this.projectId, bucketId: bucketId ?? this.bucketId, created: created ?? this.created, updated: updated ?? this.updated, diff --git a/lib/pages/landing_page.dart b/lib/pages/landing_page.dart index 09200d3..d4c3f1a 100644 --- a/lib/pages/landing_page.dart +++ b/lib/pages/landing_page.dart @@ -35,7 +35,7 @@ class LandingPage extends HomeScreenWidget { class LandingPageState extends State with AfterLayoutMixin { int? defaultList; - List _list = []; + List _tasks = []; PageStatus landingPageStatus = PageStatus.built; static const platform = const MethodChannel('vikunja'); @@ -165,7 +165,7 @@ class LandingPageState extends State title: title, dueDate: dueDate, createdBy: globalState.currentUser!, - listId: defaultList!, + projectId: defaultList!, ), ); @@ -176,7 +176,7 @@ class LandingPageState extends State } List _listTasks(BuildContext context) { - var tasks = (_list.map((task) => _buildTile(task, context))).toList(); + var tasks = (_tasks.map((task) => _buildTile(task, context))).toList(); //tasks.addAll(_loadingTasks.map(_buildLoadingTile)); return tasks; } @@ -193,8 +193,7 @@ class LandingPageState extends State } Future _loadList(BuildContext context) { - log("reloading list"); - _list = []; + _tasks = []; landingPageStatus = PageStatus.loading; // FIXME: loads and reschedules tasks each time list is updated VikunjaGlobal.of(context).notifications.scheduleDueNotifications(VikunjaGlobal.of(context).taskService); @@ -208,18 +207,16 @@ class LandingPageState extends State }); return null; } - return VikunjaGlobal.of(context).listService.getAll().then((lists) { //taskList.forEach((task) {task.list = lists.firstWhere((element) => element.id == task.list_id);}); setState(() { if (taskList != null) { - _list = taskList; + _tasks = taskList; landingPageStatus = PageStatus.success; } else { landingPageStatus = PageStatus.error; } }); return null; - }); }); } } diff --git a/lib/stores/list_store.dart b/lib/stores/list_store.dart index 55f7336..3b344eb 100644 --- a/lib/stores/list_store.dart +++ b/lib/stores/list_store.dart @@ -112,7 +112,7 @@ class ListProvider with ChangeNotifier { title: title, createdBy: globalState.currentUser!, done: false, - listId: listId, + projectId: listId, ); pageStatus = PageStatus.loading; From 6f32e1ff3856b6c49e021cbced84968fc08a27c2 Mon Sep 17 00:00:00 2001 From: Benimautner Date: Sat, 22 Jul 2023 22:55:25 +0200 Subject: [PATCH 03/12] renamed namespaces to projects, added basic project views --- lib/pages/home.dart | 6 +- lib/pages/list/list.dart | 2 +- lib/pages/project/overview.dart | 149 ++++++++++++++++++++++++++++++++ lib/pages/project/project.dart | 23 +++++ 4 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 lib/pages/project/overview.dart create mode 100644 lib/pages/project/project.dart diff --git a/lib/pages/home.dart b/lib/pages/home.dart index 181fd97..ba456b9 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -7,12 +7,14 @@ import 'package:provider/provider.dart'; import 'package:vikunja_app/components/AddDialog.dart'; import 'package:vikunja_app/components/ErrorDialog.dart'; +import 'package:vikunja_app/models/project.dart'; import 'package:vikunja_app/pages/namespace/namespace.dart'; import 'package:vikunja_app/pages/namespace/namespace_edit.dart'; import 'package:vikunja_app/pages/landing_page.dart'; import 'package:vikunja_app/global.dart'; import 'package:vikunja_app/models/namespace.dart'; import 'package:vikunja_app/pages/namespace/overview.dart'; +import 'package:vikunja_app/pages/project/overview.dart'; import 'package:vikunja_app/pages/settings.dart'; import 'package:vikunja_app/stores/list_store.dart'; @@ -31,13 +33,13 @@ class HomePageState extends State { create: (_) => new ListProvider(), child: LandingPage(), ), - NamespaceOverviewPage(), + ProjectOverviewPage(), SettingsPage() ]; List navbarItems = [ BottomNavigationBarItem(icon: Icon(Icons.home), label: "Home"), - BottomNavigationBarItem(icon: Icon(Icons.list), label: "Namespaces"), + BottomNavigationBarItem(icon: Icon(Icons.list), label: "Projects"), BottomNavigationBarItem(icon: Icon(Icons.settings), label: "Settings"), ]; diff --git a/lib/pages/list/list.dart b/lib/pages/list/list.dart index 06acc27..70c5f46 100644 --- a/lib/pages/list/list.dart +++ b/lib/pages/list/list.dart @@ -314,7 +314,7 @@ class _ListPageState extends State { createdBy: currentUser, done: false, bucketId: bucket?.id, - listId: _list.id, + projectId: _list.id, ); setState(() => _loadingTasks.add(newTask)); return Provider.of(context, listen: false) diff --git a/lib/pages/project/overview.dart b/lib/pages/project/overview.dart new file mode 100644 index 0000000..2bc6c6b --- /dev/null +++ b/lib/pages/project/overview.dart @@ -0,0 +1,149 @@ +import 'package:after_layout/after_layout.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:vikunja_app/pages/project/project.dart'; + +import '../../components/AddDialog.dart'; +import '../../components/ErrorDialog.dart'; +import '../../global.dart'; +import '../../models/project.dart'; + +class ProjectOverviewPage extends StatefulWidget { + @override + _ProjectOverviewPageState createState() => + new _ProjectOverviewPageState(); +} + +class _ProjectOverviewPageState extends State + with AfterLayoutMixin { + List _projects = []; + int _selectedDrawerIndex = -2, _previousDrawerIndex = -2; + bool _loading = true; + + Project? get _currentProject => + _selectedDrawerIndex >= -1 && _selectedDrawerIndex < _projects.length + ? _projects[_selectedDrawerIndex] + : null; + + @override + void afterFirstLayout(BuildContext context) { + _loadProjects(); + } + + Widget createProjectTile(Project project, int level){ + List children = addProjectChildren(project, level); + EdgeInsets insets = EdgeInsets.fromLTRB(level * 20 + 10, 0, 0, 0); + if(children.length == 0) { + return new ListTile( + leading: const Icon(Icons.folder), + title: new Text(project.title), + contentPadding: insets, + ); + } else { + return new ExpansionTile( + leading: const Icon(Icons.folder), + title: new Text(project.title), + children: children, + tilePadding: insets + //onTap: () => _onSelectItem(i), + ); + } + } + + List addProjectChildren(Project project, level) { + Iterable children = _projects.where((element) => element.parentProjectId == project.id); + List widgets = []; + children.forEach((element) {widgets.add(createProjectTile(element, level + 1));}); + return widgets; + } + + @override + Widget build(BuildContext context) { + List projectList = []; + _projects + .asMap() + .forEach((i, project) { + if(project.parentProjectId != 0) + return; + projectList.add(createProjectTile(project, 0)); + }); + + if(_selectedDrawerIndex > -1) { + return new WillPopScope( + child: ProjectPage(project: _projects[_selectedDrawerIndex]), + onWillPop: () async {setState(() { + _selectedDrawerIndex = -2; + }); + return false;}); + + } + + return Scaffold( + body: + this._loading + ? Center(child: CircularProgressIndicator()) + : + RefreshIndicator( + child: ListView( + padding: EdgeInsets.zero, + children: ListTile.divideTiles( + context: context, tiles: projectList) + .toList()), + onRefresh: _loadProjects, + ), + floatingActionButton: Builder( + builder: (context) => FloatingActionButton( + onPressed: () => _addProjectDialog(context), + child: const Icon(Icons.add))), + appBar: AppBar( + title: Text("Projects"), + ), + ); + } + + Future _loadProjects() { + return VikunjaGlobal.of(context).projectService.getAll().then((result) { + setState(() { + _loading = false; + if (result != null) _projects = result; + }); + }); + } + + _onSelectItem(int index) { + Navigator.push(context, + MaterialPageRoute( + builder: (buildContext) => ProjectPage( + project: _projects[index], + ),)); + //setState(() => _selectedDrawerIndex = index); + } + + _addProjectDialog(BuildContext context) { + showDialog( + context: context, + builder: (_) => AddDialog( + onAdd: (name) => _addProject(name, context), + decoration: new InputDecoration( + labelText: 'Project', hintText: 'eg. Personal Project'), + )); + } + + _addProject(String name, BuildContext context) { + final currentUser = VikunjaGlobal.of(context).currentUser; + if (currentUser == null) { + return; + } + + VikunjaGlobal.of(context) + .projectService + .create(Project(title: name, owner: currentUser)) + .then((_) { + _loadProjects(); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('The project was created successfully!'), + )); + }).catchError((error) => showDialog( + context: context, builder: (context) => ErrorDialog(error: error))); + } +} diff --git a/lib/pages/project/project.dart b/lib/pages/project/project.dart new file mode 100644 index 0000000..ed2a624 --- /dev/null +++ b/lib/pages/project/project.dart @@ -0,0 +1,23 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +import '../../models/project.dart'; + +class ProjectPage extends StatefulWidget { + final Project project; + + ProjectPage({required this.project}) + : super(key: Key(project.id.toString())); + + @override + _ProjectPageState createState() => new _ProjectPageState(); +} + +class _ProjectPageState extends State { + @override + Widget build(BuildContext context) { + // TODO: implement build + return Scaffold(); + } + +} \ No newline at end of file From 1c523d929c69059f94d028df81730bd9b1f27260 Mon Sep 17 00:00:00 2001 From: Benimautner Date: Sat, 22 Jul 2023 23:31:28 +0200 Subject: [PATCH 04/12] added project list page with expandable sublists --- lib/pages/project/overview.dart | 137 ++++++++++++++++++++------------ lib/pages/project/project.dart | 3 +- 2 files changed, 85 insertions(+), 55 deletions(-) diff --git a/lib/pages/project/overview.dart b/lib/pages/project/overview.dart index 2bc6c6b..2c63691 100644 --- a/lib/pages/project/overview.dart +++ b/lib/pages/project/overview.dart @@ -1,3 +1,4 @@ + import 'package:after_layout/after_layout.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -10,8 +11,7 @@ import '../../models/project.dart'; class ProjectOverviewPage extends StatefulWidget { @override - _ProjectOverviewPageState createState() => - new _ProjectOverviewPageState(); + _ProjectOverviewPageState createState() => new _ProjectOverviewPageState(); } class _ProjectOverviewPageState extends State @@ -30,67 +30,96 @@ class _ProjectOverviewPageState extends State _loadProjects(); } - Widget createProjectTile(Project project, int level){ - List children = addProjectChildren(project, level); - EdgeInsets insets = EdgeInsets.fromLTRB(level * 20 + 10, 0, 0, 0); - if(children.length == 0) { - return new ListTile( - leading: const Icon(Icons.folder), - title: new Text(project.title), - contentPadding: insets, - ); + List expandedList = []; + + Widget createProjectTile(Project project, int level) { + EdgeInsets insets = EdgeInsets.fromLTRB(level * 10 + 10, 0, 0, 0); + + bool expanded = expandedList.contains(project.id); + Widget icon; + + List? children = addProjectChildren(project, level+1); + bool no_children = children.length == 0; + if(no_children) { + icon = Icon(Icons.list); } else { - return new ExpansionTile( - leading: const Icon(Icons.folder), - title: new Text(project.title), - children: children, - tilePadding: insets - //onTap: () => _onSelectItem(i), - ); + if (expanded) { + icon = Icon(Icons.arrow_drop_down_sharp); + } else { + children = null; + icon = Icon(Icons.arrow_right_sharp); + } } - } + + + return Column(children: [ + ListTile( + onTap: () { + setState(() { + _onSelectItem(project); + }); + }, + contentPadding: insets, + leading: IconButton( + disabledColor: Theme.of(context).unselectedWidgetColor, + icon: icon, + onPressed: !no_children ? () { + setState(() { + if (expanded) + expandedList.remove(project.id); + else + expandedList.add(project.id); + }); + } : null, + ), + title: new Text(project.title), + //onTap: () => _onSelectItem(i), + ), + ...?children + ]); + } + List addProjectChildren(Project project, level) { - Iterable children = _projects.where((element) => element.parentProjectId == project.id); + Iterable children = + _projects.where((element) => element.parentProjectId == project.id); List widgets = []; - children.forEach((element) {widgets.add(createProjectTile(element, level + 1));}); + children.forEach((element) { + widgets.add(createProjectTile(element, level + 1)); + }); return widgets; } @override Widget build(BuildContext context) { List projectList = []; - _projects - .asMap() - .forEach((i, project) { - if(project.parentProjectId != 0) - return; - projectList.add(createProjectTile(project, 0)); - }); + _projects.asMap().forEach((i, project) { + if (project.parentProjectId != 0) return; + projectList.add(createProjectTile(project, 0)); + }); - if(_selectedDrawerIndex > -1) { + if (_selectedDrawerIndex > -1) { return new WillPopScope( child: ProjectPage(project: _projects[_selectedDrawerIndex]), - onWillPop: () async {setState(() { - _selectedDrawerIndex = -2; + onWillPop: () async { + setState(() { + _selectedDrawerIndex = -2; + }); + return false; }); - return false;}); - } return Scaffold( - body: - this._loading + body: this._loading ? Center(child: CircularProgressIndicator()) - : - RefreshIndicator( - child: ListView( - padding: EdgeInsets.zero, - children: ListTile.divideTiles( - context: context, tiles: projectList) - .toList()), - onRefresh: _loadProjects, - ), + : RefreshIndicator( + child: ListView( + padding: EdgeInsets.zero, + children: + ListTile.divideTiles(context: context, tiles: projectList) + .toList()), + onRefresh: _loadProjects, + ), floatingActionButton: Builder( builder: (context) => FloatingActionButton( onPressed: () => _addProjectDialog(context), @@ -110,12 +139,14 @@ class _ProjectOverviewPageState extends State }); } - _onSelectItem(int index) { - Navigator.push(context, + _onSelectItem(Project project) { + Navigator.push( + context, MaterialPageRoute( builder: (buildContext) => ProjectPage( - project: _projects[index], - ),)); + project: project, + ), + )); //setState(() => _selectedDrawerIndex = index); } @@ -123,10 +154,10 @@ class _ProjectOverviewPageState extends State showDialog( context: context, builder: (_) => AddDialog( - onAdd: (name) => _addProject(name, context), - decoration: new InputDecoration( - labelText: 'Project', hintText: 'eg. Personal Project'), - )); + onAdd: (name) => _addProject(name, context), + decoration: new InputDecoration( + labelText: 'Project', hintText: 'eg. Personal Project'), + )); } _addProject(String name, BuildContext context) { @@ -144,6 +175,6 @@ class _ProjectOverviewPageState extends State content: Text('The project was created successfully!'), )); }).catchError((error) => showDialog( - context: context, builder: (context) => ErrorDialog(error: error))); + context: context, builder: (context) => ErrorDialog(error: error))); } } diff --git a/lib/pages/project/project.dart b/lib/pages/project/project.dart index ed2a624..e5fadbf 100644 --- a/lib/pages/project/project.dart +++ b/lib/pages/project/project.dart @@ -16,8 +16,7 @@ class ProjectPage extends StatefulWidget { class _ProjectPageState extends State { @override Widget build(BuildContext context) { - // TODO: implement build - return Scaffold(); + return Scaffold(body: Text(widget.project.title),); } } \ No newline at end of file From 7c7d6be9df26ac04f23c103343a4dfd0eb81a90f Mon Sep 17 00:00:00 2001 From: Benimautner Date: Sun, 23 Jul 2023 00:04:13 +0200 Subject: [PATCH 05/12] added subproject view to project home page --- lib/models/project.dart | 1 + lib/pages/project/overview.dart | 14 +++-------- lib/pages/project/project.dart | 44 ++++++++++++++++++++++++++++++++- 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/lib/models/project.dart b/lib/models/project.dart index 484d911..4ed5d1c 100644 --- a/lib/models/project.dart +++ b/lib/models/project.dart @@ -7,6 +7,7 @@ class Project { final String description; final String title; final DateTime created, updated; + Iterable? subprojects; Project( {this.id = 0, diff --git a/lib/pages/project/overview.dart b/lib/pages/project/overview.dart index 2c63691..22a78bc 100644 --- a/lib/pages/project/overview.dart +++ b/lib/pages/project/overview.dart @@ -56,7 +56,7 @@ class _ProjectOverviewPageState extends State ListTile( onTap: () { setState(() { - _onSelectItem(project); + onSelectProject(context, project); }); }, contentPadding: insets, @@ -83,6 +83,7 @@ class _ProjectOverviewPageState extends State List addProjectChildren(Project project, level) { Iterable children = _projects.where((element) => element.parentProjectId == project.id); + project.subprojects = children; List widgets = []; children.forEach((element) { widgets.add(createProjectTile(element, level + 1)); @@ -139,16 +140,7 @@ class _ProjectOverviewPageState extends State }); } - _onSelectItem(Project project) { - Navigator.push( - context, - MaterialPageRoute( - builder: (buildContext) => ProjectPage( - project: project, - ), - )); - //setState(() => _selectedDrawerIndex = index); - } + _addProjectDialog(BuildContext context) { showDialog( diff --git a/lib/pages/project/project.dart b/lib/pages/project/project.dart index e5fadbf..528e179 100644 --- a/lib/pages/project/project.dart +++ b/lib/pages/project/project.dart @@ -16,7 +16,49 @@ class ProjectPage extends StatefulWidget { class _ProjectPageState extends State { @override Widget build(BuildContext context) { - return Scaffold(body: Text(widget.project.title),); + return Scaffold( + body: Column( + children: [ + buildSubProjectSelector(), + ] + ), + appBar: AppBar( + title: Text(widget.project.title), + ),); } + Widget buildSubProjectSelector() { + return Container( + height: 80, + child: + ListView( + scrollDirection: Axis.horizontal, + //mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ...?widget.project.subprojects?.map((elem) => + InkWell( + onTap: () {onSelectProject(context, elem);}, + child: + Container( + alignment: Alignment.center, + height: 20, + width: 100, + child: + Text(elem.title, overflow: TextOverflow.ellipsis,softWrap: false,))) + ), + ], + ), + ); + } +} + +onSelectProject(BuildContext context, Project project) { + Navigator.push( + context, + MaterialPageRoute( + builder: (buildContext) => ProjectPage( + project: project, + ), + )); + //setState(() => _selectedDrawerIndex = index); } \ No newline at end of file From 9c5ad5829995dcf805dcfc48321fd1154ea8023b Mon Sep 17 00:00:00 2001 From: Benimautner Date: Sun, 23 Jul 2023 01:50:55 +0200 Subject: [PATCH 06/12] moving from lists and namespaces to projects. --- lib/api/bucket_implementation.dart | 14 +- lib/api/client.dart | 14 +- lib/api/project.dart | 61 +++- lib/api/task_implementation.dart | 10 +- lib/components/BucketTaskCard.dart | 5 +- lib/components/KanbanWidget.dart | 23 +- lib/components/SliverBucketList.dart | 5 +- lib/components/TaskTile.dart | 7 +- lib/global.dart | 2 +- lib/models/bucket.dart | 8 +- lib/models/project.dart | 62 +++- lib/pages/home.dart | 7 +- lib/pages/list/list.dart | 16 +- lib/pages/list/task_edit.dart | 5 +- lib/pages/project/overview.dart | 6 +- lib/pages/project/project.dart | 64 ---- lib/pages/project/project_edit.dart | 173 ++++++++++ lib/pages/project/project_task_list.dart | 388 +++++++++++++++++++++++ lib/pages/settings.dart | 21 +- lib/service/mocked_services.dart | 12 +- lib/service/services.dart | 10 +- lib/stores/list_store.dart | 5 +- lib/stores/project_store.dart | 251 +++++++++++++++ 23 files changed, 1016 insertions(+), 153 deletions(-) create mode 100644 lib/pages/project/project_edit.dart create mode 100644 lib/pages/project/project_task_list.dart create mode 100644 lib/stores/project_store.dart diff --git a/lib/api/bucket_implementation.dart b/lib/api/bucket_implementation.dart index 492b93e..df6a7f8 100644 --- a/lib/api/bucket_implementation.dart +++ b/lib/api/bucket_implementation.dart @@ -8,9 +8,9 @@ class BucketAPIService extends APIService implements BucketService { BucketAPIService(Client client) : super(client); @override - Future add(int listId, Bucket bucket) { + Future add(int projectId, Bucket bucket) { return client - .put('/lists/$listId/buckets', body: bucket.toJSON()) + .put('/projects/$projectId/buckets', body: bucket.toJSON()) .then((response) { if (response == null) return null; return Bucket.fromJSON(response.body); @@ -18,9 +18,9 @@ class BucketAPIService extends APIService implements BucketService { } @override - Future delete(int listId, int bucketId) { + Future delete(int projectId, int bucketId) { return client - .delete('/lists/$listId/buckets/$bucketId'); + .delete('/projects/$projectId/buckets/$bucketId'); } /* Not implemented in the Vikunja API @@ -33,10 +33,10 @@ class BucketAPIService extends APIService implements BucketService { */ @override - Future getAllByList(int listId, + Future getAllByList(int projectId, [Map>? queryParameters]) { return client - .get('/lists/$listId/buckets', queryParameters) + .get('/projects/$projectId/buckets', queryParameters) .then((response) => response != null ? new Response( convertList(response.body, (result) => Bucket.fromJSON(result)), response.statusCode, @@ -51,7 +51,7 @@ class BucketAPIService extends APIService implements BucketService { @override Future update(Bucket bucket) { return client - .post('/lists/${bucket.listId}/buckets/${bucket.id}', body: bucket.toJSON()) + .post('/projects/${bucket.projectId}/buckets/${bucket.id}', body: bucket.toJSON()) .then((response) { if (response == null) return null; return Bucket.fromJSON(response.body); diff --git a/lib/api/client.dart b/lib/api/client.dart index 0d12456..27939fb 100644 --- a/lib/api/client.dart +++ b/lib/api/client.dart @@ -79,7 +79,19 @@ class Client { Future get(String url, [Map>? queryParameters]) { - return http.get('${this.base}$url'.toUri()!, headers: _headers) + Uri uri = Uri.tryParse('${this.base}$url')!; + // why are we doing it like this? because Uri doesnt have setters. wtf. + uri = Uri( + scheme: uri.scheme, + userInfo: uri.userInfo, + host: uri.host, + port: uri.port, + path: uri.path, + queryParameters: {...uri.queryParameters, ...?queryParameters}, + fragment: uri.fragment + ); + + return http.get(uri, headers: _headers) .then(_handleResponse).onError((error, stackTrace) => _handleError(error, stackTrace)); } diff --git a/lib/api/project.dart b/lib/api/project.dart index 04523e2..9f2f6b4 100644 --- a/lib/api/project.dart +++ b/lib/api/project.dart @@ -1,9 +1,12 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:vikunja_app/api/service.dart'; import 'package:vikunja_app/models/project.dart'; import 'package:vikunja_app/service/services.dart'; class ProjectAPIService extends APIService implements ProjectService { - ProjectAPIService(super.client); + FlutterSecureStorage _storage; + + ProjectAPIService(client, storage) : _storage = storage, super(client); @override Future create(Project p) { @@ -13,14 +16,24 @@ class ProjectAPIService extends APIService implements ProjectService { @override Future delete(int projectId) { - // TODO: implement delete - throw UnimplementedError(); + return client.delete('/projects/$projectId').then((_) {}); } @override Future get(int projectId) { - // TODO: implement get - throw UnimplementedError(); + return client.get('/projects/$projectId').then((response) { + if (response == null) return null; + final map = response.body; + /*if (map.containsKey('id')) { + return client + .get("/lists/$projectId/tasks") + .then((tasks) { + map['tasks'] = tasks?.body; + return Project.fromJson(map); + }); + }*/ + return Project.fromJson(map); + }); } @override @@ -33,9 +46,41 @@ class ProjectAPIService extends APIService implements ProjectService { } @override - Future update(int projectId) { - // TODO: implement update - throw UnimplementedError(); + Future update(Project p) { + return client + .post('/projects/${p.id}', body: p.toJSON()) + .then((response) { + if (response == null) return null; + return Project.fromJson(response.body); + }); + } + + @override + Future getDisplayDoneTasks(int listId) { + return _storage.read(key: "display_done_tasks_list_$listId").then((value) + { + if(value == null) { + // TODO: implement default value + setDisplayDoneTasks(listId, "1"); + return Future.value("1"); + } + return value; + }); + } + + @override + void setDisplayDoneTasks(int listId, String value) { + _storage.write(key: "display_done_tasks_list_$listId", value: value); + } + + @override + Future getDefaultList() { + return _storage.read(key: "default_list_id"); + } + + @override + void setDefaultList(int? listId) { + _storage.write(key: "default_list_id", value: listId.toString()); } } \ No newline at end of file diff --git a/lib/api/task_implementation.dart b/lib/api/task_implementation.dart index 281c9d7..b916a12 100644 --- a/lib/api/task_implementation.dart +++ b/lib/api/task_implementation.dart @@ -11,9 +11,9 @@ class TaskAPIService extends APIService implements TaskService { TaskAPIService(Client client) : super(client); @override - Future add(int listId, Task task) { + Future add(int projectId, Task task) { return client - .put('/lists/$listId', body: task.toJSON()) + .put('/projects/$projectId', body: task.toJSON()) .then((response) { if (response == null) return null; return Task.fromJson(response.body); @@ -23,7 +23,7 @@ class TaskAPIService extends APIService implements TaskService { @override Future get(int listId) { return client - .get('/list/$listId/tasks') + .get('/project/$listId/tasks') .then((response) { if (response == null) return null; return Task.fromJson(response.body); @@ -75,10 +75,10 @@ class TaskAPIService extends APIService implements TaskService { } @override - Future getAllByList(int listId, + Future getAllByProject(int projectId, [Map>? queryParameters]) { return client - .get('/lists/$listId/tasks', queryParameters).then( + .get('/projects/$projectId/tasks', queryParameters).then( (response) { return response != null ? new Response( diff --git a/lib/components/BucketTaskCard.dart b/lib/components/BucketTaskCard.dart index dc89c1f..074bcd4 100644 --- a/lib/components/BucketTaskCard.dart +++ b/lib/components/BucketTaskCard.dart @@ -6,10 +6,11 @@ import 'package:dotted_border/dotted_border.dart'; import 'package:provider/provider.dart'; import 'package:vikunja_app/models/task.dart'; import 'package:vikunja_app/pages/list/task_edit.dart'; -import 'package:vikunja_app/stores/list_store.dart'; import 'package:vikunja_app/utils/misc.dart'; import 'package:vikunja_app/theme/constants.dart'; +import '../stores/project_store.dart'; + enum DropLocation {above, below, none} class TaskData { @@ -47,7 +48,7 @@ class _BucketTaskCardState extends State with AutomaticKeepAlive super.build(context); if (_cardSize == null) _updateCardSize(context); - final taskState = Provider.of(context); + final taskState = Provider.of(context); final bucket = taskState.buckets[taskState.buckets.indexWhere((b) => b.id == widget.task.bucketId)]; // default chip height: 32 const double chipHeight = 28; diff --git a/lib/components/KanbanWidget.dart b/lib/components/KanbanWidget.dart index b1ba2cc..f3d2368 100644 --- a/lib/components/KanbanWidget.dart +++ b/lib/components/KanbanWidget.dart @@ -8,8 +8,9 @@ import 'package:provider/provider.dart'; import '../global.dart'; import '../models/bucket.dart'; import '../models/list.dart'; +import '../models/project.dart'; import '../pages/list/list.dart'; -import '../stores/list_store.dart'; +import '../stores/project_store.dart'; import '../utils/calculate_item_position.dart'; import 'AddDialog.dart'; import 'BucketLimitDialog.dart'; @@ -19,19 +20,19 @@ import 'SliverBucketPersistentHeader.dart'; class KanbanClass { PageController? _pageController; - ListProvider? taskState; + ProjectProvider? taskState; int? _draggedBucketIndex; BuildContext context; Function _onViewTapped, _addItemDialog, notify; Duration _lastTaskDragUpdateAction = Duration.zero; - TaskList _list; + Project _list; Map _bucketProps = {}; KanbanClass(this.context, this.notify, this._onViewTapped, this._addItemDialog, this._list) { - taskState = Provider.of(context); + taskState = Provider.of(context); } @@ -177,12 +178,12 @@ class KanbanClass { return; } - await Provider.of(context, listen: false).addBucket( + await Provider.of(context, listen: false).addBucket( context: context, newBucket: Bucket( title: title, createdBy: currentUser, - listId: _list.id, + projectId: _list.id, limit: 0, ), listId: _list.id, @@ -196,7 +197,7 @@ class KanbanClass { } Future _updateBucket(BuildContext context, Bucket bucket) { - return Provider.of(context, listen: false) + return Provider.of(context, listen: false) .updateBucket( context: context, bucket: bucket, @@ -211,9 +212,9 @@ class KanbanClass { } Future _deleteBucket(BuildContext context, Bucket bucket) async { - await Provider.of(context, listen: false).deleteBucket( + await Provider.of(context, listen: false).deleteBucket( context: context, - listId: bucket.listId, + listId: bucket.projectId, bucketId: bucket.id, ); @@ -499,7 +500,7 @@ class KanbanClass { return true; }, onAccept: (data) { - Provider.of(context, listen: false) + Provider.of(context, listen: false) .moveTaskToBucket( context: context, task: data.task, @@ -539,7 +540,7 @@ class KanbanClass { } Future loadBucketsForPage(int page) { - return Provider.of(context, listen: false).loadBuckets( + return Provider.of(context, listen: false).loadBuckets( context: context, listId: _list.id, page: page diff --git a/lib/components/SliverBucketList.dart b/lib/components/SliverBucketList.dart index b6cf39d..07c94f2 100644 --- a/lib/components/SliverBucketList.dart +++ b/lib/components/SliverBucketList.dart @@ -3,7 +3,8 @@ import 'package:provider/provider.dart'; import 'package:vikunja_app/components/BucketTaskCard.dart'; import 'package:vikunja_app/models/bucket.dart'; import 'package:vikunja_app/models/task.dart'; -import 'package:vikunja_app/stores/list_store.dart'; + +import '../stores/project_store.dart'; class SliverBucketList extends StatelessWidget { final Bucket bucket; @@ -33,7 +34,7 @@ class SliverBucketList extends StatelessWidget { } Future _moveTaskToBucket(BuildContext context, Task task, int index) async { - await Provider.of(context, listen: false).moveTaskToBucket( + await Provider.of(context, listen: false).moveTaskToBucket( context: context, task: task, newBucketId: bucket.id, diff --git a/lib/components/TaskTile.dart b/lib/components/TaskTile.dart index 78efc22..324231c 100644 --- a/lib/components/TaskTile.dart +++ b/lib/components/TaskTile.dart @@ -5,7 +5,8 @@ import 'package:provider/provider.dart'; import 'package:vikunja_app/models/task.dart'; import 'package:vikunja_app/utils/misc.dart'; import 'package:vikunja_app/pages/list/task_edit.dart'; -import 'package:vikunja_app/stores/list_store.dart'; + +import '../stores/project_store.dart'; class TaskTile extends StatefulWidget { final Task task; @@ -41,7 +42,7 @@ class TaskTileState extends State with AutomaticKeepAliveClientMixin { @override Widget build(BuildContext context) { super.build(context); - final taskState = Provider.of(context); + final taskState = Provider.of(context); Duration? durationUntilDue = _currentTask.dueDate?.difference(DateTime.now()); if (_currentTask.loading) { return ListTile( @@ -119,7 +120,7 @@ class TaskTileState extends State with AutomaticKeepAliveClientMixin { } Future _updateTask(Task task, bool checked) { - return Provider.of(context, listen: false).updateTask( + return Provider.of(context, listen: false).updateTask( context: context, task: task.copyWith( done: checked, diff --git a/lib/global.dart b/lib/global.dart index 2df37a1..a9f68ec 100644 --- a/lib/global.dart +++ b/lib/global.dart @@ -69,7 +69,7 @@ class VikunjaGlobalState extends State { NamespaceService get namespaceService => new NamespaceAPIService(client); - ProjectService get projectService => new ProjectAPIService(client); + ProjectService get projectService => new ProjectAPIService(client, _storage); TaskService get taskService => new TaskAPIService(client); diff --git a/lib/models/bucket.dart b/lib/models/bucket.dart index 09055dd..f6ed3e6 100644 --- a/lib/models/bucket.dart +++ b/lib/models/bucket.dart @@ -5,7 +5,7 @@ import 'package:vikunja_app/models/user.dart'; @JsonSerializable() class Bucket { - int id, listId, limit; + int id, projectId, limit; String title; double? position; final DateTime created, updated; @@ -15,7 +15,7 @@ class Bucket { Bucket({ this.id = 0, - required this.listId, + required this.projectId, required this.title, this.position, required this.limit, @@ -30,7 +30,7 @@ class Bucket { Bucket.fromJSON(Map json) : id = json['id'], - listId = json['list_id'], + projectId = json['project_id'], title = json['title'], position = json['position'] is int ? json['position'].toDouble() @@ -48,7 +48,7 @@ class Bucket { toJSON() => { 'id': id, - 'list_id': listId, + 'list_id': projectId, 'title': title, 'position': position, 'limit': limit, diff --git a/lib/models/project.dart b/lib/models/project.dart index 4ed5d1c..0eaf64a 100644 --- a/lib/models/project.dart +++ b/lib/models/project.dart @@ -1,20 +1,31 @@ +import 'dart:ui'; + import 'package:vikunja_app/models/user.dart'; class Project { final int id; + final double position; final User? owner; final int parentProjectId; final String description; final String title; final DateTime created, updated; + final Color? color; + final bool isArchived, isFavourite; + Iterable? subprojects; Project( - {this.id = 0, - this.owner, - this.parentProjectId = 0, - this.description = '', - required this.title, + { + this.id = 0, + this.owner, + this.parentProjectId = 0, + this.description = '', + this.position = 0, + this.color, + this.isArchived = false, + this.isFavourite = false, + required this.title, created, updated}) : this.created = created ?? DateTime.now(), @@ -24,9 +35,15 @@ class Project { : title = json['title'], description = json['description'], id = json['id'], + position = json['position'].toDouble(), + isArchived = json['is_archived'], + isFavourite = json['is_archived'], parentProjectId = json['parent_project_id'], created = DateTime.parse(json['created']), updated = DateTime.parse(json['updated']), + color = json['hex_color'] != '' + ? Color(int.parse(json['hex_color'], radix: 16) + 0xFF000000) + : null, owner = json['owner'] != null ? User.fromJson(json['owner']) : null; Map toJSON() => { @@ -36,6 +53,39 @@ class Project { 'title': title, 'owner': owner?.toJSON(), 'description': description, - 'parent_project_id': parentProjectId + 'parent_project_id': parentProjectId, + 'hex_color': color?.value.toRadixString(16).padLeft(8, '0').substring(2), + 'is_archived': isArchived, + 'is_favourite': isFavourite, + 'position': position }; + + Project copyWith({ + int? id, + DateTime? created, + DateTime? updated, + String? title, + User? owner, + String? description, + int? parentProjectId, + Color? color, + bool? isArchived, + bool? isFavourite, + double? position, + + }) { + return Project( + id: id ?? this.id, + created: created ?? this.created, + updated: updated ?? this.updated, + title: title ?? this.title, + owner: owner ?? this.owner, + description: description ?? this.description, + parentProjectId: parentProjectId ?? this.parentProjectId, + color: color ?? this.color, + isArchived: isArchived ?? this.isArchived, + isFavourite: isFavourite ?? this.isFavourite, + position: position ?? this.position, + ); + } } \ No newline at end of file diff --git a/lib/pages/home.dart b/lib/pages/home.dart index ba456b9..48100e4 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -16,7 +16,8 @@ import 'package:vikunja_app/models/namespace.dart'; import 'package:vikunja_app/pages/namespace/overview.dart'; import 'package:vikunja_app/pages/project/overview.dart'; import 'package:vikunja_app/pages/settings.dart'; -import 'package:vikunja_app/stores/list_store.dart'; + +import '../stores/project_store.dart'; class HomePage extends StatefulWidget { @override @@ -29,8 +30,8 @@ class HomePageState extends State { List widgets = [ - ChangeNotifierProvider( - create: (_) => new ListProvider(), + ChangeNotifierProvider( + create: (_) => new ProjectProvider(), child: LandingPage(), ), ProjectOverviewPage(), diff --git a/lib/pages/list/list.dart b/lib/pages/list/list.dart index 70c5f46..479e6af 100644 --- a/lib/pages/list/list.dart +++ b/lib/pages/list/list.dart @@ -65,8 +65,8 @@ class _ListPageState extends State { @override Widget build(BuildContext context) { taskState = Provider.of(context); - _kanban = KanbanClass( - context, nullSetState, _onViewTapped, _addItemDialog, _list); + //_kanban = KanbanClass( + // context, nullSetState, _onViewTapped, _addItemDialog, _list); Widget body; @@ -242,16 +242,8 @@ class _ListPageState extends State { TaskTile _buildLoadingTile(Task task) { return TaskTile( task: task, - loading: true, - onEdit: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => TaskEditPage( - task: task, - taskState: taskState, - ), - ), - ), + loading: true, onEdit: () {}, + ); } diff --git a/lib/pages/list/task_edit.dart b/lib/pages/list/task_edit.dart index 24ce7f2..1f4a3e1 100644 --- a/lib/pages/list/task_edit.dart +++ b/lib/pages/list/task_edit.dart @@ -6,12 +6,13 @@ import 'package:vikunja_app/components/label.dart'; import 'package:vikunja_app/global.dart'; import 'package:vikunja_app/models/label.dart'; import 'package:vikunja_app/models/task.dart'; -import 'package:vikunja_app/stores/list_store.dart'; import 'package:vikunja_app/utils/repeat_after_parse.dart'; +import '../../stores/project_store.dart'; + class TaskEditPage extends StatefulWidget { final Task task; - final ListProvider taskState; + final ProjectProvider taskState; TaskEditPage({ required this.task, diff --git a/lib/pages/project/overview.dart b/lib/pages/project/overview.dart index 22a78bc..9e0cbb5 100644 --- a/lib/pages/project/overview.dart +++ b/lib/pages/project/overview.dart @@ -2,7 +2,7 @@ import 'package:after_layout/after_layout.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:vikunja_app/pages/project/project.dart'; +import 'package:vikunja_app/pages/project/project_task_list.dart'; import '../../components/AddDialog.dart'; import '../../components/ErrorDialog.dart'; @@ -56,7 +56,7 @@ class _ProjectOverviewPageState extends State ListTile( onTap: () { setState(() { - onSelectProject(context, project); + openList(context, project); }); }, contentPadding: insets, @@ -101,7 +101,7 @@ class _ProjectOverviewPageState extends State if (_selectedDrawerIndex > -1) { return new WillPopScope( - child: ProjectPage(project: _projects[_selectedDrawerIndex]), + child: ListPage(project: _projects[_selectedDrawerIndex]), onWillPop: () async { setState(() { _selectedDrawerIndex = -2; diff --git a/lib/pages/project/project.dart b/lib/pages/project/project.dart index 528e179..e69de29 100644 --- a/lib/pages/project/project.dart +++ b/lib/pages/project/project.dart @@ -1,64 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; - -import '../../models/project.dart'; - -class ProjectPage extends StatefulWidget { - final Project project; - - ProjectPage({required this.project}) - : super(key: Key(project.id.toString())); - - @override - _ProjectPageState createState() => new _ProjectPageState(); -} - -class _ProjectPageState extends State { - @override - Widget build(BuildContext context) { - return Scaffold( - body: Column( - children: [ - buildSubProjectSelector(), - ] - ), - appBar: AppBar( - title: Text(widget.project.title), - ),); - } - Widget buildSubProjectSelector() { - return Container( - height: 80, - child: - ListView( - scrollDirection: Axis.horizontal, - //mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - ...?widget.project.subprojects?.map((elem) => - InkWell( - onTap: () {onSelectProject(context, elem);}, - child: - Container( - alignment: Alignment.center, - height: 20, - width: 100, - child: - Text(elem.title, overflow: TextOverflow.ellipsis,softWrap: false,))) - ), - ], - ), - ); - } -} - - -onSelectProject(BuildContext context, Project project) { - Navigator.push( - context, - MaterialPageRoute( - builder: (buildContext) => ProjectPage( - project: project, - ), - )); - //setState(() => _selectedDrawerIndex = index); -} \ No newline at end of file diff --git a/lib/pages/project/project_edit.dart b/lib/pages/project/project_edit.dart new file mode 100644 index 0000000..c7a589e --- /dev/null +++ b/lib/pages/project/project_edit.dart @@ -0,0 +1,173 @@ +import 'dart:ffi'; +import 'dart:developer'; +import 'package:flutter/material.dart'; +import 'package:vikunja_app/global.dart'; +import 'package:vikunja_app/theme/button.dart'; +import 'package:vikunja_app/theme/buttonText.dart'; + +import '../../models/project.dart'; + +class ProjectEditPage extends StatefulWidget { + final Project project; + + ProjectEditPage({required this.project}) : super(key: Key(project.toString())); + + @override + State createState() => _ProjectEditPageState(); +} + +class _ProjectEditPageState extends State { + final _formKey = GlobalKey(); + bool _loading = false; + String _title = '', _description = ''; + bool? displayDoneTasks; + late int listId; + + @override + void initState(){ + listId = widget.project.id; + super.initState(); + } + + @override + Widget build(BuildContext ctx) { + if(displayDoneTasks == null) + VikunjaGlobal.of(context).projectService.getDisplayDoneTasks(listId).then( + (value) => setState(() => displayDoneTasks = value == "1")); + else + log("Display done tasks: " + displayDoneTasks.toString()); + return Scaffold( + appBar: AppBar( + title: Text('Edit Project'), + ), + body: Builder( + builder: (BuildContext context) => SafeArea( + child: Form( + key: _formKey, + child: ListView( + //reverse: true, + padding: const EdgeInsets.all(16.0), + children: [ + Padding( + padding: EdgeInsets.symmetric(vertical: 10.0), + child: TextFormField( + maxLines: null, + keyboardType: TextInputType.multiline, + initialValue: widget.project.title, + onSaved: (title) => _title = title ?? '', + validator: (title) { + //if (title?.length < 3 || title.length > 250) { + // return 'The title needs to have between 3 and 250 characters.'; + //} + return null; + }, + decoration: new InputDecoration( + labelText: 'Title', + border: OutlineInputBorder(), + ), + ), + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 10.0), + child: TextFormField( + maxLines: null, + keyboardType: TextInputType.multiline, + initialValue: widget.project.description, + onSaved: (description) => _description = description ?? '', + validator: (description) { + if(description == null) + return null; + if (description.length > 1000) { + return 'The description can have a maximum of 1000 characters.'; + } + return null; + }, + decoration: new InputDecoration( + labelText: 'Description', + border: OutlineInputBorder(), + ), + ), + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 10.0), + child: CheckboxListTile( + value: displayDoneTasks ?? false, + title: Text("Show done tasks"), + onChanged: (value) { + value ??= false; + VikunjaGlobal.of(context).listService.setDisplayDoneTasks(listId, value ? "1" : "0"); + setState(() => displayDoneTasks = value); + }, + ), + ), + Builder( + builder: (context) => Padding( + padding: EdgeInsets.symmetric(vertical: 10.0), + child: FancyButton( + onPressed: !_loading + ? () { + if (_formKey.currentState!.validate()) { + Form.of(context)?.save(); + _saveList(context); + } + } + : () {}, + child: _loading + ? CircularProgressIndicator() + : VikunjaButtonText('Save'), + ))), + /*ExpansionTile( + title: Text("Sharing"), + children: [ + TypeAheadFormField( + onSuggestionSelected: (suggestion) {}, + itemBuilder: (BuildContext context, Object? itemData) { + return Card( + child: Container( + padding: EdgeInsets.all(10), + child: Text(itemData.toString())), + );}, + suggestionsCallback: (String pattern) { + List matches = []; + matches.addAll(["test", "test2", "test3"]); + matches.retainWhere((s){ + return s.toLowerCase().contains(pattern.toLowerCase()); + }); + return matches; + },) + ], + )*/ + ] + ), + ), + ), + ), + ); + } + + _saveList(BuildContext context) async { + setState(() => _loading = true); + // FIXME: is there a way we can update the list without creating a new list object? + // aka updating the existing list we got from context (setters?) + Project newProject = widget.project.copyWith( + title: _title, + description: _description + ); + VikunjaGlobal.of(context).projectService.update(newProject).then((_) { + setState(() => _loading = false); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('The task was updated successfully!'), + )); + }).catchError((err) { + setState(() => _loading = false); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Something went wrong: ' + err.toString()), + action: SnackBarAction( + label: 'CLOSE', + onPressed: ScaffoldMessenger.of(context).hideCurrentSnackBar), + ), + ); + }); + } +} diff --git a/lib/pages/project/project_task_list.dart b/lib/pages/project/project_task_list.dart new file mode 100644 index 0000000..7220e39 --- /dev/null +++ b/lib/pages/project/project_task_list.dart @@ -0,0 +1,388 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; +import 'package:vikunja_app/components/AddDialog.dart'; +import 'package:vikunja_app/components/KanbanWidget.dart'; +import 'package:vikunja_app/components/TaskTile.dart'; +import 'package:vikunja_app/global.dart'; +import 'package:vikunja_app/models/list.dart'; +import 'package:vikunja_app/models/task.dart'; +import 'package:vikunja_app/models/bucket.dart'; +import 'package:vikunja_app/pages/list/list_edit.dart'; +import 'package:vikunja_app/pages/list/task_edit.dart'; +import 'package:vikunja_app/pages/project/project_edit.dart'; + +import '../../components/pagestatus.dart'; +import '../../models/project.dart'; +import '../../stores/project_store.dart'; + +enum BucketMenu { limit, done, delete } + +class BucketProps { + final ScrollController controller = ScrollController(); + final TextEditingController titleController = TextEditingController(); + bool scrollable = false; + bool portrait = true; + int bucketLength = 0; + Size? taskDropSize; +} + +class ListPage extends StatefulWidget { + final Project project; + + //ListPage({this.taskList}) : super(key: Key(taskList.id.toString())); + ListPage({required this.project}) + : super(key: Key(Random().nextInt(100000).toString())); + + @override + _ListPageState createState() => _ListPageState(); +} + +class _ListPageState extends State { + final _keyboardController = KeyboardVisibilityController(); + int _viewIndex = 0; + late Project _project; + List _loadingTasks = []; + int _currentPage = 1; + bool displayDoneTasks = false; + late ProjectProvider taskState; + late KanbanClass _kanban; + + @override + void initState() { + _project = widget.project; + _keyboardController.onChange.listen((visible) { + if (!visible && mounted) FocusScope.of(context).unfocus(); + }); + super.initState(); + } + + void nullSetState() { + setState(() {}); + } + + @override + Widget build(BuildContext context) { + taskState = Provider.of(context); + _kanban = KanbanClass( + context, nullSetState, _onViewTapped, _addItemDialog, _project); + + Widget body; + + switch (taskState.pageStatus) { + case PageStatus.built: + Future.delayed(Duration.zero, _loadList); + body = new Stack(children: [ + ListView(), + Center( + child: CircularProgressIndicator(), + ) + ]); + break; + case PageStatus.loading: + body = new Stack(children: [ + ListView(), + Center( + child: CircularProgressIndicator(), + ) + ]); + break; + case PageStatus.error: + body = new Stack(children: [ + ListView(), + Center(child: Text("There was an error loading this view")) + ]); + break; + case PageStatus.success: + body = taskState.tasks.length > 0 || taskState.buckets.length > 0 + ? ListenableProvider.value( + value: taskState, + child: Theme( + data: (ThemeData base) { + return base.copyWith( + chipTheme: base.chipTheme.copyWith( + labelPadding: EdgeInsets.symmetric(horizontal: 2), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(5)), + ), + ), + ); + }(Theme.of(context)), + child: () { + switch (_viewIndex) { + case 0: + return _listView(context); + case 1: + return _kanban.kanbanView(); + default: + return _listView(context); + } + }(), + ), + ) + : Stack(children: [ + ListView(), + Center(child: Text('This list is empty.')) + ]); + break; + case PageStatus.empty: + body = new Stack(children: [ + ListView(), + Center(child: Text("This view is empty")) + ]); + break; + } + + return new Scaffold( + appBar: AppBar( + title: Text(_project.title), + actions: [ + IconButton( + icon: Icon(Icons.edit), + onPressed: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ProjectEditPage( + project: _project, + ), + )).whenComplete(() => _loadList()), + ), + ], + ), + body: RefreshIndicator(onRefresh: () => _loadList(), child: body), + floatingActionButton: _viewIndex == 1 + ? null + : Builder( + builder: (context) => FloatingActionButton( + onPressed: () => _addItemDialog(context), + child: Icon(Icons.add)), + ), + bottomNavigationBar: BottomNavigationBar( + items: const [ + BottomNavigationBarItem( + icon: Icon(Icons.view_list), + label: 'List', + tooltip: 'List', + ), + BottomNavigationBarItem( + icon: Icon(Icons.view_kanban), + label: 'Kanban', + tooltip: 'Kanban', + ), + ], + currentIndex: _viewIndex, + onTap: _onViewTapped, + ), + ); + } + + Widget buildSubProjectSelector() { + return Container( + height: 80, + child: + ListView( + scrollDirection: Axis.horizontal, + //mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ...?widget.project.subprojects?.map((elem) => + InkWell( + onTap: () {openList(context, elem);}, + child: + Container( + alignment: Alignment.center, + height: 20, + width: 100, + child: + Text(elem.title, overflow: TextOverflow.ellipsis,softWrap: false,))) + ), + ], + ), + ); + } + + void _onViewTapped(int index) { + _loadList().then((_) { + _currentPage = 1; + setState(() { + _viewIndex = index; + }); + }); + } + + Widget _listView(BuildContext context) { + List subProjectView = []; + if(widget.project.subprojects?.length != 0) { + subProjectView.add(Padding(child: Text("Projects", style: TextStyle(fontWeight: FontWeight.bold),), padding: EdgeInsets.fromLTRB(0, 10, 0, 0),)); + subProjectView.add(buildSubProjectSelector()); + subProjectView.add(Padding(child: Text("Tasks", style: TextStyle(fontWeight: FontWeight.bold),), padding: EdgeInsets.fromLTRB(0, 10, 0, 0),)); + subProjectView.add(Divider()); + } + + return Column( + children: [ + ...subProjectView, + Expanded(child: + ListView.builder( + padding: EdgeInsets.symmetric(vertical: 8.0), + itemCount: taskState.tasks.length * 2, + itemBuilder: (context, i) { + if (i.isOdd) return Divider(); + + if (_loadingTasks.isNotEmpty) { + final loadingTask = _loadingTasks.removeLast(); + return _buildLoadingTile(loadingTask); + } + + final index = i ~/ 2; + + if (taskState.maxPages == _currentPage && + index == taskState.tasks.length) + throw Exception("Check itemCount attribute"); + + if (index >= taskState.tasks.length && + _currentPage < taskState.maxPages) { + _currentPage++; + _loadTasksForPage(_currentPage); + } + return _buildTile(taskState.tasks[index]); + }))]); + } + + Widget _buildTile(Task task) { + return ListenableProvider.value( + value: taskState, + child: TaskTile( + task: task, + loading: false, + onEdit: () {}, + onMarkedAsDone: (done) { + Provider.of(context, listen: false).updateTask( + context: context, + task: task.copyWith(done: done), + ); + }, + ), + ); + } + + Future updateDisplayDoneTasks() { + return VikunjaGlobal.of(context) + .projectService + .getDisplayDoneTasks(_project.id) + .then((value) { + displayDoneTasks = value == "1"; + }); + } + + TaskTile _buildLoadingTile(Task task) { + return TaskTile( + task: task, + loading: true, + onEdit: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TaskEditPage( + task: task, + taskState: taskState, + ), + ), + ), + ); + } + + Future _loadList() async { + taskState.pageStatus = (PageStatus.loading); + + updateDisplayDoneTasks().then((value) async { + switch (_viewIndex) { + case 0: + _loadTasksForPage(1); + break; + case 1: + await _kanban + .loadBucketsForPage(1); + // load all buckets to get length for RecordableListView + while (_currentPage < taskState.maxPages) { + _currentPage++; + await _kanban + .loadBucketsForPage(_currentPage); + } + break; + default: + _loadTasksForPage(1); + } + }); + } + + Future _loadTasksForPage(int page) { + return Provider.of(context, listen: false) + .loadTasks( + context: context, + listId: _project.id, + page: page, + displayDoneTasks: displayDoneTasks); + } + + Future _addItemDialog(BuildContext context, [Bucket? bucket]) { + return showDialog( + context: context, + builder: (_) => AddDialog( + onAdd: (title) => _addItem(title, context, bucket), + decoration: InputDecoration( + labelText: + (bucket != null ? '\'${bucket.title}\': ' : '') + 'New Task Name', + hintText: 'eg. Milk', + ), + ), + ); + } + + Future _addItem(String title, BuildContext context, + [Bucket? bucket]) async { + final currentUser = VikunjaGlobal.of(context).currentUser; + if (currentUser == null) { + return; + } + + final newTask = Task( + title: title, + createdBy: currentUser, + done: false, + bucketId: bucket?.id, + projectId: _project.id, + ); + setState(() => _loadingTasks.add(newTask)); + return Provider.of(context, listen: false) + .addTask( + context: context, + newTask: newTask, + listId: _project.id, + ) + .then((_) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('The task was added successfully' + + (bucket != null ? ' to \'${bucket.title}\'' : '') + + '!'), + )); + setState(() { + _loadingTasks.remove(newTask); + }); + }); + } +} + + +openList(BuildContext context, Project project) { + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => ChangeNotifierProvider( + create: (_) => new ProjectProvider(), + child: ListPage( + project: project, + ), + ), + // ListPage(taskList: list) + )); +} \ No newline at end of file diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index db19b58..ffb55d8 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -5,6 +5,7 @@ import 'package:vikunja_app/global.dart'; import 'package:vikunja_app/models/list.dart'; import '../main.dart'; +import '../models/project.dart'; class SettingsPage extends StatefulWidget { @@ -13,8 +14,8 @@ class SettingsPage extends StatefulWidget { } class SettingsPageState extends State { - List? taskListList; - int? defaultList; + List? projectList; + int? defaultProject; bool? ignoreCertificates; bool? getVersionNotifications; String? versionTag, newestVersionTag; @@ -27,13 +28,13 @@ class SettingsPageState extends State { durationTextController = TextEditingController(); VikunjaGlobal.of(context) - .listService + .projectService .getAll() - .then((value) => setState(() => taskListList = value)); + .then((value) => setState(() => projectList = value)); - VikunjaGlobal.of(context).listService.getDefaultList().then((value) => + VikunjaGlobal.of(context).projectService.getDefaultList().then((value) => setState( - () => defaultList = value == null ? null : int.tryParse(value))); + () => defaultProject = value == null ? null : int.tryParse(value))); VikunjaGlobal.of(context).settingsManager.getIgnoreCertificates().then( (value) => @@ -84,7 +85,7 @@ class SettingsPageState extends State { Theme.of(context).primaryColor, BlendMode.multiply)), ), ), - taskListList != null + projectList != null ? ListTile( title: Text("Default List"), trailing: DropdownButton( @@ -93,14 +94,14 @@ class SettingsPageState extends State { child: Text("None"), value: null, ), - ...taskListList! + ...projectList! .map((e) => DropdownMenuItem( child: Text(e.title), value: e.id)) .toList() ], - value: defaultList, + value: defaultProject, onChanged: (int? value) { - setState(() => defaultList = value); + setState(() => defaultProject = value); VikunjaGlobal.of(context) .listService .setDefaultList(value); diff --git a/lib/service/mocked_services.dart b/lib/service/mocked_services.dart index c5808fc..273f872 100644 --- a/lib/service/mocked_services.dart +++ b/lib/service/mocked_services.dart @@ -170,12 +170,6 @@ class MockedTaskService implements TaskService { return Future.value(task); } - @override - Future getAllByList(int listId, - [Map>? queryParameters]) { - return Future.value(new Response(_tasks.values.toList(), 200, {})); - } - @override int get maxPages => 1; Future get(int taskId) { @@ -194,6 +188,12 @@ class MockedTaskService implements TaskService { // TODO: implement getAll throw UnimplementedError(); } + + @override + Future getAllByProject(int projectId, [Map>? queryParameters]) { + // TODO: implement getAllByProject + return Future.value(new Response(_tasks.values.toList(), 200, {})); + } } class MockedUserService implements UserService { diff --git a/lib/service/services.dart b/lib/service/services.dart index 38b29c8..26d2180 100644 --- a/lib/service/services.dart +++ b/lib/service/services.dart @@ -134,8 +134,14 @@ abstract class ProjectService { Future get(int projectId); Future create(Project p); - Future update(int projectId); + Future update(Project p); Future delete(int projectId); + + + Future getDisplayDoneTasks(int listId); + void setDisplayDoneTasks(int listId, String value); + Future getDefaultList(); + void setDefaultList(int? listId); } @@ -184,7 +190,7 @@ abstract class TaskService { Future?> getAll(); - Future getAllByList(int listId, + Future getAllByProject(int projectId, [Map> queryParameters]); Future?> getByOptions(TaskServiceOptions options); diff --git a/lib/stores/list_store.dart b/lib/stores/list_store.dart index 3b344eb..98ef752 100644 --- a/lib/stores/list_store.dart +++ b/lib/stores/list_store.dart @@ -1,3 +1,4 @@ + import 'package:flutter/material.dart'; import 'package:vikunja_app/models/task.dart'; import 'package:vikunja_app/models/bucket.dart'; @@ -65,6 +66,8 @@ class ListProvider with ChangeNotifier { "filter_value": ["false"] }); } + return Future.value(); + /* return VikunjaGlobal.of(context).taskService.getAllByList(listId, queryParams).then((response) { if(response == null) { pageStatus = PageStatus.error; @@ -75,7 +78,7 @@ class ListProvider with ChangeNotifier { } _tasks.addAll(response.body); pageStatus = PageStatus.success; - }); + });*/ } Future loadBuckets({required BuildContext context, required int listId, int page = 1}) { diff --git a/lib/stores/project_store.dart b/lib/stores/project_store.dart new file mode 100644 index 0000000..a5d8a52 --- /dev/null +++ b/lib/stores/project_store.dart @@ -0,0 +1,251 @@ +import 'package:flutter/material.dart'; +import 'package:vikunja_app/models/task.dart'; +import 'package:vikunja_app/models/bucket.dart'; +import 'package:vikunja_app/utils/calculate_item_position.dart'; +import 'package:vikunja_app/global.dart'; + +import '../components/pagestatus.dart'; + +class ProjectProvider with ChangeNotifier { + bool _taskDragging = false; + int _maxPages = 0; + + // TODO: Streams + List _tasks = []; + List _buckets = []; + + + bool get taskDragging => _taskDragging; + + set taskDragging(bool value) { + _taskDragging = value; + notifyListeners(); + } + + int get maxPages => _maxPages; + + set tasks(List tasks) { + _tasks = tasks; + notifyListeners(); + } + + List get tasks => _tasks; + + set buckets(List buckets) { + _buckets = buckets; + notifyListeners(); + } + + List get buckets => _buckets; + + + PageStatus _pageStatus = PageStatus.built; + + PageStatus get pageStatus => _pageStatus; + + set pageStatus(PageStatus ps) { + _pageStatus = ps; + print("new PageStatus: ${ps.toString()}"); + notifyListeners(); + } + + Future loadTasks({required BuildContext context, required int listId, int page = 1, bool displayDoneTasks = true}) { + _tasks = []; + notifyListeners(); + + Map> queryParams = { + "sort_by": ["done", "id"], + "order_by": ["asc", "desc"], + "page": [page.toString()] + }; + + if(!displayDoneTasks) { + queryParams.addAll({ + "filter_by": ["done"], + "filter_value": ["false"], + "sort_by": ["done"], + }); + } + return VikunjaGlobal.of(context).taskService.getAllByProject(listId, queryParams).then((response) { + if(response == null) { + pageStatus = PageStatus.error; + return; + } + if (response.headers["x-pagination-total-pages"] != null) { + _maxPages = int.parse(response.headers["x-pagination-total-pages"]!); + } + _tasks.addAll(response.body); + pageStatus = PageStatus.success; + }); + } + + Future loadBuckets({required BuildContext context, required int listId, int page = 1}) { + _buckets = []; + pageStatus = PageStatus.loading; + notifyListeners(); + + Map> queryParams = { + "page": [page.toString()] + }; + + return VikunjaGlobal.of(context).bucketService.getAllByList(listId, queryParams).then((response) { + if(response == null) { + pageStatus = PageStatus.error; + return; + } + if (response.headers["x-pagination-total-pages"] != null) { + _maxPages = int.parse(response.headers["x-pagination-total-pages"]!); + } + _buckets.addAll(response.body); + + pageStatus = PageStatus.success; + }); + } + + Future addTaskByTitle( + {required BuildContext context, required String title, required int projectId}) async{ + final globalState = VikunjaGlobal.of(context); + if (globalState.currentUser == null) { + return; + } + + final newTask = Task( + title: title, + createdBy: globalState.currentUser!, + done: false, + projectId: projectId, + ); + pageStatus = PageStatus.loading; + + return globalState.taskService.add(projectId, newTask).then((task) { + if(task != null) + _tasks.insert(0, task); + pageStatus = PageStatus.success; + }); + } + + Future addTask({required BuildContext context, required Task newTask, required int listId}) { + var globalState = VikunjaGlobal.of(context); + if (newTask.bucketId == null) pageStatus = PageStatus.loading; + notifyListeners(); + + return globalState.taskService.add(listId, newTask).then((task) { + if (task == null) { + pageStatus = PageStatus.error; + return; + } + if (_tasks.isNotEmpty) + _tasks.insert(0, task); + if (_buckets.isNotEmpty) { + final bucket = _buckets[_buckets.indexWhere((b) => task.bucketId == b.id)]; + bucket.tasks.add(task); + } + pageStatus = PageStatus.success; + }); + } + + Future updateTask({required BuildContext context, required Task task}) { + return VikunjaGlobal.of(context).taskService.update(task).then((task) { + // FIXME: This is ugly. We should use a redux to not have to do these kind of things. + // This is enough for now (it works™) but we should definitely fix it later. + if(task == null) + return null; + _tasks.asMap().forEach((i, t) { + if (task.id == t.id) { + _tasks[i] = task; + } + }); + _buckets.asMap().forEach((i, b) => b.tasks.asMap().forEach((v, t) { + if (task.id == t.id){ + _buckets[i].tasks[v] = task; + } + })); + notifyListeners(); + return task; + }); + } + + Future addBucket({required BuildContext context, required Bucket newBucket, required int listId}) { + notifyListeners(); + return VikunjaGlobal.of(context).bucketService.add(listId, newBucket) + .then((bucket) { + if(bucket == null) + return null; + _buckets.add(bucket); + notifyListeners(); + }); + } + + Future updateBucket({required BuildContext context, required Bucket bucket}) { + return VikunjaGlobal.of(context).bucketService.update(bucket) + .then((rBucket) { + if(rBucket == null) + return null; + _buckets[_buckets.indexWhere((b) => rBucket.id == b.id)] = rBucket; + _buckets.sort((a, b) => a.position!.compareTo(b.position!)); + notifyListeners(); + }); + } + + Future deleteBucket({required BuildContext context, required int listId, required int bucketId}) { + return VikunjaGlobal.of(context).bucketService.delete(listId, bucketId) + .then((_) { + _buckets.removeWhere((bucket) => bucket.id == bucketId); + notifyListeners(); + }); + } + + Future moveTaskToBucket({required BuildContext context, required Task? task, int? newBucketId, required int index}) async { + if(task == null) + throw Exception("Task to be moved may not be null"); + final sameBucket = task.bucketId == newBucketId; + final newBucketIndex = _buckets.indexWhere((b) => b.id == newBucketId); + if (sameBucket && index > _buckets[newBucketIndex].tasks.indexWhere((t) => t.id == task?.id)) index--; + + _buckets[_buckets.indexWhere((b) => b.id == task?.bucketId)].tasks.remove(task); + if (index >= _buckets[newBucketIndex].tasks.length) + _buckets[newBucketIndex].tasks.add(task); + else + _buckets[newBucketIndex].tasks.insert(index, task); + + task = await VikunjaGlobal.of(context).taskService.update(task.copyWith( + bucketId: newBucketId, + kanbanPosition: calculateItemPosition( + positionBefore: index != 0 + ? _buckets[newBucketIndex].tasks[index - 1].kanbanPosition : null, + positionAfter: index < _buckets[newBucketIndex].tasks.length - 1 + ? _buckets[newBucketIndex].tasks[index + 1].kanbanPosition : null, + ), + )); + if(task == null) + return; + _buckets[newBucketIndex].tasks[index] = task; + + // make sure the first 2 tasks don't have 0 kanbanPosition + Task? secondTask; + if (index == 0 && _buckets[newBucketIndex].tasks.length > 1 + && _buckets[newBucketIndex].tasks[1].kanbanPosition == 0) { + secondTask = await VikunjaGlobal.of(context).taskService.update( + _buckets[newBucketIndex].tasks[1].copyWith( + kanbanPosition: calculateItemPosition( + positionBefore: task.kanbanPosition, + positionAfter: 1 < _buckets[newBucketIndex].tasks.length - 1 + ? _buckets[newBucketIndex].tasks[2].kanbanPosition : null, + ), + )); + if(secondTask != null) + _buckets[newBucketIndex].tasks[1] = secondTask; + } + + if (_tasks.isNotEmpty) { + _tasks[_tasks.indexWhere((t) => t.id == task?.id)] = task; + if (secondTask != null) + _tasks[_tasks.indexWhere((t) => t.id == secondTask!.id)] = secondTask; + } + + _buckets[newBucketIndex].tasks[_buckets[newBucketIndex].tasks.indexWhere((t) => t.id == task?.id)] = task; + _buckets[newBucketIndex].tasks.sort((a, b) => a.kanbanPosition!.compareTo(b.kanbanPosition!)); + + notifyListeners(); + } +} From 33242c2bfb325ab0a1d2f0e8f14c1f722791f9e5 Mon Sep 17 00:00:00 2001 From: Benimautner Date: Sun, 23 Jul 2023 17:25:58 +0200 Subject: [PATCH 07/12] migrated to material3, added option for other themes for later --- lib/main.dart | 12 ++++++++---- lib/pages/home.dart | 16 ++++++++-------- lib/pages/project/project_edit.dart | 2 +- lib/pages/project/project_task_list.dart | 1 + lib/pages/settings.dart | 17 +++++++++++------ lib/service/services.dart | 21 ++++++++++++++------- lib/theme/theme.dart | 22 +++++++++++++++++++++- 7 files changed, 64 insertions(+), 27 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index ec49aef..0e66d5e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -97,11 +97,15 @@ class VikunjaApp extends StatelessWidget { return new ValueListenableBuilder(valueListenable: updateTheme, builder: (_,mode,__) { updateTheme.value = false; Future theme = manager.getThemeMode().then((value) { - if (value == ThemeMode.dark) { - return buildVikunjaDarkTheme(); - } else { - return buildVikunjaTheme(); + switch(value) { + case FlutterThemeMode.dark: + return buildVikunjaDarkTheme(); + case FlutterThemeMode.materialUi: + return buildVikunjaMaterialTheme(); + default: + return buildVikunjaTheme(); } + }); return FutureBuilder( future: theme, diff --git a/lib/pages/home.dart b/lib/pages/home.dart index 48100e4..a92e04e 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -38,10 +38,10 @@ class HomePageState extends State { SettingsPage() ]; - List navbarItems = [ - BottomNavigationBarItem(icon: Icon(Icons.home), label: "Home"), - BottomNavigationBarItem(icon: Icon(Icons.list), label: "Projects"), - BottomNavigationBarItem(icon: Icon(Icons.settings), label: "Settings"), + List navbarItems = [ + NavigationDestination(icon: Icon(Icons.home), label: "Home"), + NavigationDestination(icon: Icon(Icons.list), label: "Projects"), + NavigationDestination(icon: Icon(Icons.settings), label: "Settings"), ]; @override @@ -51,10 +51,10 @@ class HomePageState extends State { drawerItem = _getDrawerItemWidget(_selectedDrawerIndex); return new Scaffold( - bottomNavigationBar: BottomNavigationBar( - items: navbarItems, - currentIndex: _selectedDrawerIndex, - onTap: (index) { + bottomNavigationBar: NavigationBar( + destinations: navbarItems, + selectedIndex: _selectedDrawerIndex, + onDestinationSelected: (index) { setState(() { _selectedDrawerIndex = index; }); diff --git a/lib/pages/project/project_edit.dart b/lib/pages/project/project_edit.dart index c7a589e..fb89886 100644 --- a/lib/pages/project/project_edit.dart +++ b/lib/pages/project/project_edit.dart @@ -156,7 +156,7 @@ class _ProjectEditPageState extends State { VikunjaGlobal.of(context).projectService.update(newProject).then((_) { setState(() => _loading = false); ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text('The task was updated successfully!'), + content: Text('The project was updated successfully!'), )); }).catchError((err) { setState(() => _loading = false); diff --git a/lib/pages/project/project_task_list.dart b/lib/pages/project/project_task_list.dart index 7220e39..0972e5b 100644 --- a/lib/pages/project/project_task_list.dart +++ b/lib/pages/project/project_task_list.dart @@ -369,6 +369,7 @@ class _ListPageState extends State { )); setState(() { _loadingTasks.remove(newTask); + _loadList(); }); }); } diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index ffb55d8..88f88fd 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -6,6 +6,7 @@ import 'package:vikunja_app/models/list.dart'; import '../main.dart'; import '../models/project.dart'; +import '../service/services.dart'; class SettingsPage extends StatefulWidget { @@ -21,7 +22,7 @@ class SettingsPageState extends State { String? versionTag, newestVersionTag; late TextEditingController durationTextController; bool initialized = false; - ThemeMode? themeMode; + FlutterThemeMode? themeMode; void init() { @@ -114,23 +115,27 @@ class SettingsPageState extends State { Divider(), ListTile( title: Text("Theme"), - trailing: DropdownButton( + trailing: DropdownButton( items: [ DropdownMenuItem( child: Text("System"), - value: ThemeMode.system, + value: FlutterThemeMode.system, ), DropdownMenuItem( child: Text("Light"), - value: ThemeMode.light, + value: FlutterThemeMode.light, ), DropdownMenuItem( child: Text("Dark"), - value: ThemeMode.dark, + value: FlutterThemeMode.dark, + ), + DropdownMenuItem( + child: Text("Material You"), + value: FlutterThemeMode.materialUi, ), ], value: themeMode, - onChanged: (ThemeMode? value) { + onChanged: (FlutterThemeMode? value) { VikunjaGlobal.of(context) .settingsManager .setThemeMode(value!); diff --git a/lib/service/services.dart b/lib/service/services.dart index 26d2180..ced10fb 100644 --- a/lib/service/services.dart +++ b/lib/service/services.dart @@ -309,24 +309,31 @@ class SettingsManager { return _storage.write(key: "recent-servers", value: jsonEncode(server)); } - Future getThemeMode() async { + Future getThemeMode() async { String? theme_mode = await _storage.read(key: "theme_mode"); if(theme_mode == null) - setThemeMode(ThemeMode.system); + setThemeMode(FlutterThemeMode.system); switch(theme_mode) { case "system": - return ThemeMode.system; + return FlutterThemeMode.system; case "light": - return ThemeMode.light; + return FlutterThemeMode.light; case "dark": - return ThemeMode.dark; + return FlutterThemeMode.dark; default: - return ThemeMode.system; + return FlutterThemeMode.system; } } - Future setThemeMode(ThemeMode newMode) async { + Future setThemeMode(FlutterThemeMode newMode) async { await _storage.write(key: "theme_mode", value: newMode.toString().split('.').last); } } + +enum FlutterThemeMode { + system, + light, + dark, + materialUi +} \ No newline at end of file diff --git a/lib/theme/theme.dart b/lib/theme/theme.dart index 29d7e1a..5cdf31b 100644 --- a/lib/theme/theme.dart +++ b/lib/theme/theme.dart @@ -6,8 +6,15 @@ import 'package:vikunja_app/theme/constants.dart'; ThemeData buildVikunjaTheme() => _buildVikunjaTheme(ThemeData.light()); ThemeData buildVikunjaDarkTheme() => _buildVikunjaTheme(ThemeData.dark(), isDark: true); +ThemeData buildVikunjaMaterialTheme() { + return _buildVikunjaTheme(ThemeData.light()).copyWith( + useMaterial3: true, + ); +} + ThemeData _buildVikunjaTheme(ThemeData base, {bool isDark = false}) { return base.copyWith( + useMaterial3: true, errorColor: vRed, primaryColor: vPrimaryDark, primaryColorLight: vPrimary, @@ -31,7 +38,20 @@ ThemeData _buildVikunjaTheme(ThemeData base, {bool isDark = false}) { vWhite, // This does not work, looks like a bug in Flutter: https://github.com/flutter/flutter/issues/19623 ), ), - bottomNavigationBarTheme: base.bottomNavigationBarTheme.copyWith( + inputDecorationTheme: InputDecorationTheme( + enabledBorder: UnderlineInputBorder( + borderSide: const BorderSide(color: Colors.grey, width: 1) + ), + + ), + + dividerTheme: DividerThemeData( + color: () { + return isDark ? Colors.white10 : Colors.black12; + }(), + ), + navigationBarTheme: base.navigationBarTheme.copyWith( + indicatorColor: vPrimary, // Make bottomNavigationBar backgroundColor darker to provide more separation backgroundColor: () { final _hslColor = HSLColor.fromColor( From c3a817273916d06321b734f549e0cb5ea94a52c4 Mon Sep 17 00:00:00 2001 From: Benimautner Date: Sun, 23 Jul 2023 21:56:34 +0200 Subject: [PATCH 08/12] added material you, changed button style --- lib/main.dart | 21 +++++-- lib/pages/project/project_task_list.dart | 70 ++++++++++++------------ lib/pages/settings.dart | 8 ++- lib/service/services.dart | 7 ++- lib/theme/button.dart | 3 +- lib/theme/buttonText.dart | 3 +- lib/theme/theme.dart | 9 ++- pubspec.lock | 10 +++- pubspec.yaml | 1 + 9 files changed, 86 insertions(+), 46 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 0e66d5e..1f80c0f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -96,12 +97,16 @@ class VikunjaApp extends StatelessWidget { return new ValueListenableBuilder(valueListenable: updateTheme, builder: (_,mode,__) { updateTheme.value = false; + FlutterThemeMode themeMode = FlutterThemeMode.system; Future theme = manager.getThemeMode().then((value) { + themeMode = value; switch(value) { case FlutterThemeMode.dark: return buildVikunjaDarkTheme(); - case FlutterThemeMode.materialUi: - return buildVikunjaMaterialTheme(); + case FlutterThemeMode.materialYouLight: + return buildVikunjaMaterialLightTheme(); + case FlutterThemeMode.materialYouDark: + return buildVikunjaMaterialDarkTheme(); default: return buildVikunjaTheme(); } @@ -111,14 +116,22 @@ class VikunjaApp extends StatelessWidget { future: theme, builder: (BuildContext context, AsyncSnapshot data) { if(data.hasData) { - return new MaterialApp( + return new DynamicColorBuilder(builder: (lightTheme, darkTheme) + { + ThemeData? themeData = data.data; + if(themeMode == FlutterThemeMode.materialYouLight) + themeData = themeData?.copyWith(colorScheme: lightTheme); + else if(themeMode == FlutterThemeMode.materialYouDark) + themeData = themeData?.copyWith(colorScheme: darkTheme); + return MaterialApp( title: 'Vikunja', - theme: data.data, + theme: themeData, scaffoldMessengerKey: globalSnackbarKey, navigatorKey: navkey, // <= this home: this.home, ); + }); } else { return Center(child: CircularProgressIndicator()); } diff --git a/lib/pages/project/project_task_list.dart b/lib/pages/project/project_task_list.dart index 0972e5b..ade420e 100644 --- a/lib/pages/project/project_task_list.dart +++ b/lib/pages/project/project_task_list.dart @@ -97,7 +97,7 @@ class _ListPageState extends State { ]); break; case PageStatus.success: - body = taskState.tasks.length > 0 || taskState.buckets.length > 0 + body = taskState.tasks.length > 0 || taskState.buckets.length > 0 || _project.subprojects!.length > 0 ? ListenableProvider.value( value: taskState, child: Theme( @@ -182,6 +182,7 @@ class _ListPageState extends State { Widget buildSubProjectSelector() { return Container( height: 80, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), child: ListView( scrollDirection: Axis.horizontal, @@ -213,42 +214,43 @@ class _ListPageState extends State { } Widget _listView(BuildContext context) { - List subProjectView = []; + List children = []; if(widget.project.subprojects?.length != 0) { - subProjectView.add(Padding(child: Text("Projects", style: TextStyle(fontWeight: FontWeight.bold),), padding: EdgeInsets.fromLTRB(0, 10, 0, 0),)); - subProjectView.add(buildSubProjectSelector()); - subProjectView.add(Padding(child: Text("Tasks", style: TextStyle(fontWeight: FontWeight.bold),), padding: EdgeInsets.fromLTRB(0, 10, 0, 0),)); - subProjectView.add(Divider()); + children.add(Padding(child: Text("Projects", style: TextStyle(fontWeight: FontWeight.bold),), padding: EdgeInsets.fromLTRB(0, 10, 0, 0),)); + children.add(buildSubProjectSelector()); + + } + if(taskState.tasks.length != 0) { + children.add(Padding(child: Text("Tasks", style: TextStyle(fontWeight: FontWeight.bold),), padding: EdgeInsets.fromLTRB(0, 10, 0, 0),)); + children.add(Divider()); + children.add(Expanded(child: + ListView.builder( + padding: EdgeInsets.symmetric(vertical: 8.0), + itemCount: taskState.tasks.length * 2, + itemBuilder: (context, i) { + if (i.isOdd) return Divider(); + + if (_loadingTasks.isNotEmpty) { + final loadingTask = _loadingTasks.removeLast(); + return _buildLoadingTile(loadingTask); + } + + final index = i ~/ 2; + + if (taskState.maxPages == _currentPage && + index == taskState.tasks.length) + throw Exception("Check itemCount attribute"); + + if (index >= taskState.tasks.length && + _currentPage < taskState.maxPages) { + _currentPage++; + _loadTasksForPage(_currentPage); + } + return _buildTile(taskState.tasks[index]); + }))); } - return Column( - children: [ - ...subProjectView, - Expanded(child: - ListView.builder( - padding: EdgeInsets.symmetric(vertical: 8.0), - itemCount: taskState.tasks.length * 2, - itemBuilder: (context, i) { - if (i.isOdd) return Divider(); - - if (_loadingTasks.isNotEmpty) { - final loadingTask = _loadingTasks.removeLast(); - return _buildLoadingTile(loadingTask); - } - - final index = i ~/ 2; - - if (taskState.maxPages == _currentPage && - index == taskState.tasks.length) - throw Exception("Check itemCount attribute"); - - if (index >= taskState.tasks.length && - _currentPage < taskState.maxPages) { - _currentPage++; - _loadTasksForPage(_currentPage); - } - return _buildTile(taskState.tasks[index]); - }))]); + return Column(children: children); } Widget _buildTile(Task task) { diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index 88f88fd..e563028 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -130,8 +130,12 @@ class SettingsPageState extends State { value: FlutterThemeMode.dark, ), DropdownMenuItem( - child: Text("Material You"), - value: FlutterThemeMode.materialUi, + child: Text("Material You Light"), + value: FlutterThemeMode.materialYouLight, + ), + DropdownMenuItem( + child: Text("Material You Dark"), + value: FlutterThemeMode.materialYouDark, ), ], value: themeMode, diff --git a/lib/service/services.dart b/lib/service/services.dart index ced10fb..9a4f7b9 100644 --- a/lib/service/services.dart +++ b/lib/service/services.dart @@ -320,6 +320,10 @@ class SettingsManager { return FlutterThemeMode.light; case "dark": return FlutterThemeMode.dark; + case "materialYouLight": + return FlutterThemeMode.materialYouLight; + case "materialYouDark": + return FlutterThemeMode.materialYouDark; default: return FlutterThemeMode.system; } @@ -335,5 +339,6 @@ enum FlutterThemeMode { system, light, dark, - materialUi + materialYouLight, + materialYouDark, } \ No newline at end of file diff --git a/lib/theme/button.dart b/lib/theme/button.dart index 4647e66..44b22ba 100644 --- a/lib/theme/button.dart +++ b/lib/theme/button.dart @@ -17,6 +17,7 @@ class FancyButton extends StatelessWidget { @override Widget build(BuildContext context) { + return ElevatedButton(onPressed: onPressed, child: child); return Padding( padding: vStandardVerticalPadding, child: Container( @@ -33,7 +34,7 @@ class FancyButton extends StatelessWidget { ]), child: Material( borderRadius: BorderRadius.circular(3), - color: vButtonColor, + color: Theme.of(context).colorScheme.primary, child: InkWell( onTap: onPressed, child: Center( diff --git a/lib/theme/buttonText.dart b/lib/theme/buttonText.dart index 0603a78..4c12819 100644 --- a/lib/theme/buttonText.dart +++ b/lib/theme/buttonText.dart @@ -11,9 +11,10 @@ class VikunjaButtonText extends StatelessWidget { @override Widget build(BuildContext context) { + return Text(text); return Text( text, - style: TextStyle(color: vButtonTextColor, fontWeight: FontWeight.w600), + style: TextStyle(color: Theme.of(context).primaryTextTheme.labelMedium?.color, fontWeight: FontWeight.w600), ); } } diff --git a/lib/theme/theme.dart b/lib/theme/theme.dart index 5cdf31b..ccdf5dd 100644 --- a/lib/theme/theme.dart +++ b/lib/theme/theme.dart @@ -6,8 +6,13 @@ import 'package:vikunja_app/theme/constants.dart'; ThemeData buildVikunjaTheme() => _buildVikunjaTheme(ThemeData.light()); ThemeData buildVikunjaDarkTheme() => _buildVikunjaTheme(ThemeData.dark(), isDark: true); -ThemeData buildVikunjaMaterialTheme() { - return _buildVikunjaTheme(ThemeData.light()).copyWith( +ThemeData buildVikunjaMaterialLightTheme() { + return ThemeData.light().copyWith( + useMaterial3: true, + ); +} +ThemeData buildVikunjaMaterialDarkTheme() { + return ThemeData.dark().copyWith( useMaterial3: true, ); } diff --git a/pubspec.lock b/pubspec.lock index 7b05cc9..a7a280d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -177,6 +177,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0+3" + dynamic_color: + dependency: "direct main" + description: + name: dynamic_color + sha256: de4798a7069121aee12d5895315680258415de9b00e717723a1bd73d58f0126d + url: "https://pub.dev" + source: hosted + version: "1.6.6" fake_async: dependency: transitive description: @@ -1023,4 +1031,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.0.0-0 <4.0.0" - flutter: ">=3.3.0" + flutter: ">=3.4.0-17.0.pre" diff --git a/pubspec.yaml b/pubspec.yaml index b7509cb..11a08fb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -31,6 +31,7 @@ dependencies: url_launcher: ^6.1.7 workmanager: ^0.5.1 permission_handler: ^10.2.0 + dynamic_color: ^1.6.6 dev_dependencies: flutter_test: From c7a556311d6bc52efdfab054fd0d02952f3da52b Mon Sep 17 00:00:00 2001 From: Benimautner Date: Sun, 23 Jul 2023 22:08:09 +0200 Subject: [PATCH 09/12] fixed new button width --- lib/theme/button.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/theme/button.dart b/lib/theme/button.dart index 44b22ba..87ccf54 100644 --- a/lib/theme/button.dart +++ b/lib/theme/button.dart @@ -17,7 +17,11 @@ class FancyButton extends StatelessWidget { @override Widget build(BuildContext context) { - return ElevatedButton(onPressed: onPressed, child: child); + return ElevatedButton(onPressed: onPressed, + child: SizedBox( + width: width, + child: Center(child: child), + ),); return Padding( padding: vStandardVerticalPadding, child: Container( From 6b276e511dac78098fccf873b65f294b3600abc3 Mon Sep 17 00:00:00 2001 From: Benimautner Date: Sun, 23 Jul 2023 22:34:10 +0200 Subject: [PATCH 10/12] added option to select what to see on landing page --- lib/pages/landing_page.dart | 107 ++++++++++++++++++++++++------------ lib/service/services.dart | 8 +++ 2 files changed, 79 insertions(+), 36 deletions(-) diff --git a/lib/pages/landing_page.dart b/lib/pages/landing_page.dart index d4c3f1a..670ac96 100644 --- a/lib/pages/landing_page.dart +++ b/lib/pages/landing_page.dart @@ -19,11 +19,8 @@ class HomeScreenWidget extends StatefulWidget { // TODO: implement createState throw UnimplementedError(); } - - } - class LandingPage extends HomeScreenWidget { LandingPage({Key? key}) : super(key: key); @@ -31,10 +28,10 @@ class LandingPage extends HomeScreenWidget { State createState() => LandingPageState(); } - class LandingPageState extends State with AfterLayoutMixin { int? defaultList; + bool onlyDueDate = true; List _tasks = []; PageStatus landingPageStatus = PageStatus.built; static const platform = const MethodChannel('vikunja'); @@ -56,6 +53,7 @@ class LandingPageState extends State } catch (e) { log(e.toString()); } + VikunjaGlobal.of(context).settingsManager.getLandingPageOnlyDueDateTasks().then((value) => onlyDueDate = value); })); super.initState(); } @@ -105,10 +103,8 @@ class LandingPageState extends State ]); break; case PageStatus.empty: - body = new Stack(children: [ - ListView(), - Center(child: Text("This view is empty")) - ]); + body = new Stack( + children: [ListView(), Center(child: Text("This view is empty"))]); break; case PageStatus.success: body = ListView( @@ -121,19 +117,44 @@ class LandingPageState extends State break; } return new Scaffold( - body: - RefreshIndicator(onRefresh: () => _loadList(context), child: body), - floatingActionButton: Builder( - builder: (context) => FloatingActionButton( - onPressed: () { - _addItemDialog(context); - }, - child: const Icon(Icons.add), - )), + body: RefreshIndicator(onRefresh: () => _loadList(context), child: body), + floatingActionButton: Builder( + builder: (context) => FloatingActionButton( + onPressed: () { + _addItemDialog(context); + }, + child: const Icon(Icons.add), + )), appBar: AppBar( title: Text("Vikunja"), + actions: [ + PopupMenuButton(itemBuilder: (BuildContext context) { + return [ + PopupMenuItem( + child: + InkWell( + onTap: () { + Navigator.pop(context); + bool newval = !onlyDueDate; + VikunjaGlobal.of(context).settingsManager.setLandingPageOnlyDueDateTasks(newval).then((value) { + setState(() { + onlyDueDate = newval; + _loadList(context); + }); + }); + }, + child: + Row(mainAxisAlignment: MainAxisAlignment.end, children: [ + Text("Only show tasks with due date"), + Checkbox( + value: onlyDueDate, + onChanged: (bool? value) { }, + ) + ]))) + ]; + }), + ], ), - ); } @@ -196,27 +217,41 @@ class LandingPageState extends State _tasks = []; landingPageStatus = PageStatus.loading; // FIXME: loads and reschedules tasks each time list is updated - VikunjaGlobal.of(context).notifications.scheduleDueNotifications(VikunjaGlobal.of(context).taskService); + VikunjaGlobal.of(context) + .notifications + .scheduleDueNotifications(VikunjaGlobal.of(context).taskService); return VikunjaGlobal.of(context) - .taskService - .getByOptions(TaskServiceOptions()) - .then?>((taskList) { - if (taskList != null && taskList.isEmpty) { - setState(() { - landingPageStatus = PageStatus.empty; - }); - return null; + .settingsManager + .getLandingPageOnlyDueDateTasks() + .then((showOnlyDueDateTasks) { + if (!showOnlyDueDateTasks) { + return VikunjaGlobal.of(context).taskService.getAll().then((value) => _handleTaskList(value)); + } else { + return VikunjaGlobal + .of(context) + .taskService + .getByOptions(TaskServiceOptions()) + .then?>((taskList) => _handleTaskList(taskList)); } - //taskList.forEach((task) {task.list = lists.firstWhere((element) => element.id == task.list_id);}); - setState(() { - if (taskList != null) { - _tasks = taskList; - landingPageStatus = PageStatus.success; - } else { - landingPageStatus = PageStatus.error; - } }); - return null; + } + + Future _handleTaskList(List? taskList) { + if (taskList != null && taskList.isEmpty) { + setState(() { + landingPageStatus = PageStatus.empty; + }); + return Future.value(); + } + //taskList.forEach((task) {task.list = lists.firstWhere((element) => element.id == task.list_id);}); + setState(() { + if (taskList != null) { + _tasks = taskList; + landingPageStatus = PageStatus.success; + } else { + landingPageStatus = PageStatus.error; + } }); + return Future.value(); } } diff --git a/lib/service/services.dart b/lib/service/services.dart index 9a4f7b9..f8b99e5 100644 --- a/lib/service/services.dart +++ b/lib/service/services.dart @@ -259,6 +259,7 @@ class SettingsManager { "workmanager-duration": "0", "recent-servers": "[\"https://try.vikunja.io\"]", "theme_mode": "system", + "landing-page-due-date-tasks": "1" }; void applydefaults() { @@ -283,6 +284,13 @@ class SettingsManager { _storage.write(key: "ignore-certificates", value: value ? "1" : "0"); } + Future getLandingPageOnlyDueDateTasks() { + return _storage.read(key: "landing-page-due-date-tasks").then((value) => value == "1"); + } + Future setLandingPageOnlyDueDateTasks(bool value) { + return _storage.write(key: "landing-page-due-date-tasks", value: value ? "1" : "0"); + } + Future getVersionNotifications() { return _storage.read(key: "get-version-notifications"); From c4885b4d419add27543f19a6fcef633e04e5eb8d Mon Sep 17 00:00:00 2001 From: Benimautner Date: Mon, 24 Jul 2023 00:00:37 +0200 Subject: [PATCH 11/12] changed how options are used, fixed "only show tasks with due date" --- lib/api/task_implementation.dart | 2 +- lib/managers/notifications.dart | 26 ++++++++------------------ lib/pages/landing_page.dart | 29 ++++++++++++++++++----------- lib/service/services.dart | 31 +++++++++++++++++++------------ 4 files changed, 46 insertions(+), 42 deletions(-) diff --git a/lib/api/task_implementation.dart b/lib/api/task_implementation.dart index b916a12..786364d 100644 --- a/lib/api/task_implementation.dart +++ b/lib/api/task_implementation.dart @@ -92,7 +92,7 @@ class TaskAPIService extends APIService implements TaskService { Future?> getByOptions(TaskServiceOptions options) { String optionString = options.getOptions(); return client - .get('/tasks/all?$optionString') + .get('/tasks/all$optionString') .then((response) { if (response == null) return null; return convertList(response.body, (result) => Task.fromJson(result)); diff --git a/lib/managers/notifications.dart b/lib/managers/notifications.dart index 62f3af1..973ca89 100644 --- a/lib/managers/notifications.dart +++ b/lib/managers/notifications.dart @@ -9,6 +9,8 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'as import 'package:rxdart/subjects.dart' as rxSub; import 'package:vikunja_app/service/services.dart'; +import '../models/task.dart'; + class NotificationClass { final int? id; final String? title; @@ -120,30 +122,18 @@ class NotificationClass { } - Future scheduleDueNotifications(TaskService taskService) async { - final tasks = await taskService.getByOptions(new TaskServiceOptions(newOptions: [ - TaskServiceOption("filter_by", [ - TaskServiceOptionFilterBy.done, - TaskServiceOptionFilterBy.due_date - ]), - TaskServiceOption( - "filter_comparator", [ - TaskServiceOptionFilterComparator.equals, - TaskServiceOptionFilterComparator.greater - ]), - TaskServiceOption( - "filter_concat", TaskServiceOptionFilterConcat.and), - TaskServiceOption("filter_value", [ - TaskServiceOptionFilterValue.enum_false, - DateTime.now().toUtc().toIso8601String() - ]), - ])); + Future scheduleDueNotifications(TaskService taskService, + {List? tasks}) async { + if (tasks == null) + tasks = await taskService.getAll(); if (tasks == null) { print("did not receive tasks on notification update"); return; } await notificationsPlugin.cancelAll(); for (final task in tasks) { + if(task.done) + continue; for (final reminder in task.reminderDates) { scheduleNotification( "Reminder", diff --git a/lib/pages/landing_page.dart b/lib/pages/landing_page.dart index 670ac96..ba33f99 100644 --- a/lib/pages/landing_page.dart +++ b/lib/pages/landing_page.dart @@ -224,19 +224,26 @@ class LandingPageState extends State .settingsManager .getLandingPageOnlyDueDateTasks() .then((showOnlyDueDateTasks) { - if (!showOnlyDueDateTasks) { - return VikunjaGlobal.of(context).taskService.getAll().then((value) => _handleTaskList(value)); - } else { - return VikunjaGlobal - .of(context) - .taskService - .getByOptions(TaskServiceOptions()) - .then?>((taskList) => _handleTaskList(taskList)); - } - }); + return VikunjaGlobal + .of(context) + .taskService + .getByOptions(TaskServiceOptions( + newOptions: [ + TaskServiceOption("filter_by", "done"), + TaskServiceOption("filter_value", "false"), + ], + clearOther: true + )) + .then?>((taskList) => _handleTaskList(taskList, showOnlyDueDateTasks)); + + }).onError((error, stackTrace) {print("error");}); } - Future _handleTaskList(List? taskList) { + Future _handleTaskList(List? taskList, bool showOnlyDueDateTasks) { + if(showOnlyDueDateTasks) + taskList?.removeWhere((element) => element.dueDate == null || element.dueDate!.year == 0001); + taskList?.forEach((element) {print(element.dueDate);}); + if (taskList != null && taskList.isEmpty) { setState(() { landingPageStatus = PageStatus.empty; diff --git a/lib/service/services.dart b/lib/service/services.dart index f8b99e5..6c9f81c 100644 --- a/lib/service/services.dart +++ b/lib/service/services.dart @@ -73,9 +73,10 @@ class TaskServiceOption { } } -List defaultOptions = [ +final List defaultOptions = [ TaskServiceOption("sort_by", - [TaskServiceOptionSortBy.due_date, TaskServiceOptionSortBy.id]), + [TaskServiceOptionSortBy.due_date, + TaskServiceOptionSortBy.id]), TaskServiceOption( "order_by", TaskServiceOptionOrderBy.asc), TaskServiceOption("filter_by", [ @@ -84,7 +85,7 @@ List defaultOptions = [ ]), TaskServiceOption("filter_value", [ TaskServiceOptionFilterValue.enum_false, - '0001-01-02T00:00:00.000Z' + '1970-01-01T00:00:00.000Z' ]), TaskServiceOption( "filter_comparator", [ @@ -96,15 +97,20 @@ List defaultOptions = [ ]; class TaskServiceOptions { - List? options; + List options = []; - TaskServiceOptions({List? newOptions}) { - options = [...defaultOptions]; + TaskServiceOptions({List? newOptions, bool clearOther = false}) { + if(!clearOther) + options = new List.from(defaultOptions); if (newOptions != null) { for (TaskServiceOption custom_option in newOptions) { - int index = options!.indexWhere((element) => element.name == custom_option.name); - options!.removeAt(index); - options!.insert(index, custom_option); + int index = options.indexWhere((element) => element.name == custom_option.name); + if(index > -1) { + options.removeAt(index); + } else { + index = options.length; + } + options.insert(index, custom_option); } } } @@ -112,8 +118,8 @@ class TaskServiceOptions { String getOptions() { String result = ''; - if (options == null) return ''; - for (TaskServiceOption option in options!) { + if (options.length == 0) return ''; + for (TaskServiceOption option in options) { dynamic value = option.getValue(); if (value is List) { for (dynamic valueEntry in value) { @@ -124,7 +130,8 @@ class TaskServiceOptions { } } - if (result.startsWith('&')) result.substring(1); + if (result.startsWith('&')) result = result.substring(1); + result = "?" + result; return result; } } From 7b6da4970c1aa213bd786c32a876a607e503f903 Mon Sep 17 00:00:00 2001 From: Benimautner Date: Mon, 24 Jul 2023 00:12:42 +0200 Subject: [PATCH 12/12] removed add project button, pushed version tag --- lib/pages/project/overview.dart | 5 +---- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/pages/project/overview.dart b/lib/pages/project/overview.dart index 9e0cbb5..68a869b 100644 --- a/lib/pages/project/overview.dart +++ b/lib/pages/project/overview.dart @@ -121,10 +121,7 @@ class _ProjectOverviewPageState extends State .toList()), onRefresh: _loadProjects, ), - floatingActionButton: Builder( - builder: (context) => FloatingActionButton( - onPressed: () => _addProjectDialog(context), - child: const Icon(Icons.add))), + appBar: AppBar( title: Text("Projects"), ), diff --git a/pubspec.yaml b/pubspec.yaml index 11a08fb..b6c7385 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: vikunja_app description: Vikunja as Flutter cross platform app -version: 0.1.0-beta +version: 0.1.1-beta environment: sdk: ">=2.18.0 <3.0.0"