1
0
mirror of https://github.com/go-vikunja/app synced 2024-06-02 18:49:47 +00:00

feat: improve landing page

This commit is contained in:
Denys Vitali 2024-03-27 00:37:57 +01:00
parent ecd2006955
commit 37009e2eaa
No known key found for this signature in database
GPG Key ID: 5227C664145098BC
5 changed files with 336 additions and 311 deletions

View File

@ -1,14 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
import 'package:vikunja_app/components/label.dart';
import 'package:vikunja_app/models/task.dart';
import 'package:vikunja_app/pages/list/task_edit.dart';
import 'package:vikunja_app/stores/project_store.dart';
import 'package:vikunja_app/theme/constants.dart';
import 'package:vikunja_app/utils/priority.dart';
import '../models/label.dart';
import '../models/task.dart';
import '../pages/list/task_edit.dart';
import '../stores/project_store.dart';
import '../theme/constants.dart';
import 'label.dart';
class TaskBottomSheet extends StatefulWidget {
final Task task;
final bool showInfo;
@ -26,129 +24,131 @@ class TaskBottomSheet extends StatefulWidget {
this.showInfo = false,
this.onMarkedAsDone,
}) : super(key: key);
/*
@override
TaskTileState createState() {
return new TaskTileState(this.task, this.loading);
}
*/
@override
TaskBottomSheetState createState() => TaskBottomSheetState(this.task);
TaskBottomSheetState createState() => TaskBottomSheetState();
}
class TaskBottomSheetState extends State<TaskBottomSheet> {
Task _currentTask;
late Task _currentTask;
TaskBottomSheetState(this._currentTask);
@override
void initState() {
super.initState();
_currentTask = widget.task;
}
@override
Widget build(BuildContext context) {
ThemeData theme = Theme.of(context);
return Container(
height: MediaQuery.of(context).size.height * 0.9,
child: Padding(
padding: EdgeInsets.fromLTRB(20, 10, 10, 20),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
// Title and edit button
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(_currentTask.title,
style: theme.textTheme.headlineLarge),
IconButton(
onPressed: () {
Navigator.push<Task>(
context,
MaterialPageRoute(
builder: (buildContext) => TaskEditPage(
task: _currentTask,
taskState: widget.taskState,
),
),
)
.then((task) => setState(() {
if (task != null) _currentTask = task;
}))
.whenComplete(() => widget.onEdit());
},
icon: Icon(Icons.edit)),
],
),
Wrap(
spacing: 10,
children: _currentTask.labels.map((Label label) {
return LabelComponent(
label: label,
);
}).toList()),
final theme = Theme.of(context);
return SingleChildScrollView(
child: Padding(
padding: EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_currentTask.title,
style: theme.textTheme.headline6,
),
IconButton(
onPressed: _editTask,
icon: Icon(Icons.edit),
),
],
),
SizedBox(height: 16),
Wrap(
spacing: 8,
runSpacing: 8,
children: _currentTask.labels.map((label) {
return LabelComponent(label: label);
}).toList(),
),
HtmlWidget(
_currentTask.description.isNotEmpty
? _currentTask.description
: 'No description',
),
SizedBox(height: 16),
_buildRowWithIconAndText(
Icons.access_time,
_currentTask.dueDate != null
? vDateFormatShort.format(_currentTask.dueDate!.toLocal())
: 'No due date',
),
_buildRowWithIconAndText(
Icons.play_arrow_rounded,
_currentTask.startDate != null
? vDateFormatShort.format(_currentTask.startDate!.toLocal())
: 'No start date',
),
_buildRowWithIconAndText(
Icons.stop_rounded,
_currentTask.endDate != null
? vDateFormatShort.format(_currentTask.endDate!.toLocal())
: 'No end date',
),
_buildRowWithIconAndText(
Icons.priority_high,
_currentTask.priority != null
? priorityToString(_currentTask.priority)
: 'No priority',
),
_buildRowWithIconAndText(
Icons.percent,
_currentTask.percent_done != null
? '${(_currentTask.percent_done! * 100).toInt()}%'
: 'Unset',
),
],
),
),
);
}
// description with html rendering
Text("Description", style: theme.textTheme.headlineSmall),
Padding(
padding: EdgeInsets.fromLTRB(10, 0, 0, 0),
child: HtmlWidget(_currentTask.description.isNotEmpty
? _currentTask.description
: "No description"),
),
// Due date
Row(
children: [
Icon(Icons.access_time),
Padding(padding: EdgeInsets.fromLTRB(10, 0, 0, 0)),
Text(_currentTask.dueDate != null
? vDateFormatShort.format(_currentTask.dueDate!.toLocal())
: "No due date"),
],
),
// start date
Row(
children: [
Icon(Icons.play_arrow_rounded),
Padding(padding: EdgeInsets.fromLTRB(10, 0, 0, 0)),
Text(_currentTask.startDate != null
? vDateFormatShort
.format(_currentTask.startDate!.toLocal())
: "No start date"),
],
),
// end date
Row(
children: [
Icon(Icons.stop_rounded),
Padding(padding: EdgeInsets.fromLTRB(10, 0, 0, 0)),
Text(_currentTask.endDate != null
? vDateFormatShort.format(_currentTask.endDate!.toLocal())
: "No end date"),
],
),
// priority
Row(
children: [
Icon(Icons.priority_high),
Padding(padding: EdgeInsets.fromLTRB(10, 0, 0, 0)),
Text(_currentTask.priority != null
? priorityToString(_currentTask.priority)
: "No priority"),
],
),
// progress
Row(
children: [
Icon(Icons.percent),
Padding(padding: EdgeInsets.fromLTRB(10, 0, 0, 0)),
Text(_currentTask.percent_done != null
? (_currentTask.percent_done! * 100).toInt().toString() +
"%"
: "Unset"),
],
),
],
),
));
void _editTask() {
Navigator.push<Task>(
context,
MaterialPageRoute(
builder: (buildContext) => TaskEditPage(
task: _currentTask,
taskState: widget.taskState,
),
),
).then((task) {
if (task != null) {
setState(() {
_currentTask = task;
});
}
widget.onEdit();
});
}
Widget _buildSectionTitle(String title) {
return Text(
title,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
),
);
}
Widget _buildRowWithIconAndText(IconData icon, String text) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Icon(icon),
SizedBox(width: 8),
Text(text),
],
),
);
}
}

View File

@ -1,160 +1,65 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
import 'package:provider/provider.dart';
import 'package:vikunja_app/components/TaskBottomSheet.dart';
import 'package:vikunja_app/models/project.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/project_store.dart';
import 'package:vikunja_app/utils/misc.dart';
import 'package:vikunja_app/utils/priority.dart';
import '../stores/project_store.dart';
class TaskTile extends StatefulWidget {
final Task task;
final Function onEdit;
final bool showInfo;
final bool loading;
final ValueSetter<bool>? onMarkedAsDone;
final Map<int, Project>? projectsMap;
const TaskTile({
Key? key,
required this.task,
required this.onEdit,
this.projectsMap,
this.loading = false,
this.showInfo = false,
this.onMarkedAsDone,
}) : super(key: key);
/*
@override
TaskTileState createState() {
return new TaskTileState(this.task, this.loading);
}
*/
@override
TaskTileState createState() => TaskTileState(this.task);
_TaskTileState createState() => _TaskTileState();
}
Widget? _buildTaskSubtitle(Task? task, bool showInfo, BuildContext context) {
Duration? durationUntilDue = task?.dueDate?.difference(DateTime.now());
class _TaskTileState extends State<TaskTile> {
late Task _currentTask;
if (task == null) return null;
List<TextSpan> texts = [];
if (showInfo && task.hasDueDate) {
texts.add(TextSpan(
text: "Due " + durationToHumanReadable(durationUntilDue!),
style: durationUntilDue.isNegative
? TextStyle(color: Colors.red)
: Theme.of(context).textTheme.bodyMedium));
@override
void initState() {
super.initState();
_currentTask = widget.task;
}
if (task.priority != null && task.priority != 0) {
texts.add(TextSpan(
text: " !" + priorityToString(task.priority),
style: TextStyle(color: Colors.orange)));
}
//if(texts.isEmpty && task.description.isNotEmpty) {
// return HtmlWidget(task.description);
// }
if (texts.isNotEmpty) {
return RichText(text: TextSpan(children: texts));
}
return null;
}
class TaskTileState extends State<TaskTile> with AutomaticKeepAliveClientMixin {
Task _currentTask;
TaskTileState(this._currentTask);
@override
Widget build(BuildContext context) {
super.build(context);
final taskState = Provider.of<ProjectProvider>(context);
if (_currentTask.loading) {
return ListTile(
leading: Padding(
padding: const EdgeInsets.all(8.0),
child: SizedBox(
height: Checkbox.width,
width: Checkbox.width,
child: CircularProgressIndicator(
strokeWidth: 2.0,
)),
),
title: Text(_currentTask.title),
subtitle: _currentTask.description.isEmpty
? null
: HtmlWidget(_currentTask.description),
trailing: IconButton(
icon: Icon(Icons.edit),
onPressed: () {},
),
);
}
return IntrinsicHeight(
child: Row(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
Container(
width: 4.0, // Adjust the width of the red line
color: widget.task.color,
//margin: EdgeInsets.only(left: 10.0),
return ListTile(
onTap: () {
_showBottomSheet(context, taskState);
},
leading: _buildLeading(),
title: _buildTitle(),
subtitle: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_buildSubtitle(context),
SizedBox(height: 8),
buildChip() ?? Container(),
],
),
Flexible(
child: ListTile(
onTap: () {
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) {
return TaskBottomSheet(task: widget.task, onEdit: widget.onEdit, taskState: taskState);
});
},
title: widget.showInfo
? RichText(
text: TextSpan(
text: null,
children: <TextSpan>[
// TODO: get list name of task
//TextSpan(text: widget.task.list.title+" - ", style: TextStyle(color: Colors.grey)),
TextSpan(text: widget.task.title),
],
style: TextStyle(
color: Theme.of(context).brightness == Brightness.dark
? Colors.white
: Colors.black,
),
))
: Text(_currentTask.title),
subtitle: _buildTaskSubtitle(widget.task, widget.showInfo, context),
leading: Checkbox(
value: _currentTask.done,
onChanged: (bool? newValue) {
_change(newValue);
},
),
trailing: IconButton(
icon: Icon(Icons.edit),
onPressed: () {
Navigator.push<Task>(
context,
MaterialPageRoute(
builder: (buildContext) => TaskEditPage(
task: _currentTask,
taskState: taskState,
),
),
)
.then((task) => setState(() {
if (task != null) _currentTask = task;
}))
.whenComplete(() => widget.onEdit());
}),
))
]));
trailing: _buildTrailing(context),
);
}
void _change(bool? value) async {
@ -179,8 +84,123 @@ class TaskTileState extends State<TaskTile> with AutomaticKeepAliveClientMixin {
);
}
@override
bool get wantKeepAlive => _currentTask != widget.task;
}
Widget _buildLeading() {
return Checkbox(
value: _currentTask.done,
onChanged: (bool? newValue) {
_change(newValue);
},
);
}
typedef Future<void> TaskChanged(Task task, bool newValue);
Widget _buildTitle() {
return widget.showInfo
? RichText(
text: TextSpan(
text: null,
children: <TextSpan>[
TextSpan(
text: widget.task.title,
style: TextStyle(
decoration:
_currentTask.done ? TextDecoration.lineThrough : null,
),
),
],
style: TextStyle(
color: Theme.of(context).brightness == Brightness.dark
? Colors.white
: Colors.black,
),
),
)
: Text(
_currentTask.title,
style: TextStyle(
decoration: _currentTask.done ? TextDecoration.lineThrough : null,
),
);
}
Widget _buildSubtitle(BuildContext context) {
final durationUntilDue = _currentTask.dueDate?.difference(DateTime.now());
if (widget.loading) {
return CircularProgressIndicator(
strokeWidth: 2.0,
);
} else if (widget.showInfo && _currentTask.hasDueDate) {
return Text(
"Due " + durationToHumanReadable(durationUntilDue!),
style: TextStyle(
color: durationUntilDue.isNegative ? Colors.red : null,
),
);
} else if (_currentTask.priority != null && _currentTask.priority != 0) {
return Text(
" !" + priorityToString(_currentTask.priority),
style: TextStyle(color: Colors.orange),
);
} else if (_currentTask.description.isNotEmpty) {
return HtmlWidget(_currentTask.description);
} else {
return Container();
}
}
Widget _buildTrailing(BuildContext context) {
return IconButton(
icon: Icon(Icons.edit),
onPressed: () {
Navigator.push<Task>(
context,
MaterialPageRoute(
builder: (buildContext) => TaskEditPage(
task: _currentTask,
taskState: Provider.of<ProjectProvider>(context, listen: false),
),
),
).then((task) {
if (task != null) {
setState(() {
_currentTask = task;
});
}
widget.onEdit();
});
},
);
}
void _showBottomSheet(BuildContext context, ProjectProvider taskState) {
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) {
return TaskBottomSheet(
task: widget.task,
onEdit: widget.onEdit,
taskState: taskState,
);
},
);
}
Widget? buildChip() {
if (_currentTask.projectId == null || widget.projectsMap == null) {
return null;
}
Project? p = widget.projectsMap![_currentTask.projectId!];
if (p != null) {
return Transform(
transform: new Matrix4.identity()..scale(0.8),
child: Chip(
label: Text(p.title),
backgroundColor: p.color,
labelStyle: TextStyle(color: Colors.white),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity: VisualDensity(horizontal: 0.0, vertical: -4),
),
);
}
return null;
}
}

View File

@ -197,13 +197,20 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
}
var token = await _storage.read(key: currentUser);
var base = await _storage.read(key: '${currentUser}_base');
var xClientToken =
await _storage.read(key: '${currentUser}_x_client_token');
if (token == null || base == null) {
setState(() {
_loading = false;
});
return;
}
client.configure(token: token, base: base, authenticated: true);
client.configure(
token: token,
base: base,
authenticated: true,
xClientToken: xClientToken,
);
User loadedCurrentUser;
try {
loadedCurrentUser = await UserAPIService(client).getCurrentUser();

View File

@ -2,6 +2,7 @@ import 'package:after_layout/after_layout.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:vikunja_app/global.dart';
import 'package:vikunja_app/models/project.dart';
import 'package:vikunja_app/service/services.dart';
import 'dart:developer';
@ -33,6 +34,7 @@ class LandingPageState extends State<LandingPage>
int? defaultList;
bool onlyDueDate = true;
List<Task> _tasks = [];
Map<int, Project> _projectsMap = {};
PageStatus landingPageStatus = PageStatus.built;
static const platform = const MethodChannel('vikunja');
@ -115,9 +117,10 @@ class LandingPageState extends State<LandingPage>
body = ListView(
scrollDirection: Axis.vertical,
padding: EdgeInsets.symmetric(vertical: 8.0),
children:
ListTile.divideTiles(context: context, tiles: _listTasks(context))
.toList(),
children: ListTile.divideTiles(
context: context,
tiles: _listTasks(context),
).toList(),
);
break;
}
@ -205,66 +208,66 @@ class LandingPageState extends State<LandingPage>
}
List<Widget> _listTasks(BuildContext context) {
var tasks = (_tasks.map((task) => _buildTile(task, context))).toList();
//tasks.addAll(_loadingTasks.map(_buildLoadingTile));
return tasks;
return (_tasks.map((task) => _buildTile(task, context))).toList();
}
TaskTile _buildTile(Task task, BuildContext context) {
// key: UniqueKey() seems like a weird workaround to fix the loading issue
// is there a better way?
return TaskTile(
key: UniqueKey(),
key: Key("task_${task.id}"),
projectsMap: _projectsMap,
task: task,
onEdit: () => _loadList(context),
showInfo: true,
);
}
Future<void> _loadList(BuildContext context) {
Future<void> _loadList(BuildContext context) async {
_tasks = [];
_projectsMap = {};
landingPageStatus = PageStatus.loading;
// FIXME: loads and reschedules tasks each time list is updated
VikunjaGlobal.of(context)
.notifications
.scheduleDueNotifications(VikunjaGlobal.of(context).taskService);
return VikunjaGlobal.of(context)
bool showOnlyDueDateTasks = await VikunjaGlobal.of(context)
.settingsManager
.getLandingPageOnlyDueDateTasks()
.then((showOnlyDueDateTasks) {
VikunjaGlobalState global = VikunjaGlobal.of(context);
Map<String, dynamic>? frontend_settings =
global.currentUser?.settings?.frontend_settings;
int? filterId = 0;
if (frontend_settings != null) {
if (frontend_settings["filter_id_used_on_overview"] != null)
filterId = frontend_settings["filter_id_used_on_overview"];
}
if (filterId != null && filterId != 0) {
return global.taskService.getAllByProject(filterId, {
"sort_by": ["due_date", "id"],
"order_by": ["asc", "desc"],
}).then<Future<void>?>((response) =>
_handleTaskList(response?.body, showOnlyDueDateTasks));
}
.getLandingPageOnlyDueDateTasks();
return global.taskService
.getByOptions(TaskServiceOptions(newOptions: [
TaskServiceOption<TaskServiceOptionSortBy>(
"sort_by", ["due_date", "id"]),
TaskServiceOption<TaskServiceOptionSortBy>(
"order_by", ["asc", "desc"]),
TaskServiceOption<TaskServiceOptionFilterBy>("filter_by", "done"),
TaskServiceOption<TaskServiceOptionFilterValue>(
"filter_value", "false"),
TaskServiceOption<TaskServiceOptionFilterComparator>(
"filter_comparator", "equals"),
TaskServiceOption<TaskServiceOptionFilterConcat>(
"filter_concat", "and"),
], clearOther: true))
.then<Future<void>?>(
(taskList) => _handleTaskList(taskList, showOnlyDueDateTasks));
}); //.onError((error, stackTrace) {print("error");});
VikunjaGlobalState global = VikunjaGlobal.of(context);
List<Project>? projects = await global.projectService.getAll();
if (projects != null) {
projects.forEach((project) {
_projectsMap[project.id] = project;
});
}
Map<String, dynamic>? frontend_settings =
global.currentUser?.settings?.frontend_settings;
int? filterId = 0;
if (frontend_settings != null) {
if (frontend_settings["filter_id_used_on_overview"] != null)
filterId = frontend_settings["filter_id_used_on_overview"];
}
if (filterId != null && filterId != 0) {
var response = await global.taskService.getAllByProject(filterId, {
"sort_by": ["due_date", "id"],
"order_by": ["asc", "desc"],
});
await _handleTaskList(response?.body, showOnlyDueDateTasks);
return;
}
var taskList =
await global.taskService.getByOptions(TaskServiceOptions(newOptions: [
TaskServiceOption<TaskServiceOptionSortBy>("sort_by", ["due_date", "id"]),
TaskServiceOption<TaskServiceOptionSortBy>("order_by", ["asc", "desc"]),
TaskServiceOption<TaskServiceOptionFilterBy>("filter_by", "done"),
TaskServiceOption<TaskServiceOptionFilterValue>("filter_value", "false"),
TaskServiceOption<TaskServiceOptionFilterComparator>(
"filter_comparator", "equals"),
TaskServiceOption<TaskServiceOptionFilterConcat>("filter_concat", "and"),
], clearOther: true));
await _handleTaskList(taskList, showOnlyDueDateTasks);
}
Future<void> _handleTaskList(

View File

@ -65,7 +65,7 @@ class _ListPageState extends State<ListPage> {
Widget build(BuildContext context) {
taskState = Provider.of<ListProvider>(context);
//_kanban = KanbanClass(
// context, nullSetState, _onViewTapped, _addItemDialog, _list);
// context, nullSetState, _onViewTapped, _addItemDialog, _list);
Widget body;
@ -126,10 +126,8 @@ class _ListPageState extends State<ListPage> {
]);
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;
}
@ -241,8 +239,8 @@ class _ListPageState extends State<ListPage> {
TaskTile _buildLoadingTile(Task task) {
return TaskTile(
task: task,
loading: true, onEdit: () {},
loading: true,
onEdit: () {},
);
}
@ -255,13 +253,11 @@ class _ListPageState extends State<ListPage> {
_loadTasksForPage(1);
break;
case 1:
await _kanban
.loadBucketsForPage(1);
await _kanban.loadBucketsForPage(1);
// load all buckets to get length for RecordableListView
while (_currentPage < taskState.maxPages) {
_currentPage++;
await _kanban
.loadBucketsForPage(_currentPage);
await _kanban.loadBucketsForPage(_currentPage);
}
break;
default:
@ -271,12 +267,11 @@ class _ListPageState extends State<ListPage> {
}
Future<void> _loadTasksForPage(int page) {
return Provider.of<ListProvider>(context, listen: false)
.loadTasks(
context: context,
listId: _list.id,
page: page,
displayDoneTasks: displayDoneTasks);
return Provider.of<ListProvider>(context, listen: false).loadTasks(
context: context,
listId: _list.id,
page: page,
displayDoneTasks: displayDoneTasks);
}
Future<void> _addItemDialog(BuildContext context, [Bucket? bucket]) {