mirror of
https://github.com/go-vikunja/app
synced 2024-06-01 02:06:51 +00:00
Compare commits
22 Commits
0.1.4-beta
...
main
Author | SHA1 | Date | |
---|---|---|---|
85f79e4e5a | |||
d30a3cf5f2 | |||
4e78b5615e | |||
9961447788 | |||
828a57a642 | |||
|
d83114e9aa | ||
|
0e29b6620d | ||
|
1d538d6816 | ||
|
5ab6a59b05 | ||
|
056b2d72c9 | ||
43b9fe6d8f | |||
1ef9ed4e67 | |||
aa0f56231f | |||
092386db2e | |||
c2eb96c6e1 | |||
2fbd8ed27e | |||
5ee93a64be | |||
92c5ff5a44 | |||
bd1ada1b9c | |||
515be2e88f | |||
f91509502a | |||
13b7eb7368 |
6
.github/workflows/build-unsigned-debug.yml
vendored
6
.github/workflows/build-unsigned-debug.yml
vendored
|
@ -33,3 +33,9 @@ jobs:
|
|||
- name: Build Debug Build
|
||||
run: flutter build apk --debug --flavor core
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: app-core-debug.apk
|
||||
path: build/app/outputs/flutter-apk/app-core-debug.apk
|
||||
compression-level: 0
|
20
.github/workflows/flutter-format.yml
vendored
Normal file
20
.github/workflows/flutter-format.yml
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
name: Flutter Check Format
|
||||
|
||||
on:
|
||||
pull_request: {}
|
||||
push: {}
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v1
|
||||
with:
|
||||
channel: stable
|
||||
- name: Check Dart Format
|
||||
run: dart format --set-exit-if-changed .
|
26
.metadata
26
.metadata
|
@ -4,5 +4,27 @@
|
|||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: 3b309bda072a6b326e8aa4591a5836af600923ce
|
||||
channel: beta
|
||||
revision: "300451adae589accbece3490f4396f10bdf15e6e"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 300451adae589accbece3490f4396f10bdf15e6e
|
||||
base_revision: 300451adae589accbece3490f4396f10bdf15e6e
|
||||
- platform: web
|
||||
create_revision: 300451adae589accbece3490f4396f10bdf15e6e
|
||||
base_revision: 300451adae589accbece3490f4396f10bdf15e6e
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||
|
|
29
.vscode/launch.json
vendored
29
.vscode/launch.json
vendored
|
@ -1,14 +1,19 @@
|
|||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Flutter",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "debug",
|
||||
"args": [
|
||||
"--flavor",
|
||||
"main"
|
||||
],
|
||||
}
|
||||
]
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Flutter (Chromium)",
|
||||
"type": "dart",
|
||||
"request": "launch",
|
||||
"program": "lib/main.dart",
|
||||
"args": ["-d", "chrome", "--flavor", "main"],
|
||||
"deviceId": "chrome",
|
||||
},
|
||||
{
|
||||
"name": "Flutter",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "debug",
|
||||
"args": ["--flavor", "main"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ if (keystorePropertiesFile.exists()) {
|
|||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 33
|
||||
compileSdkVersion 34
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
|
@ -42,7 +42,7 @@ android {
|
|||
defaultConfig {
|
||||
applicationId "io.vikunja.app"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 33
|
||||
targetSdkVersion 34
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
|
|
|
@ -8,6 +8,9 @@
|
|||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
||||
calls FlutterMain.startInitialization(this); in its onCreate method.
|
||||
|
@ -18,7 +21,9 @@
|
|||
android:label="Vikunja"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:usesCleartextTraffic="true">
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
>
|
||||
<meta-data android:name="io.flutter.network-policy" android:resource="@xml/network_security_config"/>
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:launchMode="singleTop"
|
||||
|
@ -103,4 +108,4 @@
|
|||
</provider>
|
||||
</application>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
|
9
android/app/src/main/res/xml/network_security_config.xml
Normal file
9
android/app/src/main/res/xml/network_security_config.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<base-config cleartextTrafficPermitted="true">
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
<certificates src="user" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
</network-security-config>
|
|
@ -1,5 +1,5 @@
|
|||
buildscript {
|
||||
ext.kotlin_version = '1.7.20'
|
||||
ext.kotlin_version = '1.9.23'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
|
|
0
android/gradlew
vendored
Normal file → Executable file
0
android/gradlew
vendored
Normal file → Executable file
|
@ -12,15 +12,14 @@ class BucketAPIService extends APIService implements BucketService {
|
|||
return client
|
||||
.put('/projects/$projectId/buckets', body: bucket.toJSON())
|
||||
.then((response) {
|
||||
if (response == null) return null;
|
||||
return Bucket.fromJSON(response.body);
|
||||
});
|
||||
if (response == null) return null;
|
||||
return Bucket.fromJSON(response.body);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future delete(int projectId, int bucketId) {
|
||||
return client
|
||||
.delete('/projects/$projectId/buckets/$bucketId');
|
||||
return client.delete('/projects/$projectId/buckets/$bucketId');
|
||||
}
|
||||
|
||||
/* Not implemented in the Vikunja API
|
||||
|
@ -35,13 +34,13 @@ class BucketAPIService extends APIService implements BucketService {
|
|||
@override
|
||||
Future<Response?> getAllByList(int projectId,
|
||||
[Map<String, List<String>>? queryParameters]) {
|
||||
return client
|
||||
.get('/projects/$projectId/buckets', queryParameters)
|
||||
.then((response) => response != null ? new Response(
|
||||
convertList(response.body, (result) => Bucket.fromJSON(result)),
|
||||
response.statusCode,
|
||||
response.headers
|
||||
) : null);
|
||||
return client.get('/projects/$projectId/buckets', queryParameters).then(
|
||||
(response) => response != null
|
||||
? new Response(
|
||||
convertList(response.body, (result) => Bucket.fromJSON(result)),
|
||||
response.statusCode,
|
||||
response.headers)
|
||||
: null);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -51,10 +50,11 @@ class BucketAPIService extends APIService implements BucketService {
|
|||
@override
|
||||
Future<Bucket?> update(Bucket bucket) {
|
||||
return client
|
||||
.post('/projects/${bucket.projectId}/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);
|
||||
});
|
||||
if (response == null) return null;
|
||||
return Bucket.fromJSON(response.body);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:core';
|
||||
import 'dart:io';
|
||||
import 'package:cronet_http/cronet_http.dart' as cronet_http;
|
||||
import 'package:cupertino_http/cupertino_http.dart' as cupertino_http;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/io_client.dart' as io_client;
|
||||
import 'package:vikunja_app/api/response.dart';
|
||||
import 'package:vikunja_app/components/string_extension.dart';
|
||||
import 'package:vikunja_app/global.dart';
|
||||
|
||||
import '../main.dart';
|
||||
|
||||
|
||||
class Client {
|
||||
GlobalKey<ScaffoldMessengerState>? global_scaffold_key;
|
||||
final JsonDecoder _decoder = new JsonDecoder();
|
||||
|
@ -26,32 +27,55 @@ class Client {
|
|||
|
||||
String? post_body;
|
||||
|
||||
|
||||
|
||||
bool operator ==(dynamic otherClient) {
|
||||
@override
|
||||
bool operator ==(Object otherClient) {
|
||||
if (otherClient is! Client) return false;
|
||||
return otherClient._token == _token;
|
||||
}
|
||||
|
||||
Client(this.global_scaffold_key,
|
||||
{String? token, String? base, bool authenticated = false}) {
|
||||
configure(token: token, base: base, authenticated: authenticated);
|
||||
Client(
|
||||
this.global_scaffold_key, {
|
||||
String? token,
|
||||
String? base,
|
||||
bool authenticated = false,
|
||||
}) {
|
||||
configure(
|
||||
token: token,
|
||||
base: base,
|
||||
authenticated: authenticated,
|
||||
);
|
||||
}
|
||||
|
||||
void reload_ignore_certs(bool? val) {
|
||||
http.Client get httpClient {
|
||||
if (Platform.isAndroid) {
|
||||
final engine = cronet_http.CronetEngine.build(
|
||||
cacheMode: cronet_http.CacheMode.memory, cacheMaxSize: 1000000);
|
||||
return cronet_http.CronetClient.fromCronetEngine(engine);
|
||||
}
|
||||
if (Platform.isIOS || Platform.isMacOS) {
|
||||
final config =
|
||||
cupertino_http.URLSessionConfiguration.ephemeralSessionConfiguration()
|
||||
..cache =
|
||||
cupertino_http.URLCache.withCapacity(memoryCapacity: 1000000);
|
||||
return cupertino_http.CupertinoClient.fromSessionConfiguration(config);
|
||||
}
|
||||
return io_client.IOClient();
|
||||
}
|
||||
|
||||
void reloadIgnoreCerts(bool? val) {
|
||||
ignoreCertificates = val ?? false;
|
||||
HttpOverrides.global = new IgnoreCertHttpOverrides(ignoreCertificates);
|
||||
if(global_scaffold_key == null || global_scaffold_key!.currentContext == null) return;
|
||||
VikunjaGlobal
|
||||
.of(global_scaffold_key!.currentContext!)
|
||||
if (global_scaffold_key == null ||
|
||||
global_scaffold_key!.currentContext == null) return;
|
||||
VikunjaGlobal.of(global_scaffold_key!.currentContext!)
|
||||
.settingsManager
|
||||
.setIgnoreCertificates(ignoreCertificates);
|
||||
}
|
||||
|
||||
get _headers =>
|
||||
{
|
||||
get _headers => {
|
||||
'Authorization': _token != '' ? 'Bearer $_token' : '',
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'Vikunja Mobile App'
|
||||
'User-Agent': 'Vikunja Mobile App',
|
||||
};
|
||||
|
||||
get headers => _headers;
|
||||
|
@ -59,23 +83,21 @@ class Client {
|
|||
@override
|
||||
int get hashCode => _token.hashCode;
|
||||
|
||||
void configure({String? token, String? base, bool? authenticated}) {
|
||||
if (token != null)
|
||||
_token = token;
|
||||
void configure({
|
||||
String? token,
|
||||
String? base,
|
||||
bool? authenticated,
|
||||
}) {
|
||||
if (token != null) _token = token;
|
||||
if (base != null) {
|
||||
base = base.replaceAll(" ", "");
|
||||
if(base.endsWith("/"))
|
||||
base = base.substring(0,base.length-1);
|
||||
if (base.endsWith("/")) base = base.substring(0, base.length - 1);
|
||||
_base = base.endsWith('/api/v1') ? base : '$base/api/v1';
|
||||
}
|
||||
if (authenticated != null)
|
||||
this.authenticated = authenticated;
|
||||
|
||||
if (authenticated != null) this.authenticated = authenticated;
|
||||
}
|
||||
|
||||
|
||||
void reset() {
|
||||
_token = _base = '';
|
||||
authenticated = false;
|
||||
}
|
||||
|
||||
|
@ -85,54 +107,61 @@ class Client {
|
|||
// why are we doing it like this? because Uri doesnt have setters. wtf.
|
||||
|
||||
uri = Uri(
|
||||
scheme: uri.scheme,
|
||||
scheme: uri.scheme,
|
||||
userInfo: uri.userInfo,
|
||||
host: uri.host,
|
||||
port: uri.port,
|
||||
path: uri.path,
|
||||
//queryParameters: {...uri.queryParameters, ...?queryParameters},
|
||||
queryParameters: queryParameters,
|
||||
fragment: uri.fragment
|
||||
);
|
||||
fragment: uri.fragment);
|
||||
|
||||
return http.get(uri, headers: _headers)
|
||||
.then(_handleResponse).onError((error, stackTrace) =>
|
||||
_handleError(error, stackTrace));
|
||||
return httpClient
|
||||
.get(uri, headers: _headers)
|
||||
.then(_handleResponse)
|
||||
.onError((error, stackTrace) => _handleError(error, stackTrace));
|
||||
}
|
||||
|
||||
Future<Response?> delete(String url) {
|
||||
return http.delete(
|
||||
'${this.base}$url'.toUri()!,
|
||||
headers: _headers,
|
||||
).then(_handleResponse).onError((error, stackTrace) =>
|
||||
_handleError(error, stackTrace));
|
||||
return httpClient
|
||||
.delete(
|
||||
'${this.base}$url'.toUri()!,
|
||||
headers: _headers,
|
||||
)
|
||||
.then(_handleResponse)
|
||||
.onError((error, stackTrace) => _handleError(error, stackTrace));
|
||||
}
|
||||
|
||||
Future<Response?> post(String url, {dynamic body}) {
|
||||
return http.post(
|
||||
'${this.base}$url'.toUri()!,
|
||||
headers: _headers,
|
||||
body: _encoder.convert(body),
|
||||
)
|
||||
.then(_handleResponse).onError((error, stackTrace) =>
|
||||
_handleError(error, stackTrace));
|
||||
return httpClient
|
||||
.post(
|
||||
'${this.base}$url'.toUri()!,
|
||||
headers: _headers,
|
||||
body: _encoder.convert(body),
|
||||
)
|
||||
.then(_handleResponse)
|
||||
.onError((error, stackTrace) => _handleError(error, stackTrace));
|
||||
}
|
||||
|
||||
Future<Response?> put(String url, {dynamic body}) {
|
||||
return http.put(
|
||||
'${this.base}$url'.toUri()!,
|
||||
headers: _headers,
|
||||
body: _encoder.convert(body),
|
||||
)
|
||||
.then(_handleResponse).onError((error, stackTrace) =>
|
||||
_handleError(error, stackTrace));
|
||||
return httpClient
|
||||
.put(
|
||||
'${this.base}$url'.toUri()!,
|
||||
headers: _headers,
|
||||
body: _encoder.convert(body),
|
||||
)
|
||||
.then(_handleResponse)
|
||||
.onError((error, stackTrace) => _handleError(error, stackTrace));
|
||||
}
|
||||
|
||||
Response? _handleError(Object? e, StackTrace? st) {
|
||||
if(global_scaffold_key == null) return null;
|
||||
if (global_scaffold_key == null) return null;
|
||||
SnackBar snackBar = SnackBar(
|
||||
content: Text("Error on request: " + e.toString()),
|
||||
action: SnackBarAction(label: "Clear", onPressed: () => global_scaffold_key!.currentState?.clearSnackBars()),);
|
||||
action: SnackBarAction(
|
||||
label: "Clear",
|
||||
onPressed: () => global_scaffold_key!.currentState?.clearSnackBars()),
|
||||
);
|
||||
global_scaffold_key!.currentState?.showSnackBar(snackBar);
|
||||
return null;
|
||||
}
|
||||
|
@ -145,39 +174,38 @@ class Client {
|
|||
return map;
|
||||
}
|
||||
|
||||
|
||||
Error? _handleResponseErrors(http.Response response) {
|
||||
if (response.statusCode < 200 ||
|
||||
response.statusCode >= 400) {
|
||||
if (response.statusCode < 200 || response.statusCode >= 400) {
|
||||
Map<String, dynamic> error;
|
||||
error = _decoder.convert(response.body);
|
||||
|
||||
|
||||
final SnackBar snackBar = SnackBar(
|
||||
content: Text(
|
||||
"Error code " + response.statusCode.toString() + " received."),
|
||||
action: globalNavigatorKey.currentContext == null ? null : SnackBarAction(
|
||||
label: ("Details"),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: globalNavigatorKey.currentContext!,
|
||||
builder: (BuildContext context) =>
|
||||
AlertDialog(
|
||||
title: Text("Error ${response.statusCode}"),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text("Message: ${error["message"]}", textAlign: TextAlign.start,),
|
||||
Text("Url: ${response.request!.url.toString()}"),
|
||||
],
|
||||
)
|
||||
)
|
||||
);
|
||||
},
|
||||
),
|
||||
content:
|
||||
Text("Error code " + response.statusCode.toString() + " received."),
|
||||
action: globalNavigatorKey.currentContext == null
|
||||
? null
|
||||
: SnackBarAction(
|
||||
label: ("Details"),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: globalNavigatorKey.currentContext!,
|
||||
builder: (BuildContext context) => AlertDialog(
|
||||
title: Text("Error ${response.statusCode}"),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
"Message: ${error["message"]}",
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
Text("Url: ${response.request!.url.toString()}"),
|
||||
],
|
||||
)));
|
||||
},
|
||||
),
|
||||
);
|
||||
if(global_scaffold_key != null && showSnackBar)
|
||||
if (global_scaffold_key != null && showSnackBar)
|
||||
global_scaffold_key!.currentState?.showSnackBar(snackBar);
|
||||
else
|
||||
print("error on request: ${error["message"]}");
|
||||
|
@ -186,7 +214,7 @@ class Client {
|
|||
}
|
||||
|
||||
Response? _handleResponse(http.Response response) {
|
||||
Error? error = _handleResponseErrors(response);
|
||||
_handleResponseErrors(response);
|
||||
return Response(
|
||||
_decoder.convert(response.body), response.statusCode, response.headers);
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ class LabelTaskAPIService extends APIService implements LabelTaskService {
|
|||
.then((response) {
|
||||
if (response == null) return null;
|
||||
return Label.fromJson(response.body);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -22,9 +22,9 @@ class LabelTaskAPIService extends APIService implements LabelTaskService {
|
|||
return client
|
||||
.delete('/tasks/${lt.task!.id}/labels/${lt.label.id}')
|
||||
.then((response) {
|
||||
if (response == null) return null;
|
||||
return Label.fromJson(response.body);
|
||||
});
|
||||
if (response == null) return null;
|
||||
return Label.fromJson(response.body);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -32,10 +32,9 @@ class LabelTaskAPIService extends APIService implements LabelTaskService {
|
|||
String? params =
|
||||
query == null ? null : '?s=' + Uri.encodeQueryComponent(query);
|
||||
|
||||
return client.get('/tasks/${lt.task!.id}/labels$params').then(
|
||||
(label) {
|
||||
if (label == null) return null;
|
||||
return convertList(label, (result) => Label.fromJson(result));
|
||||
});
|
||||
return client.get('/tasks/${lt.task!.id}/labels$params').then((label) {
|
||||
if (label == null) return null;
|
||||
return convertList(label, (result) => Label.fromJson(result));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,15 +11,14 @@ class LabelTaskBulkAPIService extends APIService
|
|||
|
||||
@override
|
||||
Future<List<Label>?> update(Task task, List<Label>? labels) {
|
||||
if(labels == null)
|
||||
labels = [];
|
||||
if (labels == null) labels = [];
|
||||
return client
|
||||
.post('/tasks/${task.id}/labels/bulk',
|
||||
body: LabelTaskBulk(labels: labels).toJSON())
|
||||
.then((response) {
|
||||
if (response == null) return null;
|
||||
return convertList(
|
||||
response.body['labels'], (result) => Label.fromJson(result));
|
||||
});
|
||||
if (response == null) return null;
|
||||
return convertList(
|
||||
response.body['labels'], (result) => Label.fromJson(result));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,52 +8,43 @@ class LabelAPIService extends APIService implements LabelService {
|
|||
|
||||
@override
|
||||
Future<Label?> create(Label label) {
|
||||
return client
|
||||
.put('/labels', body: label.toJSON())
|
||||
.then((response) {
|
||||
if (response == null) return null;
|
||||
return Label.fromJson(response.body);
|
||||
});
|
||||
return client.put('/labels', body: label.toJSON()).then((response) {
|
||||
if (response == null) return null;
|
||||
return Label.fromJson(response.body);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Label?> delete(Label label) {
|
||||
return client
|
||||
.delete('/labels/${label.id}')
|
||||
.then((response) {
|
||||
if (response == null) return null;
|
||||
return Label.fromJson(response.body);
|
||||
});
|
||||
return client.delete('/labels/${label.id}').then((response) {
|
||||
if (response == null) return null;
|
||||
return Label.fromJson(response.body);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Label?> get(int labelID) {
|
||||
return client
|
||||
.get('/labels/$labelID')
|
||||
.then((response) {
|
||||
if (response == null) return null;
|
||||
return Label.fromJson(response.body);
|
||||
});
|
||||
return client.get('/labels/$labelID').then((response) {
|
||||
if (response == null) return null;
|
||||
return Label.fromJson(response.body);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Label>?> getAll({String? query}) {
|
||||
String params =
|
||||
query == null ? '' : '?s=' + Uri.encodeQueryComponent(query);
|
||||
return client.get('/labels$params').then(
|
||||
(response) {
|
||||
if (response == null) return null;
|
||||
return convertList(response.body, (result) => Label.fromJson(result));
|
||||
});
|
||||
return client.get('/labels$params').then((response) {
|
||||
if (response == null) return null;
|
||||
return convertList(response.body, (result) => Label.fromJson(result));
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Label?> update(Label label) {
|
||||
return client
|
||||
.post('/labels/${label.id}', body: label)
|
||||
.then((response) {
|
||||
if (response == null) return null;
|
||||
return Label.fromJson(response.body);
|
||||
});
|
||||
return client.post('/labels/${label.id}', body: label).then((response) {
|
||||
if (response == null) return null;
|
||||
return Label.fromJson(response.body);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,111 +0,0 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:vikunja_app/api/client.dart';
|
||||
import 'package:vikunja_app/api/service.dart';
|
||||
import 'package:vikunja_app/models/list.dart';
|
||||
import 'package:vikunja_app/service/services.dart';
|
||||
|
||||
class ListAPIService extends APIService implements ListService {
|
||||
FlutterSecureStorage _storage;
|
||||
ListAPIService(Client client, FlutterSecureStorage storage) : _storage = storage, super(client);
|
||||
|
||||
@override
|
||||
Future<TaskList?> create(namespaceId, TaskList tl) {
|
||||
tl.namespaceId = namespaceId;
|
||||
return client
|
||||
.put('/namespaces/$namespaceId/lists', body: tl.toJSON())
|
||||
.then((response) {
|
||||
if (response == null) return null;
|
||||
return TaskList.fromJson(response.body);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future delete(int listId) {
|
||||
return client.delete('/lists/$listId').then((_) {});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<TaskList?> get(int listId) {
|
||||
return client.get('/lists/$listId').then((response) {
|
||||
if (response == null) return null;
|
||||
final map = response.body;
|
||||
if (map.containsKey('id')) {
|
||||
return client
|
||||
.get("/lists/$listId/tasks")
|
||||
.then((tasks) {
|
||||
map['tasks'] = tasks?.body;
|
||||
return TaskList.fromJson(map);
|
||||
});
|
||||
}
|
||||
return TaskList.fromJson(map);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<TaskList>?> getAll() {
|
||||
return client.get('/lists').then(
|
||||
(list) {
|
||||
if (list == null || list.statusCode != 200) return null;
|
||||
if (list.body.toString().isEmpty)
|
||||
return Future.value([]);
|
||||
print(list.statusCode);
|
||||
return convertList(list.body, (result) => TaskList.fromJson(result));});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<TaskList>?> getByNamespace(int namespaceId) {
|
||||
// TODO there needs to be a better way for this. /namespaces/-2/lists should
|
||||
// return favorite lists
|
||||
if(namespaceId == -2) {
|
||||
// Favourites.
|
||||
return getAll().then((value) {
|
||||
if (value == null) return null;
|
||||
value.removeWhere((element) => !element.isFavorite); return value;});
|
||||
}
|
||||
return client.get('/namespaces/$namespaceId/lists').then(
|
||||
(list) {
|
||||
if (list == null || list.statusCode != 200) return null;
|
||||
return convertList(list.body, (result) => TaskList.fromJson(result));
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<TaskList?> update(TaskList tl) {
|
||||
return client
|
||||
.post('/lists/${tl.id}', body: tl.toJSON())
|
||||
.then((response) {
|
||||
if (response == null) return null;
|
||||
return TaskList.fromJson(response.body);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> 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<String?> getDefaultList() {
|
||||
return _storage.read(key: "default_list_id");
|
||||
}
|
||||
|
||||
@override
|
||||
void setDefaultList(int? listId) {
|
||||
_storage.write(key: "default_list_id", value: listId.toString());
|
||||
}
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'package:vikunja_app/api/client.dart';
|
||||
import 'package:vikunja_app/api/service.dart';
|
||||
import 'package:vikunja_app/models/namespace.dart';
|
||||
import 'package:vikunja_app/service/services.dart';
|
||||
|
||||
class NamespaceAPIService extends APIService implements NamespaceService {
|
||||
NamespaceAPIService(Client client) : super(client);
|
||||
|
||||
@override
|
||||
Future<Namespace?> create(Namespace ns) {
|
||||
return client.put('/namespaces', body: ns.toJSON()).then((response) {
|
||||
if (response == null) return null;
|
||||
return Namespace.fromJson(response.body);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future delete(int namespaceId) {
|
||||
return client.delete('/namespaces/$namespaceId');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Namespace?> get(int namespaceId) {
|
||||
return client.get('/namespaces/$namespaceId').then((response) {
|
||||
if (response == null) return null;
|
||||
return Namespace.fromJson(response.body);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Namespace>?> getAll() {
|
||||
return client.get('/namespaces').then((response) {
|
||||
if (response == null) return null;
|
||||
return convertList(response.body, (result) => Namespace.fromJson(result));
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Namespace?> update(Namespace ns) {
|
||||
return client
|
||||
.post('/namespaces/${ns.id}', body: ns.toJSON())
|
||||
.then((response) {
|
||||
if (response == null) return null;
|
||||
return Namespace.fromJson(response.body);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -6,7 +6,9 @@ import 'package:vikunja_app/service/services.dart';
|
|||
class ProjectAPIService extends APIService implements ProjectService {
|
||||
FlutterSecureStorage _storage;
|
||||
|
||||
ProjectAPIService(client, storage) : _storage = storage, super(client);
|
||||
ProjectAPIService(client, storage)
|
||||
: _storage = storage,
|
||||
super(client);
|
||||
|
||||
@override
|
||||
Future<Project?> create(Project p) {
|
||||
|
@ -47,9 +49,7 @@ class ProjectAPIService extends APIService implements ProjectService {
|
|||
|
||||
@override
|
||||
Future<Project?> update(Project p) {
|
||||
return client
|
||||
.post('/projects/${p.id}', body: p.toJSON())
|
||||
.then((response) {
|
||||
return client.post('/projects/${p.id}', body: p.toJSON()).then((response) {
|
||||
if (response == null) return null;
|
||||
return Project.fromJson(response.body);
|
||||
});
|
||||
|
@ -57,9 +57,8 @@ class ProjectAPIService extends APIService implements ProjectService {
|
|||
|
||||
@override
|
||||
Future<String> getDisplayDoneTasks(int listId) {
|
||||
return _storage.read(key: "display_done_tasks_list_$listId").then((value)
|
||||
{
|
||||
if(value == null) {
|
||||
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");
|
||||
|
@ -82,5 +81,4 @@ class ProjectAPIService extends APIService implements ProjectService {
|
|||
void setDefaultList(int? listId) {
|
||||
_storage.write(key: "default_list_id", value: listId.toString());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,8 +10,7 @@ class ServerAPIService extends APIService implements ServerService {
|
|||
@override
|
||||
Future<Server?> getInfo() {
|
||||
return client.get('/info').then((value) {
|
||||
if(value == null)
|
||||
return null;
|
||||
if (value == null) return null;
|
||||
return Server.fromJson(value.body);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -15,25 +15,22 @@ class TaskAPIService extends APIService implements TaskService {
|
|||
return client
|
||||
.put('/projects/$projectId/tasks', body: task.toJSON())
|
||||
.then((response) {
|
||||
if (response == null) return null;
|
||||
return Task.fromJson(response.body);
|
||||
});
|
||||
if (response == null) return null;
|
||||
return Task.fromJson(response.body);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Task?> get(int listId) {
|
||||
return client
|
||||
.get('/project/$listId/tasks')
|
||||
.then((response) {
|
||||
if (response == null) return null;
|
||||
return Task.fromJson(response.body);
|
||||
});
|
||||
return client.get('/project/$listId/tasks').then((response) {
|
||||
if (response == null) return null;
|
||||
return Task.fromJson(response.body);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future delete(int taskId) {
|
||||
return client
|
||||
.delete('/tasks/$taskId');
|
||||
return client.delete('/tasks/$taskId');
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -41,36 +38,38 @@ class TaskAPIService extends APIService implements TaskService {
|
|||
return client
|
||||
.post('/tasks/${task.id}', body: task.toJSON())
|
||||
.then((response) {
|
||||
if (response == null) return null;
|
||||
return Task.fromJson(response.body);
|
||||
});
|
||||
if (response == null) return null;
|
||||
return Task.fromJson(response.body);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Task>?> getAll() {
|
||||
return client
|
||||
.get('/tasks/all')
|
||||
.then((response) {
|
||||
int page_n = 0;
|
||||
if (response == null) return null;
|
||||
if (response.headers["x-pagination-total-pages"] != null) {
|
||||
page_n = int.parse(response.headers["x-pagination-total-pages"]!);
|
||||
} else {
|
||||
return Future.value(response.body);
|
||||
}
|
||||
return client.get('/tasks/all').then((response) {
|
||||
int page_n = 0;
|
||||
if (response == null) return null;
|
||||
if (response.headers["x-pagination-total-pages"] != null) {
|
||||
page_n = int.parse(response.headers["x-pagination-total-pages"]!);
|
||||
} else {
|
||||
return Future.value(response.body);
|
||||
}
|
||||
|
||||
List<Future<void>> futureList = [];
|
||||
List<Task> taskList = [];
|
||||
List<Future<void>> futureList = [];
|
||||
List<Task> taskList = [];
|
||||
|
||||
for(int i = 0; i < page_n; i++) {
|
||||
Map<String, List<String>> paramMap = {
|
||||
"page": [i.toString()]
|
||||
};
|
||||
futureList.add(client.get('/tasks/all', paramMap).then((pageResponse) {convertList(pageResponse?.body, (result) {taskList.add(Task.fromJson(result));});}));
|
||||
}
|
||||
return Future.wait(futureList).then((value) {
|
||||
return taskList;
|
||||
for (int i = 0; i < page_n; i++) {
|
||||
Map<String, List<String>> paramMap = {
|
||||
"page": [i.toString()]
|
||||
};
|
||||
futureList.add(client.get('/tasks/all', paramMap).then((pageResponse) {
|
||||
convertList(pageResponse?.body, (result) {
|
||||
taskList.add(Task.fromJson(result));
|
||||
});
|
||||
}));
|
||||
}
|
||||
return Future.wait(futureList).then((value) {
|
||||
return taskList;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -78,14 +77,15 @@ class TaskAPIService extends APIService implements TaskService {
|
|||
Future<Response?> getAllByProject(int projectId,
|
||||
[Map<String, List<String>>? queryParameters]) {
|
||||
return client
|
||||
.get('/projects/$projectId/tasks', queryParameters).then(
|
||||
(response) {
|
||||
return response != null ?
|
||||
new Response(
|
||||
convertList(response.body, (result) => Task.fromJson(result)),
|
||||
response.statusCode,
|
||||
response.headers) : null;
|
||||
});
|
||||
.get('/projects/$projectId/tasks', queryParameters)
|
||||
.then((response) {
|
||||
return response != null
|
||||
? new Response(
|
||||
convertList(response.body, (result) => Task.fromJson(result)),
|
||||
response.statusCode,
|
||||
response.headers)
|
||||
: null;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -94,16 +94,13 @@ class TaskAPIService extends APIService implements TaskService {
|
|||
//optionString = "?sort_by[]=due_date&sort_by[]=id&order_by[]=asc&order_by[]=desc&filter_by[]=done&filter_value[]=false&filter_comparator[]=equals&filter_concat=and&filter_include_nulls=false&page=1";
|
||||
//print(optionString);
|
||||
|
||||
return client
|
||||
.get('/tasks/all', optionsMap)
|
||||
.then((response) {
|
||||
if (response == null) return null;
|
||||
return convertList(response.body, (result) => Task.fromJson(result));
|
||||
return client.get('/tasks/all', optionsMap).then((response) {
|
||||
if (response == null) return null;
|
||||
return convertList(response.body, (result) => Task.fromJson(result));
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
// TODO: implement maxPages
|
||||
int get maxPages => maxPages;
|
||||
|
||||
}
|
||||
|
|
|
@ -9,21 +9,23 @@ class UserAPIService extends APIService implements UserService {
|
|||
UserAPIService(Client client) : super(client);
|
||||
|
||||
@override
|
||||
Future<UserTokenPair> login(String username, password, {bool rememberMe = false, String? totp}) async {
|
||||
Future<UserTokenPair> login(String username, password,
|
||||
{bool rememberMe = false, String? totp}) async {
|
||||
var body = {
|
||||
'long_token': rememberMe,
|
||||
'password': password,
|
||||
'username': username,
|
||||
};
|
||||
if(totp != null) {
|
||||
if (totp != null) {
|
||||
body['totp_passcode'] = totp;
|
||||
}
|
||||
}
|
||||
var response = await client.post('/login', body: body);
|
||||
var token = response?.body["token"];
|
||||
if(token == null || response == null || response.error != null)
|
||||
if (token == null || response == null || response.error != null)
|
||||
return Future.value(UserTokenPair(null, null,
|
||||
error: response != null ? response.body["code"] : 0,
|
||||
errorString: response != null ? response.body["message"] : "Login error"));
|
||||
errorString:
|
||||
response != null ? response.body["message"] : "Login error"));
|
||||
client.configure(token: token);
|
||||
return UserAPIService(client)
|
||||
.getCurrentUser()
|
||||
|
@ -46,10 +48,18 @@ class UserAPIService extends APIService implements UserService {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<UserSettings?> setCurrentUserSettings(UserSettings userSettings) async {
|
||||
return client.post('/user/settings/general', body: userSettings.toJson()).then((response) {
|
||||
if(response == null) return null;
|
||||
Future<UserSettings?> setCurrentUserSettings(
|
||||
UserSettings userSettings) async {
|
||||
return client
|
||||
.post('/user/settings/general', body: userSettings.toJson())
|
||||
.then((response) {
|
||||
if (response == null) return null;
|
||||
return userSettings;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> getToken() {
|
||||
return client.post('/user/token').then((value) => value?.body["token"]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import 'dart:convert';
|
|||
import 'package:http/http.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
|
||||
class VersionChecker {
|
||||
GlobalKey<ScaffoldMessengerState> snackbarKey;
|
||||
VersionChecker(this.snackbarKey);
|
||||
|
@ -47,7 +46,8 @@ class VersionChecker {
|
|||
content: Text("New version available: $latest"),
|
||||
action: SnackBarAction(
|
||||
label: "View on Github",
|
||||
onPressed: () => launchUrl(Uri.parse(repo), mode: LaunchMode.externalApplication)),
|
||||
onPressed: () => launchUrl(Uri.parse(repo),
|
||||
mode: LaunchMode.externalApplication)),
|
||||
);
|
||||
snackbarKey.currentState?.showSnackBar(snackBar);
|
||||
}
|
||||
|
|
|
@ -4,7 +4,8 @@ import 'package:vikunja_app/global.dart';
|
|||
import 'dart:developer';
|
||||
import '../models/task.dart';
|
||||
|
||||
enum NewTaskDue {day,week, month, custom}
|
||||
enum NewTaskDue { day, week, month, custom }
|
||||
|
||||
// TODO: add to enum above
|
||||
Map<NewTaskDue, Duration> newTaskDueToDuration = {
|
||||
NewTaskDue.day: Duration(days: 1),
|
||||
|
@ -16,11 +17,11 @@ class AddDialog extends StatefulWidget {
|
|||
final ValueChanged<String>? onAdd;
|
||||
final void Function(String title, DateTime? dueDate)? onAddTask;
|
||||
final InputDecoration? decoration;
|
||||
const AddDialog({Key? key, this.onAdd, this.decoration, this.onAddTask}) : super(key: key);
|
||||
const AddDialog({Key? key, this.onAdd, this.decoration, this.onAddTask})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => AddDialogState();
|
||||
|
||||
}
|
||||
|
||||
class AddDialogState extends State<AddDialog> {
|
||||
|
@ -30,13 +31,11 @@ class AddDialogState extends State<AddDialog> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if(newTaskDue != NewTaskDue.custom)
|
||||
if (newTaskDue != NewTaskDue.custom)
|
||||
customDueDate = DateTime.now().add(newTaskDueToDuration[newTaskDue]!);
|
||||
return new AlertDialog(
|
||||
contentPadding: const EdgeInsets.all(16.0),
|
||||
content: new Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
content: new Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Row(children: <Widget>[
|
||||
Expanded(
|
||||
child: new TextField(
|
||||
|
@ -46,14 +45,24 @@ class AddDialogState extends State<AddDialog> {
|
|||
),
|
||||
),
|
||||
]),
|
||||
widget.onAddTask != null ? taskDueList("1 Day", NewTaskDue.day) : new Container(),
|
||||
widget.onAddTask != null ? taskDueList("1 Week", NewTaskDue.week) : new Container(),
|
||||
widget.onAddTask != null ? taskDueList("1 Month", NewTaskDue.month) : new Container(),
|
||||
widget.onAddTask != null ? VikunjaDateTimePicker(
|
||||
label: "Enter exact time",
|
||||
onChanged: (value) {setState(() => newTaskDue = NewTaskDue.custom); customDueDate = value;},
|
||||
|
||||
) : new Container(),
|
||||
widget.onAddTask != null
|
||||
? taskDueList("1 Day", NewTaskDue.day)
|
||||
: new Container(),
|
||||
widget.onAddTask != null
|
||||
? taskDueList("1 Week", NewTaskDue.week)
|
||||
: new Container(),
|
||||
widget.onAddTask != null
|
||||
? taskDueList("1 Month", NewTaskDue.month)
|
||||
: new Container(),
|
||||
widget.onAddTask != null
|
||||
? VikunjaDateTimePicker(
|
||||
label: "Enter exact time",
|
||||
onChanged: (value) {
|
||||
setState(() => newTaskDue = NewTaskDue.custom);
|
||||
customDueDate = value;
|
||||
},
|
||||
)
|
||||
: new Container(),
|
||||
//],)
|
||||
]),
|
||||
actions: <Widget>[
|
||||
|
@ -66,7 +75,7 @@ class AddDialogState extends State<AddDialog> {
|
|||
onPressed: () {
|
||||
if (widget.onAdd != null && textController.text.isNotEmpty)
|
||||
widget.onAdd!(textController.text);
|
||||
if(widget.onAddTask != null && textController.text.isNotEmpty) {
|
||||
if (widget.onAddTask != null && textController.text.isNotEmpty) {
|
||||
widget.onAddTask!(textController.text, customDueDate);
|
||||
}
|
||||
Navigator.pop(context);
|
||||
|
@ -78,9 +87,15 @@ class AddDialogState extends State<AddDialog> {
|
|||
|
||||
Widget taskDueList(String name, NewTaskDue thisNewTaskDue) {
|
||||
return Row(children: [
|
||||
Checkbox(value: newTaskDue == thisNewTaskDue, onChanged: (value) {
|
||||
newTaskDue = thisNewTaskDue;
|
||||
setState(() => customDueDate = DateTime.now().add(newTaskDueToDuration[thisNewTaskDue]!));}, shape: CircleBorder(),),
|
||||
Checkbox(
|
||||
value: newTaskDue == thisNewTaskDue,
|
||||
onChanged: (value) {
|
||||
newTaskDue = thisNewTaskDue;
|
||||
setState(() => customDueDate =
|
||||
DateTime.now().add(newTaskDueToDuration[thisNewTaskDue]!));
|
||||
},
|
||||
shape: CircleBorder(),
|
||||
),
|
||||
Text(name),
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -36,13 +36,15 @@ class _BucketLimitDialogState extends State<BucketLimitDialog> {
|
|||
inputFormatters: <TextInputFormatter>[
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
],
|
||||
onSubmitted: (text) => Navigator.of(context).pop(int.parse(text)),
|
||||
onSubmitted: (text) =>
|
||||
Navigator.of(context).pop(int.parse(text)),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
onPressed: () => _controller.text = '${int.parse(_controller.text) + 1}',
|
||||
onPressed: () =>
|
||||
_controller.text = '${int.parse(_controller.text) + 1}',
|
||||
icon: Icon(Icons.expand_less),
|
||||
),
|
||||
IconButton(
|
||||
|
@ -68,7 +70,8 @@ class _BucketLimitDialogState extends State<BucketLimitDialog> {
|
|||
child: Text('Remove Limit'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(int.parse(_controller.text)),
|
||||
onPressed: () =>
|
||||
Navigator.of(context).pop(int.parse(_controller.text)),
|
||||
child: Text('Done'),
|
||||
)
|
||||
],
|
||||
|
|
|
@ -5,13 +5,13 @@ import 'package:flutter/scheduler.dart';
|
|||
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/pages/project/task_edit.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}
|
||||
enum DropLocation { above, below, none }
|
||||
|
||||
class TaskData {
|
||||
final Task task;
|
||||
|
@ -37,7 +37,8 @@ class BucketTaskCard extends StatefulWidget {
|
|||
State<BucketTaskCard> createState() => _BucketTaskCardState();
|
||||
}
|
||||
|
||||
class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAliveClientMixin {
|
||||
class _BucketTaskCardState extends State<BucketTaskCard>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
Size? _cardSize;
|
||||
bool _dragging = false;
|
||||
DropLocation _dropLocation = DropLocation.none;
|
||||
|
@ -49,7 +50,8 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
|
|||
if (_cardSize == null) _updateCardSize(context);
|
||||
|
||||
final taskState = Provider.of<ProjectProvider>(context);
|
||||
final bucket = taskState.buckets[taskState.buckets.indexWhere((b) => b.id == widget.task.bucketId)];
|
||||
final bucket = taskState.buckets[
|
||||
taskState.buckets.indexWhere((b) => b.id == widget.task.bucketId)];
|
||||
// default chip height: 32
|
||||
const double chipHeight = 28;
|
||||
const chipConstraints = BoxConstraints(maxHeight: chipHeight);
|
||||
|
@ -59,7 +61,8 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
|
|||
children: <Widget>[
|
||||
Text(
|
||||
widget.task.identifier.isNotEmpty
|
||||
? '#${widget.task.identifier}' : '${widget.task.id}',
|
||||
? '#${widget.task.identifier}'
|
||||
: '${widget.task.id}',
|
||||
style: (theme.textTheme.subtitle2 ?? TextStyle()).copyWith(
|
||||
color: Colors.grey,
|
||||
),
|
||||
|
@ -67,21 +70,25 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
|
|||
],
|
||||
);
|
||||
if (widget.task.done) {
|
||||
identifierRow.children.insert(0, Container(
|
||||
constraints: chipConstraints,
|
||||
padding: EdgeInsets.only(right: 4),
|
||||
child: FittedBox(
|
||||
child: Chip(
|
||||
label: Text('Done'),
|
||||
labelStyle: (theme.textTheme.labelLarge ?? TextStyle()).copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.brightness == Brightness.dark
|
||||
? Colors.black : Colors.white,
|
||||
identifierRow.children.insert(
|
||||
0,
|
||||
Container(
|
||||
constraints: chipConstraints,
|
||||
padding: EdgeInsets.only(right: 4),
|
||||
child: FittedBox(
|
||||
child: Chip(
|
||||
label: Text('Done'),
|
||||
labelStyle:
|
||||
(theme.textTheme.labelLarge ?? TextStyle()).copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.brightness == Brightness.dark
|
||||
? Colors.black
|
||||
: Colors.white,
|
||||
),
|
||||
backgroundColor: vGreen,
|
||||
),
|
||||
),
|
||||
backgroundColor: vGreen,
|
||||
),
|
||||
),
|
||||
));
|
||||
));
|
||||
}
|
||||
|
||||
final titleRow = Row(
|
||||
|
@ -89,9 +96,11 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
|
|||
Expanded(
|
||||
child: Text(
|
||||
widget.task.title,
|
||||
style: (theme.textTheme.titleMedium ?? TextStyle(fontSize: 16)).copyWith(
|
||||
style: (theme.textTheme.titleMedium ?? TextStyle(fontSize: 16))
|
||||
.copyWith(
|
||||
color: theme.brightness == Brightness.dark
|
||||
? Colors.white : Colors.black,
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -145,10 +154,10 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
|
|||
backgroundColor: Colors.grey,
|
||||
),
|
||||
),
|
||||
label: Text(
|
||||
(checkboxStatistics.checked == checkboxStatistics.total ? '' : '${checkboxStatistics.checked} of ')
|
||||
+ '${checkboxStatistics.total} tasks'
|
||||
),
|
||||
label: Text((checkboxStatistics.checked == checkboxStatistics.total
|
||||
? ''
|
||||
: '${checkboxStatistics.checked} of ') +
|
||||
'${checkboxStatistics.total} tasks'),
|
||||
));
|
||||
}
|
||||
if (widget.task.attachments.isNotEmpty) {
|
||||
|
@ -185,7 +194,8 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
|
|||
child: identifierRow,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 4, bottom: labelRow.children.isNotEmpty ? 8 : 0),
|
||||
padding: EdgeInsets.only(
|
||||
top: 4, bottom: labelRow.children.isNotEmpty ? 8 : 0),
|
||||
child: Container(
|
||||
constraints: rowConstraints,
|
||||
child: titleRow,
|
||||
|
@ -213,7 +223,9 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
|
|||
|
||||
return LongPressDraggable<TaskData>(
|
||||
data: TaskData(widget.task, _cardSize),
|
||||
maxSimultaneousDrags: taskState.taskDragging ? 0 : 1, // only one task can be dragged at a time
|
||||
maxSimultaneousDrags: taskState.taskDragging
|
||||
? 0
|
||||
: 1, // only one task can be dragged at a time
|
||||
onDragStarted: () {
|
||||
taskState.taskDragging = true;
|
||||
setState(() => _dragging = true);
|
||||
|
@ -223,14 +235,16 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
|
|||
taskState.taskDragging = false;
|
||||
setState(() => _dragging = false);
|
||||
},
|
||||
feedback: (_cardSize == null) ? SizedBox.shrink() : SizedBox.fromSize(
|
||||
size: _cardSize,
|
||||
child: Card(
|
||||
color: card.color,
|
||||
child: (card.child as InkWell).child,
|
||||
elevation: (card.elevation ?? 0) + 5,
|
||||
),
|
||||
),
|
||||
feedback: (_cardSize == null)
|
||||
? SizedBox.shrink()
|
||||
: SizedBox.fromSize(
|
||||
size: _cardSize,
|
||||
child: Card(
|
||||
color: card.color,
|
||||
child: (card.child as InkWell).child,
|
||||
elevation: (card.elevation ?? 0) + 5,
|
||||
),
|
||||
),
|
||||
childWhenDragging: SizedBox.shrink(),
|
||||
child: () {
|
||||
if (_dragging || _cardSize == null) return card;
|
||||
|
@ -241,16 +255,19 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
|
|||
color: Colors.grey,
|
||||
child: SizedBox.fromSize(size: dropBoxSize),
|
||||
);
|
||||
final dropAbove = taskState.taskDragging && _dropLocation == DropLocation.above;
|
||||
final dropBelow = taskState.taskDragging && _dropLocation == DropLocation.below;
|
||||
final DragTargetLeave<TaskData> dragTargetOnLeave = (data) => setState(() {
|
||||
_dropLocation = DropLocation.none;
|
||||
_dropData = null;
|
||||
});
|
||||
final dragTargetOnWillAccept = (TaskData data, DropLocation dropLocation) {
|
||||
if (data.task.bucketId != bucket.id)
|
||||
if (bucket.limit != 0 && bucket.tasks.length >= bucket.limit)
|
||||
return false;
|
||||
final dropAbove =
|
||||
taskState.taskDragging && _dropLocation == DropLocation.above;
|
||||
final dropBelow =
|
||||
taskState.taskDragging && _dropLocation == DropLocation.below;
|
||||
final DragTargetLeave<TaskData> dragTargetOnLeave =
|
||||
(data) => setState(() {
|
||||
_dropLocation = DropLocation.none;
|
||||
_dropData = null;
|
||||
});
|
||||
final dragTargetOnWillAccept =
|
||||
(TaskData data, DropLocation dropLocation) {
|
||||
if (data.task.bucketId != bucket.id) if (bucket.limit != 0 &&
|
||||
bucket.tasks.length >= bucket.limit) return false;
|
||||
setState(() {
|
||||
_dropLocation = dropLocation;
|
||||
_dropData = data;
|
||||
|
@ -259,7 +276,8 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
|
|||
};
|
||||
final DragTargetAccept<TaskData> dragTargetOnAccept = (data) {
|
||||
final index = bucket.tasks.indexOf(widget.task);
|
||||
widget.onAccept(data.task, _dropLocation == DropLocation.above ? index : index + 1);
|
||||
widget.onAccept(data.task,
|
||||
_dropLocation == DropLocation.above ? index : index + 1);
|
||||
setState(() {
|
||||
_dropLocation = DropLocation.none;
|
||||
_dropData = null;
|
||||
|
@ -268,7 +286,8 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
|
|||
|
||||
return SizedBox(
|
||||
width: cardSize.width,
|
||||
height: cardSize.height + (dropAbove || dropBelow ? dropBoxSize.height + 4 : 0),
|
||||
height: cardSize.height +
|
||||
(dropAbove || dropBelow ? dropBoxSize.height + 4 : 0),
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Column(
|
||||
|
@ -281,18 +300,22 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
|
|||
Column(
|
||||
children: <SizedBox>[
|
||||
SizedBox(
|
||||
height: (cardSize.height / 2) + (dropAbove ? dropBoxSize.height : 0),
|
||||
height: (cardSize.height / 2) +
|
||||
(dropAbove ? dropBoxSize.height : 0),
|
||||
child: DragTarget<TaskData>(
|
||||
onWillAccept: (data) => dragTargetOnWillAccept(data!, DropLocation.above),
|
||||
onWillAccept: (data) =>
|
||||
dragTargetOnWillAccept(data!, DropLocation.above),
|
||||
onAccept: dragTargetOnAccept,
|
||||
onLeave: dragTargetOnLeave,
|
||||
builder: (_, __, ___) => SizedBox.expand(),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: (cardSize.height / 2) + (dropBelow ? dropBoxSize.height : 0),
|
||||
height: (cardSize.height / 2) +
|
||||
(dropBelow ? dropBoxSize.height : 0),
|
||||
child: DragTarget<TaskData>(
|
||||
onWillAccept: (data) => dragTargetOnWillAccept(data!, DropLocation.below),
|
||||
onWillAccept: (data) =>
|
||||
dragTargetOnWillAccept(data!, DropLocation.below),
|
||||
onAccept: dragTargetOnAccept,
|
||||
onLeave: dragTargetOnLeave,
|
||||
builder: (_, __, ___) => SizedBox.expand(),
|
||||
|
@ -309,12 +332,13 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
|
|||
|
||||
void _updateCardSize(BuildContext context) {
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) setState(() {
|
||||
_cardSize = context.size;
|
||||
});
|
||||
if (mounted)
|
||||
setState(() {
|
||||
_cardSize = context.size;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => _dragging;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,9 +7,8 @@ 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 '../pages/project/project_task_list.dart';
|
||||
import '../stores/project_store.dart';
|
||||
import '../utils/calculate_item_position.dart';
|
||||
import 'AddDialog.dart';
|
||||
|
@ -26,23 +25,22 @@ class KanbanClass {
|
|||
Function _onViewTapped, _addItemDialog, notify;
|
||||
Duration _lastTaskDragUpdateAction = Duration.zero;
|
||||
|
||||
|
||||
Project _list;
|
||||
Map<int, BucketProps> _bucketProps = {};
|
||||
|
||||
|
||||
KanbanClass(this.context, this.notify, this._onViewTapped, this._addItemDialog, this._list) {
|
||||
KanbanClass(this.context, this.notify, this._onViewTapped,
|
||||
this._addItemDialog, this._list) {
|
||||
taskState = Provider.of<ProjectProvider>(context);
|
||||
}
|
||||
|
||||
|
||||
Widget kanbanView() {
|
||||
final deviceData = MediaQuery.of(context);
|
||||
final portrait = deviceData.orientation == Orientation.portrait;
|
||||
final bucketFraction = portrait ? 0.8 : 0.4;
|
||||
final bucketWidth = deviceData.size.width * bucketFraction;
|
||||
|
||||
if (_pageController == null || _pageController!.viewportFraction != bucketFraction)
|
||||
if (_pageController == null ||
|
||||
_pageController!.viewportFraction != bucketFraction)
|
||||
_pageController = PageController(viewportFraction: bucketFraction);
|
||||
|
||||
print(_list.doneBucketId);
|
||||
|
@ -171,14 +169,16 @@ class KanbanClass {
|
|||
),
|
||||
));
|
||||
}
|
||||
|
||||
Future<void> _setDoneBucket(BuildContext context, int bucketId) async {
|
||||
//setState(() {});
|
||||
_list = (await VikunjaGlobal.of(context).projectService.update(_list.copyWith(doneBucketId: bucketId)))!;
|
||||
_list = (await VikunjaGlobal.of(context)
|
||||
.projectService
|
||||
.update(_list.copyWith(doneBucketId: bucketId)))!;
|
||||
notify();
|
||||
}
|
||||
|
||||
Future<void> _addBucket(
|
||||
String title, BuildContext context) async {
|
||||
Future<void> _addBucket(String title, BuildContext context) async {
|
||||
final currentUser = VikunjaGlobal.of(context).currentUser;
|
||||
if (currentUser == null) {
|
||||
return;
|
||||
|
@ -257,14 +257,12 @@ class KanbanClass {
|
|||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
if (_bucketProps[bucket.id]!.controller.hasClients)
|
||||
//setState(() {
|
||||
_bucketProps[bucket.id]!.bucketLength = bucket.tasks.length;
|
||||
_bucketProps[bucket.id]!.scrollable =
|
||||
_bucketProps[bucket.id]!.controller.position.maxScrollExtent >
|
||||
0;
|
||||
_bucketProps[bucket.id]!.portrait = portrait;
|
||||
//});
|
||||
_bucketProps[bucket.id]!.bucketLength = bucket.tasks.length;
|
||||
_bucketProps[bucket.id]!.scrollable =
|
||||
_bucketProps[bucket.id]!.controller.position.maxScrollExtent > 0;
|
||||
_bucketProps[bucket.id]!.portrait = portrait;
|
||||
//});
|
||||
notify();
|
||||
|
||||
});
|
||||
if (_bucketProps[bucket.id]!.titleController.text.isEmpty)
|
||||
_bucketProps[bucket.id]!.titleController.text = bucket.title;
|
||||
|
@ -428,10 +426,11 @@ class KanbanClass {
|
|||
final screenSize = MediaQuery.of(context).size;
|
||||
const scrollDuration = Duration(milliseconds: 250);
|
||||
const scrollCurve = Curves.easeInOut;
|
||||
final updateAction = () { //setState(() =>
|
||||
final updateAction = () {
|
||||
//setState(() =>
|
||||
_lastTaskDragUpdateAction = details.sourceTimeStamp!;
|
||||
notify();
|
||||
};//);
|
||||
}; //);
|
||||
|
||||
if (details.globalPosition.dx < screenSize.width * 0.1) {
|
||||
// scroll left
|
||||
|
@ -503,8 +502,8 @@ class KanbanClass {
|
|||
if (bucket.tasks.length == 0)
|
||||
DragTarget<TaskData>(
|
||||
onWillAccept: (data) {
|
||||
/*setState(() =>*/ _bucketProps[bucket.id]!.taskDropSize =
|
||||
data?.size;//);
|
||||
/*setState(() =>*/ _bucketProps[bucket.id]!
|
||||
.taskDropSize = data?.size; //);
|
||||
notify();
|
||||
return true;
|
||||
},
|
||||
|
@ -523,12 +522,12 @@ class KanbanClass {
|
|||
)));
|
||||
|
||||
//setState(() =>
|
||||
_bucketProps[bucket.id]!.taskDropSize = null;//);
|
||||
_bucketProps[bucket.id]!.taskDropSize = null; //);
|
||||
notify();
|
||||
},
|
||||
onLeave: (_) {
|
||||
//setState(() =>
|
||||
_bucketProps[bucket.id]!.taskDropSize = null;//)
|
||||
_bucketProps[bucket.id]!.taskDropSize = null; //)
|
||||
notify();
|
||||
},
|
||||
builder: (_, __, ___) => SizedBox.expand(),
|
||||
|
@ -549,12 +548,7 @@ class KanbanClass {
|
|||
}
|
||||
|
||||
Future<void> loadBucketsForPage(int page) {
|
||||
return Provider.of<ProjectProvider>(context, listen: false).loadBuckets(
|
||||
context: context,
|
||||
listId: _list.id,
|
||||
page: page
|
||||
);
|
||||
return Provider.of<ProjectProvider>(context, listen: false)
|
||||
.loadBuckets(context: context, listId: _list.id, page: page);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -20,20 +20,23 @@ class SliverBucketList extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
return index >= bucket.tasks.length ? null : BucketTaskCard(
|
||||
key: ObjectKey(bucket.tasks[index]),
|
||||
task: bucket.tasks[index],
|
||||
index: index,
|
||||
onDragUpdate: onTaskDragUpdate,
|
||||
onAccept: (task, index) {
|
||||
_moveTaskToBucket(context, task, index);
|
||||
},
|
||||
);
|
||||
return index >= bucket.tasks.length
|
||||
? null
|
||||
: BucketTaskCard(
|
||||
key: ObjectKey(bucket.tasks[index]),
|
||||
task: bucket.tasks[index],
|
||||
index: index,
|
||||
onDragUpdate: onTaskDragUpdate,
|
||||
onAccept: (task, index) {
|
||||
_moveTaskToBucket(context, task, index);
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _moveTaskToBucket(BuildContext context, Task task, int index) async {
|
||||
Future<void> _moveTaskToBucket(
|
||||
BuildContext context, Task task, int index) async {
|
||||
await Provider.of<ProjectProvider>(context, listen: false).moveTaskToBucket(
|
||||
context: context,
|
||||
task: task,
|
||||
|
@ -42,7 +45,8 @@ class SliverBucketList extends StatelessWidget {
|
|||
);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text('\'${task.title}\' was moved to \'${bucket.title}\' successfully!'),
|
||||
content: Text(
|
||||
'\'${task.title}\' was moved to \'${bucket.title}\' successfully!'),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,12 +16,14 @@ class SliverBucketPersistentHeader extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return SliverPersistentHeader(
|
||||
pinned: true,
|
||||
delegate: _SliverBucketPersistentHeaderDelegate(child, minExtent, maxExtent),
|
||||
delegate:
|
||||
_SliverBucketPersistentHeaderDelegate(child, minExtent, maxExtent),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SliverBucketPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
|
||||
class _SliverBucketPersistentHeaderDelegate
|
||||
extends SliverPersistentHeaderDelegate {
|
||||
final Widget child;
|
||||
final double min;
|
||||
final double max;
|
||||
|
@ -29,7 +31,8 @@ class _SliverBucketPersistentHeaderDelegate extends SliverPersistentHeaderDelega
|
|||
_SliverBucketPersistentHeaderDelegate(this.child, this.min, this.max);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
|
||||
Widget build(
|
||||
BuildContext context, double shrinkOffset, bool overlapsContent) {
|
||||
return child;
|
||||
}
|
||||
|
||||
|
@ -40,8 +43,10 @@ class _SliverBucketPersistentHeaderDelegate extends SliverPersistentHeaderDelega
|
|||
double get minExtent => min;
|
||||
|
||||
@override
|
||||
bool shouldRebuild(covariant _SliverBucketPersistentHeaderDelegate oldDelegate) {
|
||||
return oldDelegate.child != child || oldDelegate.min != min || oldDelegate.max != max;
|
||||
bool shouldRebuild(
|
||||
covariant _SliverBucketPersistentHeaderDelegate oldDelegate) {
|
||||
return oldDelegate.child != child ||
|
||||
oldDelegate.min != min ||
|
||||
oldDelegate.max != max;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
157
lib/components/TaskBottomSheet.dart
Normal file
157
lib/components/TaskBottomSheet.dart
Normal file
|
@ -0,0 +1,157 @@
|
|||
import 'package:datetime_picker_formfield/datetime_picker_formfield.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
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/utils/priority.dart';
|
||||
|
||||
import '../models/label.dart';
|
||||
import '../models/task.dart';
|
||||
import '../pages/project/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;
|
||||
final bool loading;
|
||||
final Function onEdit;
|
||||
final ValueSetter<bool>? onMarkedAsDone;
|
||||
final ProjectProvider taskState;
|
||||
|
||||
const TaskBottomSheet({
|
||||
Key? key,
|
||||
required this.task,
|
||||
required this.onEdit,
|
||||
required this.taskState,
|
||||
this.loading = false,
|
||||
this.showInfo = false,
|
||||
this.onMarkedAsDone,
|
||||
}) : super(key: key);
|
||||
/*
|
||||
@override
|
||||
TaskTileState createState() {
|
||||
return new TaskTileState(this.task, this.loading);
|
||||
}
|
||||
|
||||
*/
|
||||
@override
|
||||
TaskBottomSheetState createState() => TaskBottomSheetState(this.task);
|
||||
}
|
||||
|
||||
class TaskBottomSheetState extends State<TaskBottomSheet> {
|
||||
Task _currentTask;
|
||||
|
||||
TaskBottomSheetState(this._currentTask);
|
||||
|
||||
@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()),
|
||||
|
||||
// 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"),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
|
@ -3,9 +3,10 @@ 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/task.dart';
|
||||
import 'package:vikunja_app/utils/misc.dart';
|
||||
import 'package:vikunja_app/pages/list/task_edit.dart';
|
||||
import 'package:vikunja_app/pages/project/task_edit.dart';
|
||||
import 'package:vikunja_app/utils/priority.dart';
|
||||
|
||||
import '../stores/project_store.dart';
|
||||
|
@ -32,30 +33,35 @@ class TaskTile extends StatefulWidget {
|
|||
}
|
||||
|
||||
*/
|
||||
@override
|
||||
@override
|
||||
TaskTileState createState() => TaskTileState(this.task);
|
||||
}
|
||||
|
||||
Widget? _buildTaskSubtitle(Task? task, bool showInfo) {
|
||||
Widget? _buildTaskSubtitle(Task? task, bool showInfo, BuildContext context) {
|
||||
Duration? durationUntilDue = task?.dueDate?.difference(DateTime.now());
|
||||
|
||||
if(task == null)
|
||||
return null;
|
||||
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) : null));
|
||||
|
||||
if (showInfo && task.hasDueDate) {
|
||||
texts.add(TextSpan(
|
||||
text: "Due " + durationToHumanReadable(durationUntilDue!),
|
||||
style: durationUntilDue.isNegative
|
||||
? TextStyle(color: Colors.red)
|
||||
: Theme.of(context).textTheme.bodyMedium));
|
||||
}
|
||||
if(task.priority != null && task.priority != 0) {
|
||||
texts.add(TextSpan(text: " !" + priorityToString(task.priority), style: TextStyle(color: Colors.orange)));
|
||||
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.isEmpty && task.description.isNotEmpty) {
|
||||
// return HtmlWidget(task.description);
|
||||
// }
|
||||
|
||||
if(texts.isNotEmpty) {
|
||||
if (texts.isNotEmpty) {
|
||||
return RichText(text: TextSpan(children: texts));
|
||||
}
|
||||
return null;
|
||||
|
@ -82,59 +88,76 @@ class TaskTileState extends State<TaskTile> with AutomaticKeepAliveClientMixin {
|
|||
)),
|
||||
),
|
||||
title: Text(_currentTask.title),
|
||||
subtitle:
|
||||
_currentTask.description.isEmpty
|
||||
? null
|
||||
: HtmlWidget(_currentTask.description),
|
||||
subtitle: _currentTask.description.isEmpty
|
||||
? null
|
||||
: HtmlWidget(_currentTask.description),
|
||||
trailing: IconButton(
|
||||
icon: Icon(Icons.settings), onPressed: () { },
|
||||
),
|
||||
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),
|
||||
),
|
||||
Flexible(child: CheckboxListTile(
|
||||
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),
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
value: _currentTask.done,
|
||||
subtitle: _buildTaskSubtitle(widget.task, widget.showInfo),
|
||||
secondary:
|
||||
IconButton(icon: Icon(Icons.settings), onPressed: () {
|
||||
Navigator.push<Task>(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (buildContext) => TaskEditPage(
|
||||
task: _currentTask,
|
||||
taskState: taskState,
|
||||
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),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
).then((task) => setState(() {
|
||||
if (task != null) _currentTask = task;
|
||||
})).whenComplete(() => widget.onEdit());
|
||||
}),
|
||||
onChanged: _change,
|
||||
))]));
|
||||
))
|
||||
: 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());
|
||||
}),
|
||||
))
|
||||
]));
|
||||
}
|
||||
|
||||
void _change(bool? value) async {
|
||||
|
@ -144,8 +167,7 @@ class TaskTileState extends State<TaskTile> with AutomaticKeepAliveClientMixin {
|
|||
});
|
||||
Task? newTask = await _updateTask(_currentTask, value);
|
||||
setState(() {
|
||||
if(newTask != null)
|
||||
this._currentTask = newTask;
|
||||
if (newTask != null) this._currentTask = newTask;
|
||||
this._currentTask.loading = false;
|
||||
});
|
||||
widget.onEdit();
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -40,8 +40,7 @@ class VikunjaDateTimePicker extends StatelessWidget {
|
|||
onSaved: onSaved,
|
||||
onChanged: onChanged,
|
||||
onShowPicker: (context, currentValue) {
|
||||
if(currentValue == null)
|
||||
currentValue = DateTime.now();
|
||||
if (currentValue == null) currentValue = DateTime.now();
|
||||
return _showDatePickerFuture(context, currentValue);
|
||||
},
|
||||
);
|
||||
|
@ -51,26 +50,22 @@ class VikunjaDateTimePicker extends StatelessWidget {
|
|||
return showDialog(
|
||||
context: context,
|
||||
builder: (_) => DatePickerDialog(
|
||||
initialDate: currentValue.year <= 1
|
||||
? DateTime.now()
|
||||
: currentValue,
|
||||
firstDate: DateTime(1900),
|
||||
lastDate: DateTime(2100),
|
||||
initialCalendarMode: DatePickerMode.day,
|
||||
)).then((date) {
|
||||
if(date == null)
|
||||
return null;
|
||||
return showDialog(
|
||||
initialDate:
|
||||
currentValue.year <= 1 ? DateTime.now() : currentValue,
|
||||
firstDate: DateTime(1900),
|
||||
lastDate: DateTime(2100),
|
||||
initialCalendarMode: DatePickerMode.day,
|
||||
)).then((date) {
|
||||
if (date == null) return null;
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (_) =>
|
||||
TimePickerDialog(
|
||||
builder: (_) => TimePickerDialog(
|
||||
initialTime: TimeOfDay.fromDateTime(currentValue),
|
||||
)
|
||||
).then((time) {
|
||||
if(time == null)
|
||||
return null;
|
||||
return DateTime(date.year,date.month, date.day,time.hour,time.minute);
|
||||
});
|
||||
)).then((time) {
|
||||
if (time == null) return null;
|
||||
return DateTime(
|
||||
date.year, date.month, date.day, time.hour, time.minute);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,9 +3,9 @@ import 'package:vikunja_app/models/label.dart';
|
|||
|
||||
class LabelComponent extends StatelessWidget {
|
||||
final Label label;
|
||||
final VoidCallback onDelete;
|
||||
final VoidCallback? onDelete;
|
||||
|
||||
const LabelComponent({Key? key, required this.label, required this.onDelete})
|
||||
const LabelComponent({Key? key, required this.label, this.onDelete})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:developer' as dev;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:vikunja_app/api/bucket_implementation.dart';
|
||||
|
@ -7,8 +8,6 @@ import 'package:vikunja_app/api/client.dart';
|
|||
import 'package:vikunja_app/api/label_task.dart';
|
||||
import 'package:vikunja_app/api/label_task_bulk.dart';
|
||||
import 'package:vikunja_app/api/labels.dart';
|
||||
import 'package:vikunja_app/api/list_implementation.dart';
|
||||
import 'package:vikunja_app/api/namespace_implementation.dart';
|
||||
import 'package:vikunja_app/api/server_implementation.dart';
|
||||
import 'package:vikunja_app/api/task_implementation.dart';
|
||||
import 'package:vikunja_app/api/user_implementation.dart';
|
||||
|
@ -23,7 +22,6 @@ import 'package:workmanager/workmanager.dart';
|
|||
import 'api/project.dart';
|
||||
import 'main.dart';
|
||||
|
||||
|
||||
class VikunjaGlobal extends StatefulWidget {
|
||||
final Widget child;
|
||||
final Widget login;
|
||||
|
@ -50,7 +48,6 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
|
|||
UserService? _newUserService;
|
||||
NotificationClass _notificationClass = NotificationClass();
|
||||
|
||||
|
||||
User? get currentUser => _currentUser;
|
||||
|
||||
Client get client => _client;
|
||||
|
@ -67,21 +64,16 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
|
|||
|
||||
VersionChecker get versionChecker => new VersionChecker(snackbarKey);
|
||||
|
||||
NamespaceService get namespaceService => new NamespaceAPIService(client);
|
||||
|
||||
ProjectService get projectService => new ProjectAPIService(client, _storage);
|
||||
|
||||
TaskService get taskService => new TaskAPIService(client);
|
||||
|
||||
BucketService get bucketService => new BucketAPIService(client);
|
||||
|
||||
ListService get listService => new ListAPIService(client, _storage);
|
||||
|
||||
TaskServiceOptions get taskServiceOptions => new TaskServiceOptions();
|
||||
|
||||
NotificationClass get notifications => _notificationClass;
|
||||
|
||||
|
||||
LabelService get labelService => new LabelAPIService(client);
|
||||
|
||||
LabelTaskService get labelTaskService => new LabelTaskAPIService(client);
|
||||
|
@ -89,18 +81,29 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
|
|||
LabelTaskBulkAPIService get labelTaskBulkService =>
|
||||
new LabelTaskBulkAPIService(client);
|
||||
|
||||
|
||||
late String currentTimeZone;
|
||||
|
||||
void updateWorkmanagerDuration() {
|
||||
if (kIsWeb) {
|
||||
return;
|
||||
}
|
||||
Workmanager().cancelAll().then((value) {
|
||||
settingsManager.getWorkmanagerDuration().then((duration) =>
|
||||
{
|
||||
if(duration.inMinutes > 0) {
|
||||
Workmanager().registerPeriodicTask(
|
||||
"update-tasks", "update-tasks", frequency: duration, constraints: Constraints(networkType: NetworkType.connected),
|
||||
initialDelay: Duration(seconds: 15), inputData: {"client_token": client.token, "client_base": client.base})
|
||||
settingsManager.getWorkmanagerDuration().then((duration) {
|
||||
if (duration.inMinutes > 0) {
|
||||
Workmanager().registerPeriodicTask("update-tasks", "update-tasks",
|
||||
frequency: duration,
|
||||
constraints: Constraints(networkType: NetworkType.connected),
|
||||
initialDelay: Duration(seconds: 15),
|
||||
inputData: {
|
||||
"client_token": client.token,
|
||||
"client_base": client.base
|
||||
});
|
||||
}
|
||||
|
||||
Workmanager().registerPeriodicTask("refresh-token", "refresh-token",
|
||||
frequency: Duration(hours: 12),
|
||||
constraints: Constraints(networkType: NetworkType.connected),
|
||||
initialDelay: Duration(seconds: 15));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -109,13 +112,15 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
|
|||
void initState() {
|
||||
super.initState();
|
||||
_client = Client(snackbarKey);
|
||||
settingsManager.getIgnoreCertificates().then((value) => client.reload_ignore_certs(value == "1"));
|
||||
settingsManager
|
||||
.getIgnoreCertificates()
|
||||
.then((value) => client.reloadIgnoreCerts(value == "1"));
|
||||
_newUserService = UserAPIService(client);
|
||||
_loadCurrentUser();
|
||||
tz.initializeTimeZones();
|
||||
notifications.notificationInitializer();
|
||||
settingsManager.getVersionNotifications().then((value) {
|
||||
if(value == "1") {
|
||||
if (value == "1") {
|
||||
versionChecker.postVersionCheckSnackbar();
|
||||
}
|
||||
});
|
||||
|
@ -148,17 +153,16 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
void logoutUser(BuildContext context) async {
|
||||
// _storage.deleteAll().then((_) {
|
||||
var userId = await _storage.read(key: "currentUser");
|
||||
await _storage.delete(key: userId!); //delete token
|
||||
await _storage.delete(key: "${userId}_base");
|
||||
setState(() {
|
||||
client.reset();
|
||||
_currentUser = null;
|
||||
});
|
||||
/* }).catchError((err) {
|
||||
var userId = await _storage.read(key: "currentUser");
|
||||
await _storage.delete(key: userId!); //delete token
|
||||
await _storage.delete(key: "${userId}_base");
|
||||
setState(() {
|
||||
client.reset();
|
||||
_currentUser = null;
|
||||
});
|
||||
/* }).catchError((err) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text('An error occurred while logging out!'),
|
||||
));
|
||||
|
@ -185,8 +189,14 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
|
|||
User loadedCurrentUser;
|
||||
try {
|
||||
loadedCurrentUser = await UserAPIService(client).getCurrentUser();
|
||||
// load new token from server to avoid expiration
|
||||
String? newToken = await newUserService?.getToken();
|
||||
if (newToken != null) {
|
||||
_storage.write(key: currentUser, value: newToken);
|
||||
client.configure(token: newToken);
|
||||
}
|
||||
} on ApiException catch (e) {
|
||||
dev.log("Error code: " + e.errorCode.toString(),level: 1000);
|
||||
dev.log("Error code: " + e.errorCode.toString(), level: 1000);
|
||||
if (e.errorCode ~/ 100 == 4) {
|
||||
client.authenticated = false;
|
||||
if (e.errorCode == 401) {
|
||||
|
@ -216,7 +226,7 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
|
|||
if (_loading) {
|
||||
return new Center(child: new CircularProgressIndicator());
|
||||
}
|
||||
if(client.authenticated) {
|
||||
if (client.authenticated) {
|
||||
notifications.scheduleDueNotifications(taskService);
|
||||
}
|
||||
return new VikunjaGlobalInherited(
|
||||
|
|
167
lib/main.dart
167
lib/main.dart
|
@ -1,6 +1,7 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
@ -15,6 +16,7 @@ import 'package:vikunja_app/theme/theme.dart';
|
|||
import 'package:timezone/data/latest_all.dart' as tz;
|
||||
import 'package:flutter_downloader/flutter_downloader.dart';
|
||||
|
||||
import 'api/user_implementation.dart';
|
||||
import 'managers/notifications.dart';
|
||||
|
||||
class IgnoreCertHttpOverrides extends HttpOverrides {
|
||||
|
@ -33,8 +35,12 @@ class IgnoreCertHttpOverrides extends HttpOverrides {
|
|||
|
||||
@pragma('vm:entry-point')
|
||||
void callbackDispatcher() {
|
||||
Workmanager().executeTask((task, inputData) {
|
||||
print("Native called background task: $task"); //simpleTask will be emitted here.
|
||||
if (kIsWeb) {
|
||||
return;
|
||||
}
|
||||
Workmanager().executeTask((task, inputData) async {
|
||||
print(
|
||||
"Native called background task: $task"); //simpleTask will be emitted here.
|
||||
if (task == "update-tasks" && inputData != null) {
|
||||
Client client = Client(null,
|
||||
token: inputData["client_token"],
|
||||
|
@ -46,7 +52,7 @@ void callbackDispatcher() {
|
|||
.getIgnoreCertificates()
|
||||
.then((value) async {
|
||||
print("ignoring: $value");
|
||||
client.reload_ignore_certs(value == "1");
|
||||
client.reloadIgnoreCerts(value == "1");
|
||||
|
||||
TaskAPIService taskService = TaskAPIService(client);
|
||||
NotificationClass nc = NotificationClass();
|
||||
|
@ -55,11 +61,34 @@ void callbackDispatcher() {
|
|||
.scheduleDueNotifications(taskService)
|
||||
.then((value) => Future.value(true));
|
||||
});
|
||||
} else if (task == "refresh-token") {
|
||||
print("running refresh from workmanager");
|
||||
final FlutterSecureStorage _storage = new FlutterSecureStorage();
|
||||
|
||||
var currentUser = await _storage.read(key: 'currentUser');
|
||||
if (currentUser == null) {
|
||||
return Future.value(true);
|
||||
}
|
||||
var token = await _storage.read(key: currentUser);
|
||||
|
||||
var base = await _storage.read(key: '${currentUser}_base');
|
||||
if (token == null || base == null) {
|
||||
return Future.value(true);
|
||||
}
|
||||
Client client = Client(null);
|
||||
client.configure(token: token, base: base, authenticated: true);
|
||||
// load new token from server to avoid expiration
|
||||
String? newToken = await UserAPIService(client).getToken();
|
||||
if (newToken != null) {
|
||||
_storage.write(key: currentUser, value: newToken);
|
||||
}
|
||||
return Future.value(true);
|
||||
} else {
|
||||
return Future.value(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
final globalSnackbarKey = GlobalKey<ScaffoldMessengerState>();
|
||||
final globalNavigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
|
@ -70,73 +99,91 @@ void main() async {
|
|||
Permission.notification.request();
|
||||
}
|
||||
});
|
||||
await FlutterDownloader.initialize();
|
||||
Workmanager().initialize(callbackDispatcher, isInDebugMode: false);
|
||||
try {
|
||||
if (!kIsWeb) {
|
||||
await FlutterDownloader.initialize();
|
||||
}
|
||||
} catch (e) {
|
||||
print("Failed to initialize downloader: $e");
|
||||
}
|
||||
try {
|
||||
if (!kIsWeb) {
|
||||
Workmanager().initialize(callbackDispatcher, isInDebugMode: false);
|
||||
}
|
||||
} catch (e) {
|
||||
print("Failed to initialize workmanager: $e");
|
||||
}
|
||||
runApp(VikunjaGlobal(
|
||||
child: new VikunjaApp(
|
||||
home: HomePage(),
|
||||
key: UniqueKey(),
|
||||
navkey: globalNavigatorKey,
|
||||
),
|
||||
login: new VikunjaApp(
|
||||
home: LoginPage(),
|
||||
key: UniqueKey(),
|
||||
)));
|
||||
child: new VikunjaApp(
|
||||
home: HomePage(),
|
||||
key: UniqueKey(),
|
||||
navkey: globalNavigatorKey,
|
||||
),
|
||||
login: new VikunjaApp(
|
||||
home: LoginPage(),
|
||||
key: UniqueKey(),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
final ValueNotifier<bool> updateTheme = ValueNotifier(false);
|
||||
|
||||
class VikunjaApp extends StatelessWidget {
|
||||
final Widget home;
|
||||
final GlobalKey<NavigatorState>? navkey;
|
||||
|
||||
const VikunjaApp({Key? key, required this.home, this.navkey}) : super(key: key);
|
||||
const VikunjaApp({Key? key, required this.home, this.navkey})
|
||||
: super(key: key);
|
||||
|
||||
Future<ThemeData> getThemedata() async {
|
||||
FlutterThemeMode themeMode = FlutterThemeMode.light;
|
||||
try {
|
||||
SettingsManager manager = SettingsManager(new FlutterSecureStorage());
|
||||
themeMode = await manager.getThemeMode();
|
||||
} catch (e) {
|
||||
print("Failed to get theme mode: $e");
|
||||
}
|
||||
switch (themeMode) {
|
||||
case FlutterThemeMode.dark:
|
||||
return buildVikunjaDarkTheme();
|
||||
case FlutterThemeMode.materialYouLight:
|
||||
return buildVikunjaMaterialLightTheme();
|
||||
case FlutterThemeMode.materialYouDark:
|
||||
return buildVikunjaMaterialDarkTheme();
|
||||
default:
|
||||
return buildVikunjaTheme();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
SettingsManager manager = SettingsManager(new FlutterSecureStorage());
|
||||
|
||||
|
||||
return new ValueListenableBuilder(valueListenable: updateTheme, builder: (_,mode,__) {
|
||||
updateTheme.value = false;
|
||||
FlutterThemeMode themeMode = FlutterThemeMode.system;
|
||||
Future<ThemeData> theme = manager.getThemeMode().then((value) {
|
||||
themeMode = value;
|
||||
switch(value) {
|
||||
case FlutterThemeMode.dark:
|
||||
return buildVikunjaDarkTheme();
|
||||
case FlutterThemeMode.materialYouLight:
|
||||
return buildVikunjaMaterialLightTheme();
|
||||
case FlutterThemeMode.materialYouDark:
|
||||
return buildVikunjaMaterialDarkTheme();
|
||||
default:
|
||||
return buildVikunjaTheme();
|
||||
}
|
||||
|
||||
});
|
||||
return FutureBuilder<ThemeData>(
|
||||
future: theme,
|
||||
builder: (BuildContext context, AsyncSnapshot<ThemeData> data) {
|
||||
if(data.hasData) {
|
||||
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: themeData,
|
||||
scaffoldMessengerKey: globalSnackbarKey,
|
||||
navigatorKey: navkey,
|
||||
// <= this
|
||||
home: this.home,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
});});
|
||||
return new ValueListenableBuilder(
|
||||
valueListenable: updateTheme,
|
||||
builder: (_, mode, __) {
|
||||
return FutureBuilder<ThemeData>(
|
||||
future: getThemedata(),
|
||||
builder: (BuildContext context, AsyncSnapshot<ThemeData> data) {
|
||||
if (data.hasData) {
|
||||
return new DynamicColorBuilder(
|
||||
builder: (lightTheme, darkTheme) {
|
||||
ThemeData? themeData = data.data;
|
||||
if (data.data == FlutterThemeMode.materialYouLight)
|
||||
themeData = themeData?.copyWith(colorScheme: lightTheme);
|
||||
else if (data.data == FlutterThemeMode.materialYouDark)
|
||||
themeData = themeData?.copyWith(colorScheme: darkTheme);
|
||||
return MaterialApp(
|
||||
title: 'Vikunja',
|
||||
theme: themeData,
|
||||
scaffoldMessengerKey: globalSnackbarKey,
|
||||
navigatorKey: navkey,
|
||||
// <= this
|
||||
home: this.home,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,8 @@ import 'dart:math';
|
|||
import 'package:flutter_timezone/flutter_timezone.dart';
|
||||
import 'package:timezone/timezone.dart' as tz;
|
||||
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart'as notifs;
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart'
|
||||
as notifs;
|
||||
import 'package:rxdart/subjects.dart' as rxSub;
|
||||
import 'package:vikunja_app/service/services.dart';
|
||||
|
||||
|
@ -19,62 +20,58 @@ class NotificationClass {
|
|||
late String currentTimeZone;
|
||||
notifs.NotificationAppLaunchDetails? notifLaunch;
|
||||
|
||||
notifs.FlutterLocalNotificationsPlugin get notificationsPlugin => new notifs.FlutterLocalNotificationsPlugin();
|
||||
|
||||
notifs.FlutterLocalNotificationsPlugin get notificationsPlugin =>
|
||||
new notifs.FlutterLocalNotificationsPlugin();
|
||||
|
||||
var androidSpecificsDueDate = notifs.AndroidNotificationDetails(
|
||||
"Vikunja1",
|
||||
"Due Date Notifications",
|
||||
"Vikunja1", "Due Date Notifications",
|
||||
channelDescription: "description",
|
||||
icon: 'vikunja_notification_logo',
|
||||
importance: notifs.Importance.high
|
||||
);
|
||||
importance: notifs.Importance.high);
|
||||
var androidSpecificsReminders = notifs.AndroidNotificationDetails(
|
||||
"Vikunja2",
|
||||
"Reminder Notifications",
|
||||
"Vikunja2", "Reminder Notifications",
|
||||
channelDescription: "description",
|
||||
icon: 'vikunja_notification_logo',
|
||||
importance: notifs.Importance.high
|
||||
);
|
||||
late notifs.IOSNotificationDetails iOSSpecifics;
|
||||
importance: notifs.Importance.high);
|
||||
late notifs.DarwinNotificationDetails iOSSpecifics;
|
||||
late notifs.NotificationDetails platformChannelSpecificsDueDate;
|
||||
late notifs.NotificationDetails platformChannelSpecificsReminders;
|
||||
|
||||
NotificationClass({this.id, this.body, this.payload, this.title});
|
||||
|
||||
final rxSub.BehaviorSubject<
|
||||
NotificationClass> didReceiveLocalNotificationSubject =
|
||||
rxSub.BehaviorSubject<NotificationClass>();
|
||||
final rxSub.BehaviorSubject<NotificationClass>
|
||||
didReceiveLocalNotificationSubject =
|
||||
rxSub.BehaviorSubject<NotificationClass>();
|
||||
final rxSub.BehaviorSubject<String> selectNotificationSubject =
|
||||
rxSub.BehaviorSubject<String>();
|
||||
rxSub.BehaviorSubject<String>();
|
||||
|
||||
Future<void> _initNotifications() async {
|
||||
var initializationSettingsAndroid =
|
||||
notifs.AndroidInitializationSettings('vikunja_logo');
|
||||
var initializationSettingsIOS = notifs.IOSInitializationSettings(
|
||||
notifs.AndroidInitializationSettings('vikunja_logo');
|
||||
var initializationSettingsIOS = notifs.DarwinInitializationSettings(
|
||||
requestAlertPermission: false,
|
||||
requestBadgePermission: false,
|
||||
requestSoundPermission: false,
|
||||
onDidReceiveLocalNotification:
|
||||
(int? id, String? title, String? body, String? payload) async {
|
||||
didReceiveLocalNotificationSubject
|
||||
.add(NotificationClass(
|
||||
didReceiveLocalNotificationSubject.add(NotificationClass(
|
||||
id: id, title: title, body: body, payload: payload));
|
||||
});
|
||||
var initializationSettings = notifs.InitializationSettings(
|
||||
android: initializationSettingsAndroid, iOS: initializationSettingsIOS);
|
||||
await notificationsPlugin.initialize(initializationSettings,
|
||||
onSelectNotification: (String? payload) async {
|
||||
if (payload != null) {
|
||||
print('notification payload: ' + payload);
|
||||
selectNotificationSubject.add(payload);
|
||||
}
|
||||
});
|
||||
onDidReceiveNotificationResponse:
|
||||
(notifs.NotificationResponse resp) async {
|
||||
if (payload != null) {
|
||||
print('notification payload: ' + resp.payload!);
|
||||
selectNotificationSubject.add(resp.payload!);
|
||||
}
|
||||
});
|
||||
print("Notifications initialised successfully");
|
||||
}
|
||||
|
||||
Future<void> notificationInitializer() async {
|
||||
iOSSpecifics = notifs.IOSNotificationDetails();
|
||||
iOSSpecifics = notifs.DarwinNotificationDetails();
|
||||
platformChannelSpecificsDueDate = notifs.NotificationDetails(
|
||||
android: androidSpecificsDueDate, iOS: iOSSpecifics);
|
||||
platformChannelSpecificsReminders = notifs.NotificationDetails(
|
||||
|
@ -86,20 +83,23 @@ class NotificationClass {
|
|||
return Future.value();
|
||||
}
|
||||
|
||||
Future<void> scheduleNotification(String title, String description,
|
||||
Future<void> scheduleNotification(
|
||||
String title,
|
||||
String description,
|
||||
notifs.FlutterLocalNotificationsPlugin notifsPlugin,
|
||||
DateTime scheduledTime, String currentTimeZone,
|
||||
notifs.NotificationDetails platformChannelSpecifics, {int? id}) async {
|
||||
if (id == null)
|
||||
id = Random().nextInt(1000000);
|
||||
DateTime scheduledTime,
|
||||
String currentTimeZone,
|
||||
notifs.NotificationDetails platformChannelSpecifics,
|
||||
{int? id}) async {
|
||||
if (id == null) id = Random().nextInt(1000000);
|
||||
// TODO: move to setup
|
||||
tz.TZDateTime time = tz.TZDateTime.from(
|
||||
scheduledTime, tz.getLocation(currentTimeZone));
|
||||
tz.TZDateTime time =
|
||||
tz.TZDateTime.from(scheduledTime, tz.getLocation(currentTimeZone));
|
||||
if (time.difference(tz.TZDateTime.now(tz.getLocation(currentTimeZone))) <
|
||||
Duration.zero)
|
||||
return;
|
||||
await notifsPlugin.zonedSchedule(id, title, description,
|
||||
time, platformChannelSpecifics, androidAllowWhileIdle: true,
|
||||
Duration.zero) return;
|
||||
await notifsPlugin.zonedSchedule(
|
||||
id, title, description, time, platformChannelSpecifics,
|
||||
androidAllowWhileIdle: true,
|
||||
uiLocalNotificationDateInterpretation: notifs
|
||||
.UILocalNotificationDateInterpretation
|
||||
.wallClockTime); // This literally schedules the notification
|
||||
|
@ -110,30 +110,27 @@ class NotificationClass {
|
|||
"This is a test notification", platformChannelSpecificsReminders);
|
||||
}
|
||||
|
||||
|
||||
void requestIOSPermissions() {
|
||||
notificationsPlugin.resolvePlatformSpecificImplementation<
|
||||
notifs.IOSFlutterLocalNotificationsPlugin>()
|
||||
notificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
notifs.IOSFlutterLocalNotificationsPlugin>()
|
||||
?.requestPermissions(
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Future<void> scheduleDueNotifications(TaskService taskService,
|
||||
{List<Task>? tasks}) async {
|
||||
if (tasks == null)
|
||||
tasks = await taskService.getAll();
|
||||
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;
|
||||
if (task.done) continue;
|
||||
for (final reminder in task.reminderDates) {
|
||||
scheduleNotification(
|
||||
"Reminder",
|
||||
|
@ -159,5 +156,4 @@ class NotificationClass {
|
|||
}
|
||||
print("notifications scheduled successfully");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,15 +47,15 @@ class Bucket {
|
|||
.toList();
|
||||
|
||||
toJSON() => {
|
||||
'id': id,
|
||||
'list_id': projectId,
|
||||
'title': title,
|
||||
'position': position,
|
||||
'limit': limit,
|
||||
'is_done_bucket': isDoneBucket,
|
||||
'created': created.toUtc().toIso8601String(),
|
||||
'updated': updated.toUtc().toIso8601String(),
|
||||
'created_by': createdBy.toJSON(),
|
||||
'tasks': tasks.map((task) => task.toJSON()).toList(),
|
||||
};
|
||||
}
|
||||
'id': id,
|
||||
'list_id': projectId,
|
||||
'title': title,
|
||||
'position': position,
|
||||
'limit': limit,
|
||||
'is_done_bucket': isDoneBucket,
|
||||
'created': created.toUtc().toIso8601String(),
|
||||
'updated': updated.toUtc().toIso8601String(),
|
||||
'created_by': createdBy.toJSON(),
|
||||
'tasks': tasks.map((task) => task.toJSON()).toList(),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -10,7 +10,9 @@ class Label {
|
|||
final User createdBy;
|
||||
final Color? color;
|
||||
|
||||
late final Color textColor = color != null && color!.computeLuminance() <= 0.5 ? vLabelLight : vLabelDark;
|
||||
late final Color textColor = color != null && color!.computeLuminance() <= 0.5
|
||||
? vLabelLight
|
||||
: vLabelDark;
|
||||
|
||||
Label({
|
||||
this.id = 0,
|
||||
|
|
|
@ -10,7 +10,8 @@ class LabelTask {
|
|||
LabelTask({required this.label, required this.task});
|
||||
|
||||
LabelTask.fromJson(Map<String, dynamic> json, User createdBy)
|
||||
: label = new Label(id: json['label_id'], title: '', createdBy: createdBy),
|
||||
: label =
|
||||
new Label(id: json['label_id'], title: '', createdBy: createdBy),
|
||||
task = null;
|
||||
|
||||
toJSON() => {
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
import 'package:vikunja_app/models/task.dart';
|
||||
import 'package:vikunja_app/models/user.dart';
|
||||
|
||||
class TaskList {
|
||||
final int id;
|
||||
int namespaceId;
|
||||
String title, description;
|
||||
final User owner;
|
||||
final DateTime created, updated;
|
||||
final List<Task> tasks;
|
||||
final bool isFavorite;
|
||||
|
||||
TaskList({
|
||||
this.id = 0,
|
||||
required this.title,
|
||||
required this.namespaceId,
|
||||
this.description = '',
|
||||
required this.owner,
|
||||
DateTime? created,
|
||||
DateTime? updated,
|
||||
List<Task>? tasks,
|
||||
this.isFavorite = false,
|
||||
}) : this.created = created ?? DateTime.now(),
|
||||
this.updated = updated ?? DateTime.now(),
|
||||
this.tasks = tasks ?? [];
|
||||
|
||||
TaskList.fromJson(Map<String, dynamic> json)
|
||||
: id = json['id'],
|
||||
owner = User.fromJson(json['owner']),
|
||||
description = json['description'],
|
||||
title = json['title'],
|
||||
updated = DateTime.parse(json['updated']),
|
||||
created = DateTime.parse(json['created']),
|
||||
isFavorite = json['is_favorite'],
|
||||
namespaceId = json['namespace_id'],
|
||||
tasks = json['tasks'] == null ? [] : (json['tasks'] as List<dynamic>)
|
||||
.map((taskJson) => Task.fromJson(taskJson))
|
||||
.toList();
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'description': description,
|
||||
'owner': owner.toJSON(),
|
||||
'created': created.toUtc().toIso8601String(),
|
||||
'updated': updated.toUtc().toIso8601String(),
|
||||
'namespace_id': namespaceId
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
import 'package:vikunja_app/models/user.dart';
|
||||
|
||||
class Namespace {
|
||||
final int id;
|
||||
final DateTime created, updated;
|
||||
final String title, description;
|
||||
final User? owner;
|
||||
|
||||
Namespace({
|
||||
this.id = 0,
|
||||
DateTime? created,
|
||||
DateTime? updated,
|
||||
required this.title,
|
||||
this.description = '',
|
||||
required this.owner,
|
||||
}) : this.created = created ?? DateTime.now(),
|
||||
this.updated = updated ?? DateTime.now();
|
||||
|
||||
Namespace.fromJson(Map<String, dynamic> json)
|
||||
: title = json['title'],
|
||||
description = json['description'],
|
||||
id = json['id'],
|
||||
created = DateTime.parse(json['created']),
|
||||
updated = DateTime.parse(json['updated']),
|
||||
owner = json['owner'] != null ? User.fromJson(json['owner']) : null;
|
||||
|
||||
Map<String, dynamic> toJSON() => {
|
||||
'id': id,
|
||||
'created': created.toUtc().toIso8601String(),
|
||||
'updated': updated.toUtc().toIso8601String(),
|
||||
'title': title,
|
||||
'owner': owner?.toJSON(),
|
||||
'description': description
|
||||
};
|
||||
|
||||
Namespace copyWith({
|
||||
int? id,
|
||||
DateTime? created,
|
||||
DateTime? updated,
|
||||
String? title,
|
||||
String? description,
|
||||
User? owner,
|
||||
}) {
|
||||
return Namespace(
|
||||
id: id ?? this.id,
|
||||
created: created ?? this.created,
|
||||
updated: updated ?? this.updated,
|
||||
title: title ?? this.title,
|
||||
description: description ?? this.description,
|
||||
owner: owner ?? this.owner,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -17,20 +17,19 @@ class Project {
|
|||
Iterable<Project>? subprojects;
|
||||
|
||||
Project(
|
||||
{
|
||||
this.id = 0,
|
||||
this.owner,
|
||||
this.parentProjectId = 0,
|
||||
this.description = '',
|
||||
this.position = 0,
|
||||
this.doneBucketId,
|
||||
this.color,
|
||||
this.isArchived = false,
|
||||
this.isFavourite = false,
|
||||
required this.title,
|
||||
{this.id = 0,
|
||||
this.owner,
|
||||
this.parentProjectId = 0,
|
||||
this.description = '',
|
||||
this.position = 0,
|
||||
this.doneBucketId,
|
||||
this.color,
|
||||
this.isArchived = false,
|
||||
this.isFavourite = false,
|
||||
required this.title,
|
||||
created,
|
||||
updated}) :
|
||||
this.created = created ?? DateTime.now(),
|
||||
updated})
|
||||
: this.created = created ?? DateTime.now(),
|
||||
this.updated = updated ?? DateTime.now();
|
||||
|
||||
Project.fromJson(Map<String, dynamic> json)
|
||||
|
@ -50,19 +49,20 @@ class Project {
|
|||
owner = json['owner'] != null ? User.fromJson(json['owner']) : null;
|
||||
|
||||
Map<String, dynamic> toJSON() => {
|
||||
'id': id,
|
||||
'created': created.toUtc().toIso8601String(),
|
||||
'updated': updated.toUtc().toIso8601String(),
|
||||
'title': title,
|
||||
'owner': owner?.toJSON(),
|
||||
'description': description,
|
||||
'parent_project_id': parentProjectId,
|
||||
'hex_color': color?.value.toRadixString(16).padLeft(8, '0').substring(2),
|
||||
'is_archived': isArchived,
|
||||
'is_favourite': isFavourite,
|
||||
'done_bucket_id': doneBucketId,
|
||||
'position': position
|
||||
};
|
||||
'id': id,
|
||||
'created': created.toUtc().toIso8601String(),
|
||||
'updated': updated.toUtc().toIso8601String(),
|
||||
'title': title,
|
||||
'owner': owner?.toJSON(),
|
||||
'description': description,
|
||||
'parent_project_id': parentProjectId,
|
||||
'hex_color':
|
||||
color?.value.toRadixString(16).padLeft(8, '0').substring(2),
|
||||
'is_archived': isArchived,
|
||||
'is_favourite': isFavourite,
|
||||
'done_bucket_id': doneBucketId,
|
||||
'position': position
|
||||
};
|
||||
|
||||
Project copyWith({
|
||||
int? id,
|
||||
|
@ -77,21 +77,20 @@ class Project {
|
|||
bool? isFavourite,
|
||||
int? doneBucketId,
|
||||
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,
|
||||
doneBucketId: doneBucketId ?? this.doneBucketId,
|
||||
color: color ?? this.color,
|
||||
isArchived: isArchived ?? this.isArchived,
|
||||
isFavourite: isFavourite ?? this.isFavourite,
|
||||
position: position ?? this.position,
|
||||
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,
|
||||
doneBucketId: doneBucketId ?? this.doneBucketId,
|
||||
color: color ?? this.color,
|
||||
isArchived: isArchived ?? this.isArchived,
|
||||
isFavourite: isFavourite ?? this.isFavourite,
|
||||
position: position ?? this.position,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,8 +13,7 @@ class Server {
|
|||
String? version;
|
||||
|
||||
Server.fromJson(Map<String, dynamic> json)
|
||||
:
|
||||
caldavEnabled = json['caldav_enabled'],
|
||||
: caldavEnabled = json['caldav_enabled'],
|
||||
emailRemindersEnabled = json['email_reminders_enabled'],
|
||||
frontendUrl = json['frontend_url'],
|
||||
linkSharingEnabled = json['link_sharing_enabled'],
|
||||
|
@ -26,4 +25,4 @@ class Server {
|
|||
totpEnabled = json['totp_enabled'],
|
||||
userDeletion = json['user_deletion'],
|
||||
version = json['version'];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ class Task {
|
|||
final bool done;
|
||||
Color? color;
|
||||
final double? kanbanPosition;
|
||||
final double? percent_done;
|
||||
final User createdBy;
|
||||
Duration? repeatAfter;
|
||||
final List<Task> subtasks;
|
||||
|
@ -45,6 +46,7 @@ class Task {
|
|||
this.repeatAfter,
|
||||
this.color,
|
||||
this.kanbanPosition,
|
||||
this.percent_done,
|
||||
this.subtasks = const [],
|
||||
this.labels = const [],
|
||||
this.attachments = const [],
|
||||
|
@ -65,6 +67,7 @@ class Task {
|
|||
}
|
||||
return Colors.white;
|
||||
}
|
||||
|
||||
bool get hasDueDate => dueDate?.year != 1;
|
||||
|
||||
Task.fromJson(Map<String, dynamic> json)
|
||||
|
@ -90,6 +93,9 @@ class Task {
|
|||
kanbanPosition = json['kanban_position'] is int
|
||||
? json['kanban_position'].toDouble()
|
||||
: json['kanban_position'],
|
||||
percent_done = json['percent_done'] is int
|
||||
? json['percent_done'].toDouble()
|
||||
: json['percent_done'],
|
||||
labels = json['labels'] != null
|
||||
? (json['labels'] as List<dynamic>)
|
||||
.map((label) => Label.fromJson(label))
|
||||
|
@ -126,8 +132,10 @@ class Task {
|
|||
'end_date': endDate?.toUtc().toIso8601String(),
|
||||
'priority': priority,
|
||||
'repeat_after': repeatAfter?.inSeconds,
|
||||
'hex_color': color?.value.toRadixString(16).padLeft(8, '0').substring(2),
|
||||
'hex_color':
|
||||
color?.value.toRadixString(16).padLeft(8, '0').substring(2),
|
||||
'kanban_position': kanbanPosition,
|
||||
'percent_done': percent_done,
|
||||
'project_id': projectId,
|
||||
'labels': labels.map((label) => label.toJSON()).toList(),
|
||||
'subtasks': subtasks.map((subtask) => subtask.toJSON()).toList(),
|
||||
|
@ -157,6 +165,7 @@ class Task {
|
|||
bool? done,
|
||||
Color? color,
|
||||
double? kanbanPosition,
|
||||
double? percent_done,
|
||||
User? createdBy,
|
||||
Duration? repeatAfter,
|
||||
List<Task>? subtasks,
|
||||
|
@ -182,6 +191,7 @@ class Task {
|
|||
done: done ?? this.done,
|
||||
color: color ?? this.color,
|
||||
kanbanPosition: kanbanPosition ?? this.kanbanPosition,
|
||||
percent_done: percent_done ?? this.percent_done,
|
||||
createdBy: createdBy ?? this.createdBy,
|
||||
repeatAfter: repeatAfter ?? this.repeatAfter,
|
||||
subtasks: subtasks ?? this.subtasks,
|
||||
|
|
|
@ -25,15 +25,14 @@ class TaskAttachmentFile {
|
|||
size = json['size'];
|
||||
|
||||
toJSON() => {
|
||||
'id': id,
|
||||
'created': created.toUtc().toIso8601String(),
|
||||
'mime': mime,
|
||||
'name': name,
|
||||
'size': size,
|
||||
};
|
||||
'id': id,
|
||||
'created': created.toUtc().toIso8601String(),
|
||||
'mime': mime,
|
||||
'name': name,
|
||||
'size': size,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@JsonSerializable()
|
||||
class TaskAttachment {
|
||||
final int id, taskId;
|
||||
|
@ -58,10 +57,10 @@ class TaskAttachment {
|
|||
createdBy = User.fromJson(json['created_by']);
|
||||
|
||||
toJSON() => {
|
||||
'id': id,
|
||||
'task_id': taskId,
|
||||
'created': created.toUtc().toIso8601String(),
|
||||
'created_by': createdBy.toJSON(),
|
||||
'file': file.toJSON(),
|
||||
};
|
||||
}
|
||||
'id': id,
|
||||
'task_id': taskId,
|
||||
'created': created.toUtc().toIso8601String(),
|
||||
'created_by': createdBy.toJSON(),
|
||||
'file': file.toJSON(),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -3,7 +3,9 @@ import 'package:vikunja_app/global.dart';
|
|||
|
||||
class UserSettings {
|
||||
final int default_project_id;
|
||||
final bool discoverable_by_email, discoverable_by_name, email_reminders_enabled;
|
||||
final bool discoverable_by_email,
|
||||
discoverable_by_name,
|
||||
email_reminders_enabled;
|
||||
final Map<String, dynamic>? frontend_settings;
|
||||
final String language;
|
||||
final String name;
|
||||
|
@ -34,24 +36,25 @@ class UserSettings {
|
|||
frontend_settings = json['frontend_settings'],
|
||||
language = json['language'],
|
||||
name = json['name'],
|
||||
overdue_tasks_reminders_enabled = json['overdue_tasks_reminders_enabled'],
|
||||
overdue_tasks_reminders_enabled =
|
||||
json['overdue_tasks_reminders_enabled'],
|
||||
overdue_tasks_reminders_time = json['overdue_tasks_reminders_time'],
|
||||
timezone = json['timezone'],
|
||||
week_start = json['week_start'];
|
||||
|
||||
toJson() => {
|
||||
'default_project_id': default_project_id,
|
||||
'discoverable_by_email': discoverable_by_email,
|
||||
'discoverable_by_name': discoverable_by_name,
|
||||
'email_reminders_enabled': email_reminders_enabled,
|
||||
'frontend_settings': frontend_settings,
|
||||
'language': language,
|
||||
'name': name,
|
||||
'overdue_tasks_reminders_enabled': overdue_tasks_reminders_enabled,
|
||||
'overdue_tasks_reminders_time': overdue_tasks_reminders_time,
|
||||
'timezone': timezone,
|
||||
'week_start': week_start,
|
||||
};
|
||||
'default_project_id': default_project_id,
|
||||
'discoverable_by_email': discoverable_by_email,
|
||||
'discoverable_by_name': discoverable_by_name,
|
||||
'email_reminders_enabled': email_reminders_enabled,
|
||||
'frontend_settings': frontend_settings,
|
||||
'language': language,
|
||||
'name': name,
|
||||
'overdue_tasks_reminders_enabled': overdue_tasks_reminders_enabled,
|
||||
'overdue_tasks_reminders_time': overdue_tasks_reminders_time,
|
||||
'timezone': timezone,
|
||||
'week_start': week_start,
|
||||
};
|
||||
|
||||
UserSettings copyWith({
|
||||
int? default_project_id,
|
||||
|
@ -68,14 +71,18 @@ class UserSettings {
|
|||
}) {
|
||||
return UserSettings(
|
||||
default_project_id: default_project_id ?? this.default_project_id,
|
||||
discoverable_by_email: discoverable_by_email ?? this.discoverable_by_email,
|
||||
discoverable_by_email:
|
||||
discoverable_by_email ?? this.discoverable_by_email,
|
||||
discoverable_by_name: discoverable_by_name ?? this.discoverable_by_name,
|
||||
email_reminders_enabled: email_reminders_enabled ?? this.email_reminders_enabled,
|
||||
email_reminders_enabled:
|
||||
email_reminders_enabled ?? this.email_reminders_enabled,
|
||||
frontend_settings: frontend_settings ?? this.frontend_settings,
|
||||
language: language ?? this.language,
|
||||
name: name ?? this.name,
|
||||
overdue_tasks_reminders_enabled: overdue_tasks_reminders_enabled ?? this.overdue_tasks_reminders_enabled,
|
||||
overdue_tasks_reminders_time: overdue_tasks_reminders_time ?? this.overdue_tasks_reminders_time,
|
||||
overdue_tasks_reminders_enabled: overdue_tasks_reminders_enabled ??
|
||||
this.overdue_tasks_reminders_enabled,
|
||||
overdue_tasks_reminders_time:
|
||||
overdue_tasks_reminders_time ?? this.overdue_tasks_reminders_time,
|
||||
timezone: timezone ?? this.timezone,
|
||||
week_start: week_start ?? this.week_start,
|
||||
);
|
||||
|
@ -104,9 +111,10 @@ class User {
|
|||
username = json['username'],
|
||||
created = DateTime.parse(json['created']),
|
||||
updated = DateTime.parse(json['updated']) {
|
||||
if(json.containsKey('settings')){
|
||||
if (json.containsKey('settings')) {
|
||||
this.settings = UserSettings.fromJson(json['settings']);
|
||||
};
|
||||
}
|
||||
;
|
||||
}
|
||||
|
||||
toJSON() => {
|
||||
|
|
|
@ -2,18 +2,10 @@ import 'dart:async';
|
|||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:after_layout/after_layout.dart';
|
||||
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';
|
||||
|
||||
|
@ -28,7 +20,6 @@ class HomePageState extends State<HomePage> {
|
|||
int _selectedDrawerIndex = 0, _previousDrawerIndex = 0;
|
||||
Widget? drawerItem;
|
||||
|
||||
|
||||
List<Widget> widgets = [
|
||||
ChangeNotifierProvider<ProjectProvider>(
|
||||
create: (_) => new ProjectProvider(),
|
||||
|
|
|
@ -37,11 +37,11 @@ class LandingPageState extends State<LandingPage>
|
|||
static const platform = const MethodChannel('vikunja');
|
||||
|
||||
Future<void> _updateDefaultList() async {
|
||||
|
||||
return VikunjaGlobal.of(context).newUserService?.getCurrentUser().then((value) =>
|
||||
setState(() {
|
||||
defaultList = value?.settings?.default_project_id;
|
||||
} ),);
|
||||
return VikunjaGlobal.of(context).newUserService?.getCurrentUser().then(
|
||||
(value) => setState(() {
|
||||
defaultList = value?.settings?.default_project_id;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -55,7 +55,10 @@ class LandingPageState extends State<LandingPage>
|
|||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
VikunjaGlobal.of(context).settingsManager.getLandingPageOnlyDueDateTasks().then((value) => onlyDueDate = value);
|
||||
VikunjaGlobal.of(context)
|
||||
.settingsManager
|
||||
.getLandingPageOnlyDueDateTasks()
|
||||
.then((value) => onlyDueDate = value);
|
||||
}));
|
||||
super.initState();
|
||||
}
|
||||
|
@ -133,26 +136,29 @@ class LandingPageState extends State<LandingPage>
|
|||
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) { },
|
||||
)
|
||||
])))
|
||||
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) {},
|
||||
)
|
||||
])))
|
||||
];
|
||||
}),
|
||||
],
|
||||
|
@ -226,28 +232,47 @@ class LandingPageState extends State<LandingPage>
|
|||
.settingsManager
|
||||
.getLandingPageOnlyDueDateTasks()
|
||||
.then((showOnlyDueDateTasks) {
|
||||
return VikunjaGlobal
|
||||
.of(context)
|
||||
.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));
|
||||
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));
|
||||
;
|
||||
}
|
||||
|
||||
});//.onError((error, stackTrace) {print("error");});
|
||||
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");});
|
||||
}
|
||||
|
||||
Future<void> _handleTaskList(List<Task>? taskList, bool showOnlyDueDateTasks) {
|
||||
if(showOnlyDueDateTasks)
|
||||
taskList?.removeWhere((element) => element.dueDate == null || element.dueDate!.year == 0001);
|
||||
Future<void> _handleTaskList(
|
||||
List<Task>? taskList, bool showOnlyDueDateTasks) {
|
||||
if (showOnlyDueDateTasks)
|
||||
taskList?.removeWhere((element) =>
|
||||
element.dueDate == null || element.dueDate!.year == 0001);
|
||||
|
||||
if (taskList != null && taskList.isEmpty) {
|
||||
setState(() {
|
||||
|
|
|
@ -1,329 +0,0 @@
|
|||
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/stores/list_store.dart';
|
||||
|
||||
import '../../components/pagestatus.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 TaskList taskList;
|
||||
|
||||
//ListPage({this.taskList}) : super(key: Key(taskList.id.toString()));
|
||||
ListPage({required this.taskList})
|
||||
: super(key: Key(Random().nextInt(100000).toString()));
|
||||
|
||||
@override
|
||||
_ListPageState createState() => _ListPageState();
|
||||
}
|
||||
|
||||
class _ListPageState extends State<ListPage> {
|
||||
final _keyboardController = KeyboardVisibilityController();
|
||||
int _viewIndex = 0;
|
||||
late TaskList _list;
|
||||
List<Task> _loadingTasks = [];
|
||||
int _currentPage = 1;
|
||||
bool displayDoneTasks = false;
|
||||
late ListProvider taskState;
|
||||
late KanbanClass _kanban;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_list = widget.taskList;
|
||||
_keyboardController.onChange.listen((visible) {
|
||||
if (!visible && mounted) FocusScope.of(context).unfocus();
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void nullSetState() {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
taskState = Provider.of<ListProvider>(context);
|
||||
//_kanban = KanbanClass(
|
||||
// context, nullSetState, _onViewTapped, _addItemDialog, _list);
|
||||
|
||||
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(_list.title),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(Icons.edit),
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ListEditPage(
|
||||
list: _list,
|
||||
),
|
||||
)).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>[
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.view_list),
|
||||
label: 'List',
|
||||
tooltip: 'List',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.view_kanban),
|
||||
label: 'Kanban',
|
||||
tooltip: 'Kanban',
|
||||
),
|
||||
],
|
||||
currentIndex: _viewIndex,
|
||||
onTap: _onViewTapped,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onViewTapped(int index) {
|
||||
_loadList().then((_) {
|
||||
_currentPage = 1;
|
||||
setState(() {
|
||||
_viewIndex = index;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ListView _listView(BuildContext context) {
|
||||
return 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<ListProvider>(context, listen: false).updateTask(
|
||||
context: context,
|
||||
task: task.copyWith(done: done),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updateDisplayDoneTasks() {
|
||||
return VikunjaGlobal.of(context)
|
||||
.listService
|
||||
.getDisplayDoneTasks(_list.id)
|
||||
.then((value) {
|
||||
displayDoneTasks = value == "1";
|
||||
});
|
||||
}
|
||||
|
||||
TaskTile _buildLoadingTile(Task task) {
|
||||
return TaskTile(
|
||||
task: task,
|
||||
loading: true, onEdit: () {},
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _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<void> _loadTasksForPage(int page) {
|
||||
return Provider.of<ListProvider>(context, listen: false)
|
||||
.loadTasks(
|
||||
context: context,
|
||||
listId: _list.id,
|
||||
page: page,
|
||||
displayDoneTasks: displayDoneTasks);
|
||||
}
|
||||
|
||||
Future<void> _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<void> _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: _list.id,
|
||||
);
|
||||
setState(() => _loadingTasks.add(newTask));
|
||||
return Provider.of<ListProvider>(context, listen: false)
|
||||
.addTask(
|
||||
context: context,
|
||||
newTask: newTask,
|
||||
listId: _list.id,
|
||||
)
|
||||
.then((_) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text('The task was added successfully' +
|
||||
(bucket != null ? ' to \'${bucket.title}\'' : '') +
|
||||
'!'),
|
||||
));
|
||||
setState(() {
|
||||
_loadingTasks.remove(newTask);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,171 +0,0 @@
|
|||
import 'dart:ffi';
|
||||
import 'dart:developer';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||
import 'package:vikunja_app/global.dart';
|
||||
import 'package:vikunja_app/models/list.dart';
|
||||
import 'package:vikunja_app/theme/button.dart';
|
||||
import 'package:vikunja_app/theme/buttonText.dart';
|
||||
|
||||
class ListEditPage extends StatefulWidget {
|
||||
final TaskList list;
|
||||
|
||||
ListEditPage({required this.list}) : super(key: Key(list.toString()));
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _ListEditPageState();
|
||||
}
|
||||
|
||||
class _ListEditPageState extends State<ListEditPage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
bool _loading = false;
|
||||
String _title = '', _description = '';
|
||||
bool? displayDoneTasks;
|
||||
late int listId;
|
||||
|
||||
@override
|
||||
void initState(){
|
||||
listId = widget.list.id;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext ctx) {
|
||||
if(displayDoneTasks == null)
|
||||
VikunjaGlobal.of(context).listService.getDisplayDoneTasks(listId).then(
|
||||
(value) => setState(() => displayDoneTasks = value == "1"));
|
||||
else
|
||||
log("Display done tasks: " + displayDoneTasks.toString());
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Edit List'),
|
||||
),
|
||||
body: Builder(
|
||||
builder: (BuildContext context) => SafeArea(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: ListView(
|
||||
//reverse: true,
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 10.0),
|
||||
child: TextFormField(
|
||||
maxLines: null,
|
||||
keyboardType: TextInputType.multiline,
|
||||
initialValue: widget.list.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.list.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<String> matches = <String>[];
|
||||
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?)
|
||||
widget.list.title = _title;
|
||||
widget.list.description = _description;
|
||||
VikunjaGlobal.of(context).listService.update(widget.list).then((_) {
|
||||
setState(() => _loading = false);
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text('The list 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),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,195 +0,0 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:vikunja_app/components/AddDialog.dart';
|
||||
import 'package:vikunja_app/global.dart';
|
||||
import 'package:vikunja_app/models/list.dart';
|
||||
import 'package:vikunja_app/models/namespace.dart';
|
||||
import 'package:vikunja_app/pages/list/list.dart';
|
||||
import 'package:vikunja_app/pages/namespace/namespace_edit.dart';
|
||||
import 'package:vikunja_app/stores/list_store.dart';
|
||||
|
||||
import '../../components/pagestatus.dart';
|
||||
|
||||
class NamespacePage extends StatefulWidget {
|
||||
final Namespace namespace;
|
||||
|
||||
NamespacePage({required this.namespace})
|
||||
: super(key: Key(namespace.id.toString()));
|
||||
|
||||
@override
|
||||
_NamespacePageState createState() => new _NamespacePageState();
|
||||
}
|
||||
|
||||
class _NamespacePageState extends State<NamespacePage> {
|
||||
List<TaskList> _lists = [];
|
||||
PageStatus namespacestatus = PageStatus.loading;
|
||||
|
||||
/////
|
||||
// This essentially shows the lists.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget body;
|
||||
switch (namespacestatus) {
|
||||
case PageStatus.built:
|
||||
_loadLists();
|
||||
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 = new ListView(
|
||||
padding: EdgeInsets.symmetric(vertical: 8.0),
|
||||
children: ListTile.divideTiles(
|
||||
context: context,
|
||||
tiles: _lists.map((ls) => Dismissible(
|
||||
key: Key(ls.id.toString()),
|
||||
direction: DismissDirection.startToEnd,
|
||||
child: ListTile(
|
||||
title: new Text(ls.title),
|
||||
onTap: () => _openList(context, ls),
|
||||
trailing: Icon(Icons.arrow_right),
|
||||
),
|
||||
background: Container(
|
||||
color: Colors.red,
|
||||
child: const ListTile(
|
||||
leading: Icon(Icons.delete,
|
||||
color: Colors.white, size: 36.0)),
|
||||
),
|
||||
onDismissed: (direction) {
|
||||
_removeList(ls).then((_) => ScaffoldMessenger.of(context)
|
||||
.showSnackBar(
|
||||
SnackBar(content: Text("${ls.title} removed"))));
|
||||
},
|
||||
))).toList(),
|
||||
);
|
||||
break;
|
||||
case PageStatus.empty:
|
||||
body = new Stack(children: [
|
||||
ListView(),
|
||||
Center(child: Text("This view is empty"))
|
||||
]);
|
||||
break;
|
||||
}
|
||||
return new Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(widget.namespace.title),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(Icons.edit),
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => NamespaceEditPage(
|
||||
namespace: widget.namespace,
|
||||
),
|
||||
)).whenComplete(() => _loadLists()),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: RefreshIndicator(onRefresh: () => _loadLists(), child: body),
|
||||
floatingActionButton: Builder(
|
||||
builder: (context) => FloatingActionButton(
|
||||
onPressed: () => _addListDialog(context),
|
||||
child: const Icon(Icons.add))),
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_loadLists();
|
||||
}
|
||||
|
||||
Future<void> _removeList(TaskList list) {
|
||||
return VikunjaGlobal.of(context)
|
||||
.listService
|
||||
.delete(list.id)
|
||||
.then((_) => _loadLists());
|
||||
}
|
||||
|
||||
Future<void> _loadLists() {
|
||||
// FIXME: This is called even when the tasks on a list are loaded - which is not needed at all
|
||||
namespacestatus = PageStatus.loading;
|
||||
return VikunjaGlobal.of(context)
|
||||
.listService
|
||||
.getByNamespace(widget.namespace.id)
|
||||
.then((lists) => setState(() {
|
||||
if (lists != null) {
|
||||
this._lists = lists;
|
||||
namespacestatus = PageStatus.success;
|
||||
} else {
|
||||
namespacestatus = PageStatus.error;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
_openList(BuildContext context, TaskList list) {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => ChangeNotifierProvider<ListProvider>(
|
||||
create: (_) => new ListProvider(),
|
||||
child: ListPage(
|
||||
taskList: list,
|
||||
),
|
||||
),
|
||||
// ListPage(taskList: list)
|
||||
));
|
||||
}
|
||||
|
||||
_addListDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => AddDialog(
|
||||
onAdd: (name) => _addList(name, context),
|
||||
decoration: new InputDecoration(
|
||||
labelText: 'List Name', hintText: 'eg. Shopping List')),
|
||||
);
|
||||
}
|
||||
|
||||
void _addList(String name, BuildContext context) {
|
||||
final curentUser = VikunjaGlobal.of(context).currentUser;
|
||||
if (curentUser == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
VikunjaGlobal.of(context)
|
||||
.listService
|
||||
.create(
|
||||
widget.namespace.id,
|
||||
TaskList(
|
||||
title: name,
|
||||
tasks: [],
|
||||
namespaceId: widget.namespace.id,
|
||||
owner: curentUser,
|
||||
))
|
||||
.then((_) {
|
||||
setState(() {});
|
||||
_loadLists();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('The list was successfully created!'),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,129 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:vikunja_app/global.dart';
|
||||
import 'package:vikunja_app/models/namespace.dart';
|
||||
import 'package:vikunja_app/theme/button.dart';
|
||||
import 'package:vikunja_app/theme/buttonText.dart';
|
||||
|
||||
class NamespaceEditPage extends StatefulWidget {
|
||||
final Namespace namespace;
|
||||
|
||||
NamespaceEditPage({required this.namespace}) : super(key: Key(namespace.toString()));
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _NamespaceEditPageState();
|
||||
}
|
||||
|
||||
class _NamespaceEditPageState extends State<NamespaceEditPage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
bool _loading = false;
|
||||
late String _name, _description;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_name = widget.namespace.title;
|
||||
_description = widget.namespace.description;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext ctx) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Edit Namespace'),
|
||||
),
|
||||
body: Builder(
|
||||
builder: (BuildContext context) => SafeArea(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 10.0),
|
||||
child: TextFormField(
|
||||
maxLines: null,
|
||||
keyboardType: TextInputType.multiline,
|
||||
initialValue: widget.namespace.title,
|
||||
onSaved: (name) => _name = name ?? '',
|
||||
validator: (name) {
|
||||
//if (name.length < 3 || name.length > 250) {
|
||||
// return 'The name needs to have between 3 and 250 characters.';
|
||||
//}
|
||||
return null;
|
||||
},
|
||||
decoration: new InputDecoration(
|
||||
labelText: 'Name',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 10.0),
|
||||
child: TextFormField(
|
||||
maxLines: null,
|
||||
keyboardType: TextInputType.multiline,
|
||||
initialValue: widget.namespace.description,
|
||||
onSaved: (description) => _description = description ?? '',
|
||||
validator: (description) {
|
||||
//if (description.length > 1000) {
|
||||
// return 'The description can have a maximum of 1000 characters.';
|
||||
//}
|
||||
return null;
|
||||
},
|
||||
decoration: new InputDecoration(
|
||||
labelText: 'Description',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
Builder(
|
||||
builder: (context) => Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 10.0),
|
||||
child: FancyButton(
|
||||
onPressed: !_loading
|
||||
? () {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
Form.of(context)?.save();
|
||||
_saveNamespace(context);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: _loading
|
||||
? CircularProgressIndicator()
|
||||
: VikunjaButtonText('Save'),
|
||||
))),
|
||||
]),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_saveNamespace(BuildContext context) async {
|
||||
setState(() => _loading = true);
|
||||
final updatedNamespace = widget.namespace.copyWith(
|
||||
title: _name,
|
||||
description: _description,
|
||||
);
|
||||
|
||||
VikunjaGlobal.of(context)
|
||||
.namespaceService
|
||||
.update(updatedNamespace)
|
||||
.then((_) {
|
||||
setState(() => _loading = false);
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text('The namespace 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),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,123 +0,0 @@
|
|||
import 'package:after_layout/after_layout.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../components/AddDialog.dart';
|
||||
import '../../components/ErrorDialog.dart';
|
||||
import '../../global.dart';
|
||||
import '../../models/namespace.dart';
|
||||
import 'namespace.dart';
|
||||
|
||||
class NamespaceOverviewPage extends StatefulWidget {
|
||||
@override
|
||||
_NamespaceOverviewPageState createState() =>
|
||||
new _NamespaceOverviewPageState();
|
||||
}
|
||||
|
||||
class _NamespaceOverviewPageState extends State<NamespaceOverviewPage>
|
||||
with AfterLayoutMixin<NamespaceOverviewPage> {
|
||||
List<Namespace> _namespaces = [];
|
||||
int _selectedDrawerIndex = -2, _previousDrawerIndex = -2;
|
||||
bool _loading = true;
|
||||
|
||||
Namespace? get _currentNamespace =>
|
||||
_selectedDrawerIndex >= -1 && _selectedDrawerIndex < _namespaces.length
|
||||
? _namespaces[_selectedDrawerIndex]
|
||||
: null;
|
||||
|
||||
@override
|
||||
void afterFirstLayout(BuildContext context) {
|
||||
_loadNamespaces();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Widget> namespacesList = <Widget>[];
|
||||
_namespaces
|
||||
.asMap()
|
||||
.forEach((i, namespace) => namespacesList.add(new ListTile(
|
||||
leading: const Icon(Icons.folder),
|
||||
title: new Text(namespace.title),
|
||||
selected: i == _selectedDrawerIndex,
|
||||
onTap: () => _onSelectItem(i),
|
||||
)));
|
||||
|
||||
if(_selectedDrawerIndex > -1) {
|
||||
return new WillPopScope(
|
||||
child: NamespacePage(namespace: _namespaces[_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: namespacesList)
|
||||
.toList()),
|
||||
onRefresh: _loadNamespaces,
|
||||
),
|
||||
floatingActionButton: Builder(
|
||||
builder: (context) => FloatingActionButton(
|
||||
onPressed: () => _addNamespaceDialog(context),
|
||||
child: const Icon(Icons.add))),
|
||||
appBar: AppBar(
|
||||
title: Text("Namespaces"),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _loadNamespaces() {
|
||||
return VikunjaGlobal.of(context).namespaceService.getAll().then((result) {
|
||||
setState(() {
|
||||
_loading = false;
|
||||
if (result != null) _namespaces = result;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_onSelectItem(int index) {
|
||||
Navigator.push(context,
|
||||
MaterialPageRoute(
|
||||
builder: (buildContext) => NamespacePage(
|
||||
namespace: _namespaces[index],
|
||||
),));
|
||||
//setState(() => _selectedDrawerIndex = index);
|
||||
}
|
||||
|
||||
_addNamespaceDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => AddDialog(
|
||||
onAdd: (name) => _addNamespace(name, context),
|
||||
decoration: new InputDecoration(
|
||||
labelText: 'Namespace', hintText: 'eg. Personal Namespace'),
|
||||
));
|
||||
}
|
||||
|
||||
_addNamespace(String name, BuildContext context) {
|
||||
final currentUser = VikunjaGlobal.of(context).currentUser;
|
||||
if (currentUser == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
VikunjaGlobal.of(context)
|
||||
.namespaceService
|
||||
.create(Namespace(title: name, owner: currentUser))
|
||||
.then((_) {
|
||||
_loadNamespaces();
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text('The namespace was created successfully!'),
|
||||
));
|
||||
}).catchError((error) => showDialog(
|
||||
context: context, builder: (context) => ErrorDialog(error: error)));
|
||||
}
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import 'package:after_layout/after_layout.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -38,9 +37,9 @@ class _ProjectOverviewPageState extends State<ProjectOverviewPage>
|
|||
bool expanded = expandedList.contains(project.id);
|
||||
Widget icon;
|
||||
|
||||
List<Widget>? children = addProjectChildren(project, level+1);
|
||||
List<Widget>? children = addProjectChildren(project, level + 1);
|
||||
bool no_children = children.length == 0;
|
||||
if(no_children) {
|
||||
if (no_children) {
|
||||
icon = Icon(Icons.list);
|
||||
} else {
|
||||
if (expanded) {
|
||||
|
@ -51,34 +50,34 @@ class _ProjectOverviewPageState extends State<ProjectOverviewPage>
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
return Column(children: [
|
||||
ListTile(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
openList(context, project);
|
||||
});
|
||||
},
|
||||
contentPadding: insets,
|
||||
return Column(children: [
|
||||
ListTile(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
openList(context, 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,
|
||||
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
|
||||
]);
|
||||
}
|
||||
|
||||
...?children
|
||||
]);
|
||||
}
|
||||
|
||||
List<Widget> addProjectChildren(Project project, level) {
|
||||
Iterable<Project> children =
|
||||
|
@ -121,7 +120,6 @@ class _ProjectOverviewPageState extends State<ProjectOverviewPage>
|
|||
.toList()),
|
||||
onRefresh: _loadProjects,
|
||||
),
|
||||
|
||||
appBar: AppBar(
|
||||
title: Text("Projects"),
|
||||
),
|
||||
|
@ -137,8 +135,6 @@ class _ProjectOverviewPageState extends State<ProjectOverviewPage>
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
_addProjectDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import 'dart:ffi';
|
||||
import 'dart:developer';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:vikunja_app/global.dart';
|
||||
|
@ -10,7 +9,8 @@ import '../../models/project.dart';
|
|||
class ProjectEditPage extends StatefulWidget {
|
||||
final Project project;
|
||||
|
||||
ProjectEditPage({required this.project}) : super(key: Key(project.toString()));
|
||||
ProjectEditPage({required this.project})
|
||||
: super(key: Key(project.toString()));
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _ProjectEditPageState();
|
||||
|
@ -24,16 +24,18 @@ class _ProjectEditPageState extends State<ProjectEditPage> {
|
|||
late int listId;
|
||||
|
||||
@override
|
||||
void initState(){
|
||||
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"));
|
||||
if (displayDoneTasks == null)
|
||||
VikunjaGlobal.of(context)
|
||||
.projectService
|
||||
.getDisplayDoneTasks(listId)
|
||||
.then((value) => setState(() => displayDoneTasks = value == "1"));
|
||||
else
|
||||
log("Display done tasks: " + displayDoneTasks.toString());
|
||||
return Scaffold(
|
||||
|
@ -45,7 +47,7 @@ class _ProjectEditPageState extends State<ProjectEditPage> {
|
|||
child: Form(
|
||||
key: _formKey,
|
||||
child: ListView(
|
||||
//reverse: true,
|
||||
//reverse: true,
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
|
@ -73,10 +75,10 @@ class _ProjectEditPageState extends State<ProjectEditPage> {
|
|||
maxLines: null,
|
||||
keyboardType: TextInputType.multiline,
|
||||
initialValue: widget.project.description,
|
||||
onSaved: (description) => _description = description ?? '',
|
||||
onSaved: (description) =>
|
||||
_description = description ?? '',
|
||||
validator: (description) {
|
||||
if(description == null)
|
||||
return null;
|
||||
if (description == null) return null;
|
||||
if (description.length > 1000) {
|
||||
return 'The description can have a maximum of 1000 characters.';
|
||||
}
|
||||
|
@ -95,7 +97,9 @@ class _ProjectEditPageState extends State<ProjectEditPage> {
|
|||
title: Text("Show done tasks"),
|
||||
onChanged: (value) {
|
||||
value ??= false;
|
||||
VikunjaGlobal.of(context).listService.setDisplayDoneTasks(listId, value ? "1" : "0");
|
||||
VikunjaGlobal.of(context)
|
||||
.projectService
|
||||
.setDisplayDoneTasks(listId, value ? "1" : "0");
|
||||
setState(() => displayDoneTasks = value);
|
||||
},
|
||||
),
|
||||
|
@ -106,11 +110,11 @@ class _ProjectEditPageState extends State<ProjectEditPage> {
|
|||
child: FancyButton(
|
||||
onPressed: !_loading
|
||||
? () {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
Form.of(context)?.save();
|
||||
_saveList(context);
|
||||
}
|
||||
}
|
||||
if (_formKey.currentState!.validate()) {
|
||||
Form.of(context)?.save();
|
||||
_saveList(context);
|
||||
}
|
||||
}
|
||||
: () {},
|
||||
child: _loading
|
||||
? CircularProgressIndicator()
|
||||
|
@ -137,8 +141,7 @@ class _ProjectEditPageState extends State<ProjectEditPage> {
|
|||
},)
|
||||
],
|
||||
)*/
|
||||
]
|
||||
),
|
||||
]),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -149,10 +152,8 @@ class _ProjectEditPageState extends State<ProjectEditPage> {
|
|||
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
|
||||
);
|
||||
Project newProject =
|
||||
widget.project.copyWith(title: _title, description: _description);
|
||||
VikunjaGlobal.of(context).projectService.update(newProject).then((_) {
|
||||
setState(() => _loading = false);
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
|
|
|
@ -8,12 +8,10 @@ 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 'package:vikunja_app/pages/project/task_edit.dart';
|
||||
|
||||
import '../../components/pagestatus.dart';
|
||||
import '../../models/project.dart';
|
||||
|
@ -97,42 +95,42 @@ class _ListPageState extends State<ListPage> {
|
|||
]);
|
||||
break;
|
||||
case PageStatus.success:
|
||||
body = taskState.tasks.length > 0 || taskState.buckets.length > 0 || _project.subprojects!.length > 0
|
||||
body = taskState.tasks.length > 0 ||
|
||||
taskState.buckets.length > 0 ||
|
||||
_project.subprojects!.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)),
|
||||
),
|
||||
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);
|
||||
}
|
||||
}(),
|
||||
),
|
||||
);
|
||||
}(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.'))
|
||||
]);
|
||||
ListView(),
|
||||
Center(child: Text('This list is empty.'))
|
||||
]);
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -156,10 +154,10 @@ class _ListPageState extends State<ListPage> {
|
|||
floatingActionButton: _viewIndex == 1
|
||||
? null
|
||||
: Builder(
|
||||
builder: (context) => FloatingActionButton(
|
||||
onPressed: () => _addItemDialog(context),
|
||||
child: Icon(Icons.add)),
|
||||
),
|
||||
builder: (context) => FloatingActionButton(
|
||||
onPressed: () => _addItemDialog(context),
|
||||
child: Icon(Icons.add)),
|
||||
),
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
items: const <BottomNavigationBarItem>[
|
||||
BottomNavigationBarItem(
|
||||
|
@ -183,22 +181,23 @@ class _ListPageState extends State<ListPage> {
|
|||
return Container(
|
||||
height: 80,
|
||||
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
|
||||
child:
|
||||
ListView(
|
||||
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,)))
|
||||
),
|
||||
...?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,
|
||||
)))),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -215,39 +214,50 @@ class _ListPageState extends State<ListPage> {
|
|||
|
||||
Widget _listView(BuildContext context) {
|
||||
List<Widget> children = [];
|
||||
if(widget.project.subprojects?.length != 0) {
|
||||
children.add(Padding(child: Text("Projects", style: TextStyle(fontWeight: FontWeight.bold),), padding: EdgeInsets.fromLTRB(0, 10, 0, 0),));
|
||||
if (widget.project.subprojects?.length != 0) {
|
||||
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),));
|
||||
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();
|
||||
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);
|
||||
}
|
||||
if (_loadingTasks.isNotEmpty) {
|
||||
final loadingTask = _loadingTasks.removeLast();
|
||||
return _buildLoadingTile(loadingTask);
|
||||
}
|
||||
|
||||
final index = i ~/ 2;
|
||||
final index = i ~/ 2;
|
||||
|
||||
if (taskState.maxPages == _currentPage &&
|
||||
index == taskState.tasks.length)
|
||||
throw Exception("Check itemCount attribute");
|
||||
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]);
|
||||
})));
|
||||
if (index >= taskState.tasks.length &&
|
||||
_currentPage < taskState.maxPages) {
|
||||
_currentPage++;
|
||||
_loadTasksForPage(_currentPage);
|
||||
}
|
||||
return _buildTile(taskState.tasks[index]);
|
||||
})));
|
||||
}
|
||||
|
||||
return Column(children: children);
|
||||
|
@ -304,13 +314,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:
|
||||
|
@ -320,8 +328,7 @@ class _ListPageState extends State<ListPage> {
|
|||
}
|
||||
|
||||
Future<void> _loadTasksForPage(int page) {
|
||||
return Provider.of<ProjectProvider>(context, listen: false)
|
||||
.loadTasks(
|
||||
return Provider.of<ProjectProvider>(context, listen: false).loadTasks(
|
||||
context: context,
|
||||
listId: _project.id,
|
||||
page: page,
|
||||
|
@ -335,7 +342,7 @@ class _ListPageState extends State<ListPage> {
|
|||
onAdd: (title) => _addItem(title, context, bucket),
|
||||
decoration: InputDecoration(
|
||||
labelText:
|
||||
(bucket != null ? '\'${bucket.title}\': ' : '') + 'New Task Name',
|
||||
(bucket != null ? '\'${bucket.title}\': ' : '') + 'New Task Name',
|
||||
hintText: 'eg. Milk',
|
||||
),
|
||||
),
|
||||
|
@ -377,7 +384,6 @@ class _ListPageState extends State<ListPage> {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
openList(BuildContext context, Project project) {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => ChangeNotifierProvider<ProjectProvider>(
|
||||
|
@ -388,4 +394,4 @@ openList(BuildContext context, Project project) {
|
|||
),
|
||||
// ListPage(taskList: list)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,8 +48,8 @@ class _TaskEditPageState extends State<TaskEditPage> {
|
|||
@override
|
||||
void initState() {
|
||||
_repeatAfter = widget.task.repeatAfter;
|
||||
if(_repeatAfterType == null)
|
||||
_repeatAfterType = getRepeatAfterTypeFromDuration(_repeatAfter);
|
||||
if (_repeatAfterType == null)
|
||||
_repeatAfterType = getRepeatAfterTypeFromDuration(_repeatAfter);
|
||||
|
||||
_reminderDates = widget.task.reminderDates;
|
||||
for (var i = 0; i < _reminderDates.length; i++) {
|
||||
|
@ -86,25 +86,30 @@ class _TaskEditPageState extends State<TaskEditPage> {
|
|||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.delete),
|
||||
onPressed: () {showDialog(context: context, builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('Delete Task'),
|
||||
content: Text('Are you sure you want to delete this task?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text('Cancel'),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
TextButton(
|
||||
child: Text('Delete'),
|
||||
onPressed: () {
|
||||
_delete(widget.task.id);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
});},
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('Delete Task'),
|
||||
content: Text(
|
||||
'Are you sure you want to delete this task?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text('Cancel'),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
TextButton(
|
||||
child: Text('Delete'),
|
||||
onPressed: () {
|
||||
_delete(widget.task.id);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -114,7 +119,8 @@ class _TaskEditPageState extends State<TaskEditPage> {
|
|||
key: _formKey,
|
||||
child: ListView(
|
||||
key: _listKey,
|
||||
padding: EdgeInsets.fromLTRB(16, 16, 16, MediaQuery.of(context).size.height / 2),
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
16, 16, 16, MediaQuery.of(context).size.height / 2),
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 10.0),
|
||||
|
@ -182,8 +188,9 @@ class _TaskEditPageState extends State<TaskEditPage> {
|
|||
flex: 2,
|
||||
child: TextFormField(
|
||||
keyboardType: TextInputType.number,
|
||||
initialValue: getRepeatAfterValueFromDuration(
|
||||
_repeatAfter)?.toString(),
|
||||
initialValue:
|
||||
getRepeatAfterValueFromDuration(_repeatAfter)
|
||||
?.toString(),
|
||||
onSaved: (repeatAfter) => _repeatAfter =
|
||||
getDurationFromType(
|
||||
repeatAfter, _repeatAfterType),
|
||||
|
@ -200,8 +207,7 @@ class _TaskEditPageState extends State<TaskEditPage> {
|
|||
isExpanded: true,
|
||||
isDense: true,
|
||||
value: _repeatAfterType ??
|
||||
getRepeatAfterTypeFromDuration(
|
||||
_repeatAfter),
|
||||
getRepeatAfterTypeFromDuration(_repeatAfter),
|
||||
onChanged: (String? newValue) {
|
||||
setState(() {
|
||||
_repeatAfterType = newValue;
|
||||
|
@ -252,7 +258,7 @@ class _TaskEditPageState extends State<TaskEditPage> {
|
|||
_reminderDates.add(DateTime(0));
|
||||
var currentIndex = _reminderDates.length - 1;
|
||||
|
||||
// FIXME: Why does putting this into a row fails?
|
||||
// FIXME: Why does putting this into a row fail?
|
||||
setState(() => _reminderInputs.add(
|
||||
VikunjaDateTimePicker(
|
||||
label: 'Reminder',
|
||||
|
@ -321,9 +327,11 @@ class _TaskEditPageState extends State<TaskEditPage> {
|
|||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
right: 15,
|
||||
left: 2.0 + (IconTheme.of(context).size??0))),
|
||||
left: 2.0 + (IconTheme.of(context).size ?? 0))),
|
||||
Container(
|
||||
width: MediaQuery.of(context).size.width - 80 - ((IconTheme.of(context).size ?? 0) * 2),
|
||||
width: MediaQuery.of(context).size.width -
|
||||
80 -
|
||||
((IconTheme.of(context).size ?? 0) * 2),
|
||||
child: TypeAheadField(
|
||||
builder: (builder, controller, focusnode) {
|
||||
return TextFormField(
|
||||
|
@ -427,18 +435,25 @@ class _TaskEditPageState extends State<TaskEditPage> {
|
|||
trailing: IconButton(
|
||||
icon: Icon(Icons.download),
|
||||
onPressed: () async {
|
||||
String url = VikunjaGlobal.of(context).client.base;
|
||||
url += '/tasks/${widget.task.id}/attachments/${widget.task.attachments[index].id}';
|
||||
String url =
|
||||
VikunjaGlobal.of(context).client.base;
|
||||
url +=
|
||||
'/tasks/${widget.task.id}/attachments/${widget.task.attachments[index].id}';
|
||||
print(url);
|
||||
final taskId = await FlutterDownloader.enqueue(
|
||||
url: url,
|
||||
fileName: widget.task.attachments[index].file.name,
|
||||
headers: VikunjaGlobal.of(context).client.headers, // optional: header send with url (auth token etc)
|
||||
fileName:
|
||||
widget.task.attachments[index].file.name,
|
||||
headers: VikunjaGlobal.of(context)
|
||||
.client
|
||||
.headers, // optional: header send with url (auth token etc)
|
||||
savedDir: '/storage/emulated/0/Download/',
|
||||
showNotification: true, // show download progress in status bar (for Android)
|
||||
openFileFromNotification: true, // click on notification to open downloaded file (for Android)
|
||||
showNotification:
|
||||
true, // show download progress in status bar (for Android)
|
||||
openFileFromNotification:
|
||||
true, // click on notification to open downloaded file (for Android)
|
||||
);
|
||||
if(taskId == null) return;
|
||||
if (taskId == null) return;
|
||||
FlutterDownloader.open(taskId: taskId);
|
||||
},
|
||||
),
|
||||
|
@ -594,8 +609,6 @@ class _TaskEditPageState extends State<TaskEditPage> {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
_onColorEdit() {
|
||||
_pickerColor = _resetColor || (_color ?? widget.task.color) == null
|
||||
? Colors.black
|
|
@ -2,7 +2,6 @@ import 'package:flutter/cupertino.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:vikunja_app/global.dart';
|
||||
import 'package:vikunja_app/models/list.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import '../main.dart';
|
||||
|
@ -10,7 +9,6 @@ import '../models/project.dart';
|
|||
import '../models/user.dart';
|
||||
import '../service/services.dart';
|
||||
|
||||
|
||||
class SettingsPage extends StatefulWidget {
|
||||
@override
|
||||
State<StatefulWidget> createState() => SettingsPageState();
|
||||
|
@ -27,7 +25,6 @@ class SettingsPageState extends State<SettingsPage> {
|
|||
FlutterThemeMode? themeMode;
|
||||
User? currentUser;
|
||||
|
||||
|
||||
void init() {
|
||||
durationTextController = TextEditingController();
|
||||
|
||||
|
@ -53,20 +50,21 @@ class SettingsPageState extends State<SettingsPage> {
|
|||
.getCurrentVersionTag()
|
||||
.then((value) => setState(() => versionTag = value));
|
||||
|
||||
VikunjaGlobal.of(context).settingsManager.getWorkmanagerDuration().then(
|
||||
(value) => setState(
|
||||
() => durationTextController.text = (value.inMinutes.toString())));
|
||||
|
||||
VikunjaGlobal.of(context)
|
||||
.settingsManager
|
||||
.getWorkmanagerDuration()
|
||||
.then((value) => setState(() => durationTextController.text = (value.inMinutes.toString())));
|
||||
|
||||
VikunjaGlobal.of(context).settingsManager.getThemeMode().then((value) => setState(() => themeMode = value));
|
||||
.getThemeMode()
|
||||
.then((value) => setState(() => themeMode = value));
|
||||
|
||||
VikunjaGlobal.of(context).newUserService?.getCurrentUser().then((value) => {
|
||||
setState(() {
|
||||
currentUser = value!;
|
||||
defaultProject = value.settings?.default_project_id;
|
||||
} ),
|
||||
});
|
||||
|
||||
setState(() {
|
||||
currentUser = value!;
|
||||
defaultProject = value.settings?.default_project_id;
|
||||
}),
|
||||
});
|
||||
|
||||
initialized = true;
|
||||
}
|
||||
|
@ -77,18 +75,22 @@ class SettingsPageState extends State<SettingsPage> {
|
|||
|
||||
if (!initialized) init();
|
||||
return new Scaffold(
|
||||
appBar: AppBar(title: Text("Settings"),),
|
||||
|
||||
appBar: AppBar(
|
||||
title: Text("Settings"),
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
UserAccountsDrawerHeader(
|
||||
accountName: currentUser != null ? Text(currentUser!.username) : null,
|
||||
accountName:
|
||||
currentUser != null ? Text(currentUser!.username) : null,
|
||||
accountEmail: currentUser != null ? Text(currentUser!.name) : null,
|
||||
currentAccountPicture: currentUser == null
|
||||
? null
|
||||
: CircleAvatar(
|
||||
backgroundImage: (currentUser?.username != "") ? NetworkImage(currentUser!.avatarUrl(context)) : null,
|
||||
),
|
||||
backgroundImage: (currentUser?.username != "")
|
||||
? NetworkImage(currentUser!.avatarUrl(context))
|
||||
: null,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage("assets/graphics/hypnotize.png"),
|
||||
|
@ -111,11 +113,17 @@ class SettingsPageState extends State<SettingsPage> {
|
|||
child: Text(e.title), value: e.id))
|
||||
.toList()
|
||||
],
|
||||
value: projectList?.firstWhereOrNull((element) => element.id == defaultProject) != null ? defaultProject : null,
|
||||
value: projectList?.firstWhereOrNull(
|
||||
(element) => element.id == defaultProject) !=
|
||||
null
|
||||
? defaultProject
|
||||
: null,
|
||||
onChanged: (int? value) {
|
||||
setState(() => defaultProject = value);
|
||||
global.newUserService?.setCurrentUserSettings(
|
||||
currentUser!.settings!.copyWith(default_project_id: value)).then((value) => currentUser!.settings = value);
|
||||
global.newUserService
|
||||
?.setCurrentUserSettings(currentUser!.settings!
|
||||
.copyWith(default_project_id: value))
|
||||
.then((value) => currentUser!.settings = value);
|
||||
//VikunjaGlobal.of(context).userManager.setDefaultList(value);
|
||||
},
|
||||
),
|
||||
|
@ -151,9 +159,7 @@ class SettingsPageState extends State<SettingsPage> {
|
|||
],
|
||||
value: themeMode,
|
||||
onChanged: (FlutterThemeMode? value) {
|
||||
VikunjaGlobal.of(context)
|
||||
.settingsManager
|
||||
.setThemeMode(value!);
|
||||
VikunjaGlobal.of(context).settingsManager.setThemeMode(value!);
|
||||
setState(() => themeMode = value);
|
||||
updateTheme.value = true;
|
||||
},
|
||||
|
@ -166,27 +172,30 @@ class SettingsPageState extends State<SettingsPage> {
|
|||
value: ignoreCertificates,
|
||||
onChanged: (value) {
|
||||
setState(() => ignoreCertificates = value);
|
||||
VikunjaGlobal.of(context).client.reload_ignore_certs(value);
|
||||
VikunjaGlobal.of(context).client.reloadIgnoreCerts(value);
|
||||
})
|
||||
: ListTile(title: Text("...")),
|
||||
Divider(),
|
||||
Padding(padding: EdgeInsets.only(left: 15, right: 15),
|
||||
Divider(),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 15, right: 15),
|
||||
child: Row(children: [
|
||||
Flexible(
|
||||
child: TextField(
|
||||
controller: durationTextController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Background Refresh Interval (minutes): ',
|
||||
helperText: 'Minimum: 15, Set limit of 0 for no refresh',
|
||||
),
|
||||
)),
|
||||
TextButton(
|
||||
onPressed: () => VikunjaGlobal.of(context)
|
||||
.settingsManager
|
||||
.setWorkmanagerDuration(Duration(
|
||||
minutes: int.parse(durationTextController.text))).then((value) => VikunjaGlobal.of(context).updateWorkmanagerDuration()),
|
||||
child: Text("Save")),
|
||||
])),
|
||||
Flexible(
|
||||
child: TextField(
|
||||
controller: durationTextController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Background Refresh Interval (minutes): ',
|
||||
helperText: 'Minimum: 15, Set limit of 0 for no refresh',
|
||||
),
|
||||
)),
|
||||
TextButton(
|
||||
onPressed: () => VikunjaGlobal.of(context)
|
||||
.settingsManager
|
||||
.setWorkmanagerDuration(Duration(
|
||||
minutes: int.parse(durationTextController.text)))
|
||||
.then((value) => VikunjaGlobal.of(context)
|
||||
.updateWorkmanagerDuration()),
|
||||
child: Text("Save")),
|
||||
])),
|
||||
Divider(),
|
||||
getVersionNotifications != null
|
||||
? CheckboxListTile(
|
||||
|
|
|
@ -33,11 +33,10 @@ class _LoginPageState extends State<LoginPage> {
|
|||
|
||||
final _serverSuggestionController = SuggestionsController();
|
||||
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Future.delayed(Duration.zero, () async{
|
||||
Future.delayed(Duration.zero, () async {
|
||||
if (VikunjaGlobal.of(context).expired) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text("Login has expired. Please reenter your details!")));
|
||||
|
@ -48,10 +47,16 @@ class _LoginPageState extends State<LoginPage> {
|
|||
});
|
||||
}
|
||||
final client = VikunjaGlobal.of(context).client;
|
||||
await VikunjaGlobal.of(context).settingsManager.getIgnoreCertificates().then(
|
||||
(value) => setState(() => client.ignoreCertificates = value == "1"));
|
||||
await VikunjaGlobal.of(context)
|
||||
.settingsManager
|
||||
.getIgnoreCertificates()
|
||||
.then((value) =>
|
||||
setState(() => client.ignoreCertificates = value == "1"));
|
||||
|
||||
await VikunjaGlobal.of(context).settingsManager.getPastServers().then((value) {
|
||||
await VikunjaGlobal.of(context)
|
||||
.settingsManager
|
||||
.getPastServers()
|
||||
.then((value) {
|
||||
print(value);
|
||||
if (value != null) setState(() => pastServers = value);
|
||||
});
|
||||
|
@ -101,11 +106,11 @@ class _LoginPageState extends State<LoginPage> {
|
|||
enabled: !_loading,
|
||||
validator: (address) {
|
||||
return (isUrl(address) ||
|
||||
address != null ||
|
||||
address!.isEmpty)
|
||||
? null
|
||||
: 'Invalid URL';
|
||||
},
|
||||
address != null ||
|
||||
address!.isEmpty)
|
||||
? null
|
||||
: 'Invalid URL';
|
||||
},
|
||||
decoration: new InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'Server Address'),
|
||||
|
@ -120,34 +125,42 @@ class _LoginPageState extends State<LoginPage> {
|
|||
),*/
|
||||
onSelected: (suggestion) {
|
||||
_serverController.text = suggestion;
|
||||
setState(() => _serverController.text = suggestion);
|
||||
setState(
|
||||
() => _serverController.text = suggestion);
|
||||
},
|
||||
itemBuilder: (BuildContext context, Object? itemData) {
|
||||
itemBuilder:
|
||||
(BuildContext context, Object? itemData) {
|
||||
return Card(
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(10),
|
||||
child:
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(itemData.toString()),
|
||||
IconButton(onPressed: () {
|
||||
setState(() {
|
||||
pastServers.remove(itemData.toString());
|
||||
//_serverSuggestionController.suggestionsBox?.close();
|
||||
VikunjaGlobal.of(context).settingsManager.setPastServers(pastServers);
|
||||
|
||||
});
|
||||
}, icon: Icon(Icons.clear))
|
||||
],
|
||||
))
|
||||
);
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(itemData.toString()),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
pastServers.remove(
|
||||
itemData.toString());
|
||||
//_serverSuggestionController.suggestionsBox?.close();
|
||||
VikunjaGlobal.of(context)
|
||||
.settingsManager
|
||||
.setPastServers(
|
||||
pastServers);
|
||||
});
|
||||
},
|
||||
icon: Icon(Icons.clear))
|
||||
],
|
||||
)));
|
||||
},
|
||||
suggestionsCallback: (String pattern) {
|
||||
List<String> matches = <String>[];
|
||||
matches.addAll(pastServers);
|
||||
matches.retainWhere((s){
|
||||
return s.toLowerCase().contains(pattern.toLowerCase());
|
||||
matches.retainWhere((s) {
|
||||
return s
|
||||
.toLowerCase()
|
||||
.contains(pattern.toLowerCase());
|
||||
});
|
||||
return matches;
|
||||
},
|
||||
|
@ -246,20 +259,18 @@ class _LoginPageState extends State<LoginPage> {
|
|||
}
|
||||
},
|
||||
child: VikunjaButtonText("Login with Frontend"))),
|
||||
CheckboxListTile(
|
||||
title: Text("Ignore Certificates"),
|
||||
value: client.ignoreCertificates,
|
||||
onChanged: (value) {
|
||||
setState(() =>
|
||||
client.reload_ignore_certs(value ?? false));
|
||||
VikunjaGlobal.of(context)
|
||||
.settingsManager
|
||||
.setIgnoreCertificates(value ?? false);
|
||||
VikunjaGlobal.of(context)
|
||||
.client
|
||||
.ignoreCertificates = value ?? false;
|
||||
})
|
||||
|
||||
CheckboxListTile(
|
||||
title: Text("Ignore Certificates"),
|
||||
value: client.ignoreCertificates,
|
||||
onChanged: (value) {
|
||||
setState(
|
||||
() => client.reloadIgnoreCerts(value ?? false));
|
||||
VikunjaGlobal.of(context)
|
||||
.settingsManager
|
||||
.setIgnoreCertificates(value ?? false);
|
||||
VikunjaGlobal.of(context).client.ignoreCertificates =
|
||||
value ?? false;
|
||||
})
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -276,7 +287,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||
String _password = _passwordController.text;
|
||||
if (_server.isEmpty) return;
|
||||
|
||||
if(!pastServers.contains(_server)) pastServers.add(_server);
|
||||
if (!pastServers.contains(_server)) pastServers.add(_server);
|
||||
await VikunjaGlobal.of(context).settingsManager.setPastServers(pastServers);
|
||||
|
||||
setState(() => _loading = true);
|
||||
|
@ -285,8 +296,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||
vGlobal.client.showSnackBar = false;
|
||||
vGlobal.client.configure(base: _server);
|
||||
Server? info = await vGlobal.serverService.getInfo();
|
||||
if (info == null)
|
||||
throw Exception("Getting server info failed");
|
||||
if (info == null) throw Exception("Getting server info failed");
|
||||
|
||||
UserTokenPair newUser;
|
||||
|
||||
|
@ -297,27 +307,27 @@ class _LoginPageState extends State<LoginPage> {
|
|||
TextEditingController totpController = TextEditingController();
|
||||
bool dismissed = true;
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) => new AlertDialog(
|
||||
title: Text("Enter One Time Passcode"),
|
||||
content: TextField(
|
||||
controller: totpController,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: <TextInputFormatter>[
|
||||
FilteringTextInputFormatter.digitsOnly
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
dismissed = false;
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text("Login"))
|
||||
],
|
||||
),
|
||||
context: context,
|
||||
builder: (context) => new AlertDialog(
|
||||
title: Text("Enter One Time Passcode"),
|
||||
content: TextField(
|
||||
controller: totpController,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: <TextInputFormatter>[
|
||||
FilteringTextInputFormatter.digitsOnly
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
dismissed = false;
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text("Login"))
|
||||
],
|
||||
),
|
||||
);
|
||||
if(!dismissed) {
|
||||
if (!dismissed) {
|
||||
newUser = await vGlobal.newUserService!.login(_username, _password,
|
||||
rememberMe: this._rememberMe, totp: totpController.text);
|
||||
} else {
|
||||
|
|
|
@ -16,27 +16,25 @@ class LoginWithWebView extends StatefulWidget {
|
|||
}
|
||||
|
||||
class LoginWithWebViewState extends State<LoginWithWebView> {
|
||||
|
||||
WebViewWidget? webView;
|
||||
late WebViewController webViewController;
|
||||
bool destroyed = false;
|
||||
|
||||
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
webViewController = WebViewController()
|
||||
..clearLocalStorage()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setUserAgent("Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Mobile Safari/537.36")
|
||||
..setUserAgent(
|
||||
"Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Mobile Safari/537.36")
|
||||
..setNavigationDelegate(NavigationDelegate(
|
||||
onPageFinished: (value) => _handlePageFinished(value),
|
||||
))
|
||||
|
||||
..loadRequest(Uri.parse(widget.frontEndUrl)).then((value) => {
|
||||
webViewController!.runJavaScript("localStorage.clear(); location.href=location.href;")
|
||||
});
|
||||
webViewController!.runJavaScript(
|
||||
"localStorage.clear(); location.href=location.href;")
|
||||
});
|
||||
|
||||
/*
|
||||
webView = WebViewWidget(
|
||||
|
@ -50,47 +48,52 @@ class LoginWithWebViewState extends State<LoginWithWebView> {
|
|||
},
|
||||
);
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(child: Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: WebViewWidget(controller: webViewController,)
|
||||
),
|
||||
onWillPop: () async {
|
||||
String? currentUrl = await webViewController?.currentUrl();
|
||||
if (currentUrl != null) {
|
||||
bool hasPopped = await _handlePageFinished(currentUrl);
|
||||
return Future.value(!hasPopped);
|
||||
}
|
||||
return Future.value(false);
|
||||
},);
|
||||
return WillPopScope(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: WebViewWidget(
|
||||
controller: webViewController,
|
||||
)),
|
||||
onWillPop: () async {
|
||||
String? currentUrl = await webViewController?.currentUrl();
|
||||
if (currentUrl != null) {
|
||||
bool hasPopped = await _handlePageFinished(currentUrl);
|
||||
return Future.value(!hasPopped);
|
||||
}
|
||||
return Future.value(false);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> _handlePageFinished(String pageLocation) async {
|
||||
log("handlePageFinished");
|
||||
if(webViewController != null) {
|
||||
if (webViewController != null) {
|
||||
String localStorage = (await webViewController!
|
||||
.runJavaScriptReturningResult("JSON.stringify(localStorage);")).toString();
|
||||
.runJavaScriptReturningResult("JSON.stringify(localStorage);"))
|
||||
.toString();
|
||||
|
||||
String apiUrl = (await webViewController!.runJavaScriptReturningResult("API_URL")).toString();
|
||||
String token = (await webViewController!.runJavaScriptReturningResult("localStorage['token']")).toString();
|
||||
String apiUrl =
|
||||
(await webViewController!.runJavaScriptReturningResult("API_URL"))
|
||||
.toString();
|
||||
String token = (await webViewController!
|
||||
.runJavaScriptReturningResult("localStorage['token']"))
|
||||
.toString();
|
||||
if (localStorage.toString() != "{}") {
|
||||
apiUrl = apiUrl.replaceAll("\"", "");
|
||||
token = token.replaceAll("\"", "");
|
||||
if(!apiUrl.startsWith("http")) {
|
||||
if(pageLocation.endsWith("/"))
|
||||
pageLocation = pageLocation.substring(0,pageLocation.length-1);
|
||||
if (!apiUrl.startsWith("http")) {
|
||||
if (pageLocation.endsWith("/"))
|
||||
pageLocation = pageLocation.substring(0, pageLocation.length - 1);
|
||||
apiUrl = pageLocation + apiUrl;
|
||||
}
|
||||
|
||||
if (apiUrl != "null" && token != "null") {
|
||||
BaseTokenPair baseTokenPair = BaseTokenPair(
|
||||
apiUrl, token);
|
||||
if(destroyed)
|
||||
return true;
|
||||
BaseTokenPair baseTokenPair = BaseTokenPair(apiUrl, token);
|
||||
if (destroyed) return true;
|
||||
destroyed = true;
|
||||
print("pop now");
|
||||
Navigator.pop(context, baseTokenPair);
|
||||
|
@ -101,5 +104,4 @@ class LoginWithWebViewState extends State<LoginWithWebView> {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -127,10 +127,9 @@ class _RegisterPageState extends State<RegisterPage> {
|
|||
setState(() => _loading = true);
|
||||
try {
|
||||
var vGlobal = VikunjaGlobal.of(context);
|
||||
var newUserLoggedIn = await vGlobal
|
||||
.newUserService
|
||||
?.register(_username!, _email, _password);
|
||||
if(newUserLoggedIn != null)
|
||||
var newUserLoggedIn =
|
||||
await vGlobal.newUserService?.register(_username!, _email, _password);
|
||||
if (newUserLoggedIn != null)
|
||||
vGlobal.changeUser(newUserLoggedIn.user!,
|
||||
token: newUserLoggedIn.token, base: _server!);
|
||||
} catch (ex) {
|
||||
|
|
|
@ -1,204 +1,15 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:vikunja_app/api/response.dart';
|
||||
import 'package:vikunja_app/models/list.dart';
|
||||
import 'package:vikunja_app/models/namespace.dart';
|
||||
import 'package:vikunja_app/models/task.dart';
|
||||
import 'package:vikunja_app/models/user.dart';
|
||||
import 'package:vikunja_app/service/services.dart';
|
||||
|
||||
// Data for mocked services
|
||||
var _users = {1: User(id: 1, username: 'test1')};
|
||||
|
||||
var _namespaces = {
|
||||
1: Namespace(
|
||||
id: 1,
|
||||
title: 'Test 1',
|
||||
created: DateTime.now(),
|
||||
updated: DateTime.now(),
|
||||
description: 'A namespace for testing purposes',
|
||||
owner: _users[1]!,
|
||||
)
|
||||
};
|
||||
|
||||
var _nsLists = {
|
||||
1: [1]
|
||||
};
|
||||
|
||||
var _lists = {
|
||||
1: TaskList(
|
||||
id: 1,
|
||||
title: 'List 1',
|
||||
tasks: _tasks.values.toList(),
|
||||
owner: _users[1]!,
|
||||
description: 'A nice list',
|
||||
created: DateTime.now(),
|
||||
updated: DateTime.now(),
|
||||
namespaceId: 1)
|
||||
};
|
||||
|
||||
var _tasks = {
|
||||
1: Task(
|
||||
id: 1,
|
||||
title: 'Task 1',
|
||||
createdBy: _users[1]!,
|
||||
updated: DateTime.now(),
|
||||
created: DateTime.now(),
|
||||
description: 'A descriptive task',
|
||||
done: false,
|
||||
projectId: 1,
|
||||
)
|
||||
};
|
||||
|
||||
// Mocked services
|
||||
|
||||
class MockedNamespaceService implements NamespaceService {
|
||||
@override
|
||||
Future<Namespace> create(Namespace ns) {
|
||||
_namespaces[ns.id] = ns;
|
||||
return Future.value(ns);
|
||||
}
|
||||
|
||||
@override
|
||||
Future delete(int namespaceId) {
|
||||
_namespaces.remove(namespaceId);
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Namespace> get(int namespaceId) {
|
||||
return Future.value(_namespaces[namespaceId]);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Namespace>> getAll() {
|
||||
return Future.value(_namespaces.values.toList());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Namespace> update(Namespace ns) {
|
||||
if (!_namespaces.containsKey(ns.id))
|
||||
throw Exception('Namespace ${ns.id} does not exsists');
|
||||
return create(ns);
|
||||
}
|
||||
}
|
||||
|
||||
class MockedListService implements ListService {
|
||||
@override
|
||||
Future<TaskList> create(namespaceId, TaskList tl) {
|
||||
_nsLists[namespaceId]?.add(tl.id);
|
||||
return Future.value(_lists[tl.id] = tl);
|
||||
}
|
||||
|
||||
@override
|
||||
Future delete(int listId) {
|
||||
_lists.remove(listId);
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<TaskList> get(int listId) {
|
||||
return Future.value(_lists[listId]);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<TaskList>> getAll() {
|
||||
return Future.value(_lists.values.toList());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<TaskList>> getByNamespace(int namespaceId) {
|
||||
return Future.value(
|
||||
_nsLists[namespaceId]!.map((listId) => _lists[listId]!).toList());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<TaskList> update(TaskList tl) {
|
||||
if (!_lists.containsKey(tl))
|
||||
throw Exception('TaskList ${tl.id} does not exists');
|
||||
return Future.value(_lists[tl.id] = tl);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> getDisplayDoneTasks(int listId) {
|
||||
// TODO: implement getDisplayDoneTasks
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
void setDisplayDoneTasks(int listId, String value) {
|
||||
// TODO: implement setDisplayDoneTasks
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> getDefaultList() {
|
||||
// TODO: implement getDefaultList
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
void setDefaultList(int? listId) {
|
||||
// TODO: implement setDefaultList
|
||||
}
|
||||
}
|
||||
|
||||
class MockedTaskService implements TaskService {
|
||||
@override
|
||||
Future delete(int taskId) {
|
||||
_lists.forEach(
|
||||
(_, list) => list.tasks.removeWhere((task) => task.id == taskId));
|
||||
_tasks.remove(taskId);
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Task> update(Task task) {
|
||||
_lists.forEach((_, list) {
|
||||
if (list.tasks.where((t) => t.id == task.id).length > 0) {
|
||||
list.tasks.removeWhere((t) => t.id == task.id);
|
||||
list.tasks.add(task);
|
||||
}
|
||||
});
|
||||
return Future.value(_tasks[task.id] = task);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Task> add(int listId, Task task) {
|
||||
var id = _tasks.keys.last + 1;
|
||||
_tasks[id] = task;
|
||||
_lists[listId]!.tasks.add(task);
|
||||
return Future.value(task);
|
||||
}
|
||||
|
||||
@override
|
||||
int get maxPages => 1;
|
||||
Future<Task> get(int taskId) {
|
||||
// TODO: implement get
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Task>> getByOptions(TaskServiceOptions options) {
|
||||
// TODO: implement getByOptions
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Task>> getAll() {
|
||||
// TODO: implement getAll
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response?> getAllByProject(int projectId, [Map<String, List<String>>? queryParameters]) {
|
||||
// TODO: implement getAllByProject
|
||||
return Future.value(new Response(_tasks.values.toList(), 200, {}));
|
||||
}
|
||||
}
|
||||
|
||||
class MockedUserService implements UserService {
|
||||
@override
|
||||
Future<UserTokenPair> login(String username, password, {bool rememberMe = false, String? totp}) {
|
||||
Future<UserTokenPair> login(String username, password,
|
||||
{bool rememberMe = false, String? totp}) {
|
||||
return Future.value(UserTokenPair(_users[1]!, 'abcdefg'));
|
||||
}
|
||||
|
||||
|
@ -218,5 +29,9 @@ class MockedUserService implements UserService {
|
|||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Future<String?> getToken() {
|
||||
// TODO: implement getToken
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:vikunja_app/api/response.dart';
|
||||
import 'package:vikunja_app/models/label.dart';
|
||||
import 'package:vikunja_app/models/labelTask.dart';
|
||||
import 'package:vikunja_app/models/list.dart';
|
||||
import 'package:vikunja_app/models/namespace.dart';
|
||||
import 'package:vikunja_app/models/task.dart';
|
||||
import 'package:vikunja_app/models/user.dart';
|
||||
import 'package:vikunja_app/models/bucket.dart';
|
||||
|
@ -60,9 +57,9 @@ class TaskServiceOption<T> {
|
|||
dynamic defValue;
|
||||
|
||||
TaskServiceOption(this.name, dynamic input_values) {
|
||||
if(input_values is List<String>) {
|
||||
if (input_values is List<String>) {
|
||||
valueList = input_values;
|
||||
} else if(input_values is String) {
|
||||
} else if (input_values is String) {
|
||||
value = input_values;
|
||||
}
|
||||
}
|
||||
|
@ -82,20 +79,14 @@ class TaskServiceOption<T> {
|
|||
|
||||
final List<TaskServiceOption> defaultOptions = [
|
||||
TaskServiceOption<TaskServiceOptionSortBy>("sort_by",
|
||||
[TaskServiceOptionSortBy.due_date,
|
||||
TaskServiceOptionSortBy.id]),
|
||||
[TaskServiceOptionSortBy.due_date, TaskServiceOptionSortBy.id]),
|
||||
TaskServiceOption<TaskServiceOptionOrderBy>(
|
||||
"order_by", TaskServiceOptionOrderBy.asc),
|
||||
TaskServiceOption<TaskServiceOptionFilterBy>("filter_by", [
|
||||
TaskServiceOptionFilterBy.done,
|
||||
TaskServiceOptionFilterBy.due_date
|
||||
]),
|
||||
TaskServiceOption<TaskServiceOptionFilterValue>("filter_value", [
|
||||
TaskServiceOptionFilterValue.enum_false,
|
||||
'1970-01-01T00:00:00.000Z'
|
||||
]),
|
||||
TaskServiceOption<TaskServiceOptionFilterComparator>(
|
||||
"filter_comparator", [
|
||||
TaskServiceOption<TaskServiceOptionFilterBy>("filter_by",
|
||||
[TaskServiceOptionFilterBy.done, TaskServiceOptionFilterBy.due_date]),
|
||||
TaskServiceOption<TaskServiceOptionFilterValue>("filter_value",
|
||||
[TaskServiceOptionFilterValue.enum_false, '1970-01-01T00:00:00.000Z']),
|
||||
TaskServiceOption<TaskServiceOptionFilterComparator>("filter_comparator", [
|
||||
TaskServiceOptionFilterComparator.equals,
|
||||
TaskServiceOptionFilterComparator.greater
|
||||
]),
|
||||
|
@ -106,13 +97,14 @@ final List<TaskServiceOption> defaultOptions = [
|
|||
class TaskServiceOptions {
|
||||
List<TaskServiceOption> options = [];
|
||||
|
||||
TaskServiceOptions({List<TaskServiceOption>? newOptions, bool clearOther = false}) {
|
||||
if(!clearOther)
|
||||
options = new List<TaskServiceOption>.from(defaultOptions);
|
||||
TaskServiceOptions(
|
||||
{List<TaskServiceOption>? newOptions, bool clearOther = false}) {
|
||||
if (!clearOther) options = new List<TaskServiceOption>.from(defaultOptions);
|
||||
if (newOptions != null) {
|
||||
for (TaskServiceOption custom_option in newOptions) {
|
||||
int index = options.indexWhere((element) => element.name == custom_option.name);
|
||||
if(index > -1) {
|
||||
int index =
|
||||
options.indexWhere((element) => element.name == custom_option.name);
|
||||
if (index > -1) {
|
||||
options.removeAt(index);
|
||||
} else {
|
||||
index = options.length;
|
||||
|
@ -122,13 +114,12 @@ class TaskServiceOptions {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
Map<String, List<String>> getOptions() {
|
||||
Map<String, List<String>> queryparams = {};
|
||||
for (TaskServiceOption option in options) {
|
||||
dynamic value = option.getValue();
|
||||
if (value is List) {
|
||||
queryparams[option.name+"[]"] = value as List<String>;
|
||||
queryparams[option.name + "[]"] = value as List<String>;
|
||||
//for (dynamic valueEntry in value) {
|
||||
// result += '&' + option.name + '[]=' + valueEntry;
|
||||
//}
|
||||
|
@ -152,48 +143,12 @@ abstract class ProjectService {
|
|||
Future<Project?> update(Project p);
|
||||
Future delete(int projectId);
|
||||
|
||||
|
||||
Future<String?> getDisplayDoneTasks(int listId);
|
||||
void setDisplayDoneTasks(int listId, String value);
|
||||
//Future<String?> getDefaultList();
|
||||
//void setDefaultList(int? listId);
|
||||
}
|
||||
|
||||
|
||||
abstract class NamespaceService {
|
||||
Future<List<Namespace>?> getAll();
|
||||
|
||||
Future<Namespace?> get(int namespaceId);
|
||||
|
||||
Future<Namespace?> create(Namespace ns);
|
||||
|
||||
Future<Namespace?> update(Namespace ns);
|
||||
|
||||
Future delete(int namespaceId);
|
||||
}
|
||||
|
||||
abstract class ListService {
|
||||
Future<List<TaskList>?> getAll();
|
||||
|
||||
Future<TaskList?> get(int listId);
|
||||
|
||||
Future<List<TaskList>?> getByNamespace(int namespaceId);
|
||||
|
||||
Future<TaskList?> create(int namespaceId, TaskList tl);
|
||||
|
||||
Future<TaskList?> update(TaskList tl);
|
||||
|
||||
Future delete(int listId);
|
||||
|
||||
Future<String?> getDisplayDoneTasks(int listId);
|
||||
|
||||
void setDisplayDoneTasks(int listId, String value);
|
||||
|
||||
Future<String?> getDefaultList();
|
||||
|
||||
//void setDefaultList(int? listId);
|
||||
}
|
||||
|
||||
abstract class TaskService {
|
||||
Future<Task?> get(int taskId);
|
||||
|
||||
|
@ -236,6 +191,8 @@ abstract class UserService {
|
|||
|
||||
Future<User?> getCurrentUser();
|
||||
Future<UserSettings?> setCurrentUserSettings(UserSettings userSettings);
|
||||
|
||||
Future<String?> getToken();
|
||||
}
|
||||
|
||||
abstract class LabelService {
|
||||
|
@ -288,7 +245,6 @@ class SettingsManager {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
SettingsManager(this._storage) {
|
||||
applydefaults();
|
||||
}
|
||||
|
@ -296,35 +252,45 @@ class SettingsManager {
|
|||
Future<String?> getIgnoreCertificates() {
|
||||
return _storage.read(key: "ignore-certificates");
|
||||
}
|
||||
|
||||
void setIgnoreCertificates(bool value) {
|
||||
_storage.write(key: "ignore-certificates", value: value ? "1" : "0");
|
||||
}
|
||||
|
||||
Future<bool> getLandingPageOnlyDueDateTasks() {
|
||||
return _storage.read(key: "landing-page-due-date-tasks").then((value) => value == "1");
|
||||
}
|
||||
Future<void> setLandingPageOnlyDueDateTasks(bool value) {
|
||||
return _storage.write(key: "landing-page-due-date-tasks", value: value ? "1" : "0");
|
||||
return _storage
|
||||
.read(key: "landing-page-due-date-tasks")
|
||||
.then((value) => value == "1");
|
||||
}
|
||||
|
||||
Future<void> setLandingPageOnlyDueDateTasks(bool value) {
|
||||
return _storage.write(
|
||||
key: "landing-page-due-date-tasks", value: value ? "1" : "0");
|
||||
}
|
||||
|
||||
Future<String?> getVersionNotifications() {
|
||||
return _storage.read(key: "get-version-notifications");
|
||||
}
|
||||
|
||||
void setVersionNotifications(bool value) {
|
||||
_storage.write(key: "get-version-notifications", value: value ? "1" : "0");
|
||||
}
|
||||
|
||||
|
||||
Future<Duration> getWorkmanagerDuration() {
|
||||
return _storage.read(key: "workmanager-duration").then((value) => Duration(minutes: int.parse(value ?? "0")));
|
||||
}
|
||||
Future<void> setWorkmanagerDuration(Duration duration) {
|
||||
return _storage.write(key: "workmanager-duration", value: duration.inMinutes.toString());
|
||||
return _storage
|
||||
.read(key: "workmanager-duration")
|
||||
.then((value) => Duration(minutes: int.parse(value ?? "0")));
|
||||
}
|
||||
|
||||
Future<List<String>?> getPastServers() {
|
||||
return _storage.read(key: "recent-servers").then((value) => (jsonDecode(value!) as List<dynamic>).cast<String>());
|
||||
Future<void> setWorkmanagerDuration(Duration duration) {
|
||||
return _storage.write(
|
||||
key: "workmanager-duration", value: duration.inMinutes.toString());
|
||||
}
|
||||
|
||||
Future<List<String>?> getPastServers() async {
|
||||
String jsonString = await _storage.read(key: "recent-servers") ?? "[]";
|
||||
List<dynamic> server = jsonDecode(jsonString);
|
||||
return server.map((e) => e as String).toList();
|
||||
}
|
||||
|
||||
Future<void> setPastServers(List<String>? server) {
|
||||
|
@ -335,18 +301,17 @@ class SettingsManager {
|
|||
|
||||
Future<FlutterThemeMode> getThemeMode() async {
|
||||
String? theme_mode = await _storage.read(key: "theme_mode");
|
||||
if(theme_mode == null)
|
||||
setThemeMode(FlutterThemeMode.system);
|
||||
switch(theme_mode) {
|
||||
if (theme_mode == null) setThemeMode(FlutterThemeMode.system);
|
||||
switch (theme_mode) {
|
||||
case "system":
|
||||
return FlutterThemeMode.system;
|
||||
case "light":
|
||||
return FlutterThemeMode.light;
|
||||
case "dark":
|
||||
return FlutterThemeMode.dark;
|
||||
case "materialYouLight":
|
||||
case "materialYouLight":
|
||||
return FlutterThemeMode.materialYouLight;
|
||||
case "materialYouDark":
|
||||
case "materialYouDark":
|
||||
return FlutterThemeMode.materialYouDark;
|
||||
default:
|
||||
return FlutterThemeMode.system;
|
||||
|
@ -354,9 +319,9 @@ class SettingsManager {
|
|||
}
|
||||
|
||||
Future<void> setThemeMode(FlutterThemeMode newMode) async {
|
||||
await _storage.write(key: "theme_mode", value: newMode.toString().split('.').last);
|
||||
await _storage.write(
|
||||
key: "theme_mode", value: newMode.toString().split('.').last);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
enum FlutterThemeMode {
|
||||
|
@ -365,4 +330,4 @@ enum FlutterThemeMode {
|
|||
dark,
|
||||
materialYouLight,
|
||||
materialYouDark,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:vikunja_app/models/task.dart';
|
||||
import 'package:vikunja_app/models/bucket.dart';
|
||||
|
@ -15,7 +14,6 @@ class ListProvider with ChangeNotifier {
|
|||
List<Task> _tasks = [];
|
||||
List<Bucket> _buckets = [];
|
||||
|
||||
|
||||
bool get taskDragging => _taskDragging;
|
||||
|
||||
set taskDragging(bool value) {
|
||||
|
@ -39,7 +37,6 @@ class ListProvider with ChangeNotifier {
|
|||
|
||||
List<Bucket> get buckets => _buckets;
|
||||
|
||||
|
||||
PageStatus _pageStatus = PageStatus.built;
|
||||
|
||||
PageStatus get pageStatus => _pageStatus;
|
||||
|
@ -50,7 +47,11 @@ class ListProvider with ChangeNotifier {
|
|||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> loadTasks({required BuildContext context, required int listId, int page = 1, bool displayDoneTasks = true}) {
|
||||
Future<void> loadTasks(
|
||||
{required BuildContext context,
|
||||
required int listId,
|
||||
int page = 1,
|
||||
bool displayDoneTasks = true}) {
|
||||
_tasks = [];
|
||||
notifyListeners();
|
||||
|
||||
|
@ -60,7 +61,7 @@ class ListProvider with ChangeNotifier {
|
|||
"page": [page.toString()]
|
||||
};
|
||||
|
||||
if(!displayDoneTasks) {
|
||||
if (!displayDoneTasks) {
|
||||
queryParams.addAll({
|
||||
"filter_by": ["done"],
|
||||
"filter_value": ["false"]
|
||||
|
@ -81,7 +82,8 @@ class ListProvider with ChangeNotifier {
|
|||
});*/
|
||||
}
|
||||
|
||||
Future<void> loadBuckets({required BuildContext context, required int listId, int page = 1}) {
|
||||
Future<void> loadBuckets(
|
||||
{required BuildContext context, required int listId, int page = 1}) {
|
||||
_buckets = [];
|
||||
pageStatus = PageStatus.loading;
|
||||
notifyListeners();
|
||||
|
@ -90,8 +92,11 @@ class ListProvider with ChangeNotifier {
|
|||
"page": [page.toString()]
|
||||
};
|
||||
|
||||
return VikunjaGlobal.of(context).bucketService.getAllByList(listId, queryParams).then((response) {
|
||||
if(response == null) {
|
||||
return VikunjaGlobal.of(context)
|
||||
.bucketService
|
||||
.getAllByList(listId, queryParams)
|
||||
.then((response) {
|
||||
if (response == null) {
|
||||
pageStatus = PageStatus.error;
|
||||
return;
|
||||
}
|
||||
|
@ -105,7 +110,9 @@ class ListProvider with ChangeNotifier {
|
|||
}
|
||||
|
||||
Future<void> addTaskByTitle(
|
||||
{required BuildContext context, required String title, required int listId}) async{
|
||||
{required BuildContext context,
|
||||
required String title,
|
||||
required int listId}) async {
|
||||
final globalState = VikunjaGlobal.of(context);
|
||||
if (globalState.currentUser == null) {
|
||||
return;
|
||||
|
@ -120,13 +127,15 @@ class ListProvider with ChangeNotifier {
|
|||
pageStatus = PageStatus.loading;
|
||||
|
||||
return globalState.taskService.add(listId, newTask).then((task) {
|
||||
if(task != null)
|
||||
_tasks.insert(0, task);
|
||||
if (task != null) _tasks.insert(0, task);
|
||||
pageStatus = PageStatus.success;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> addTask({required BuildContext context, required Task newTask, required int listId}) {
|
||||
Future<void> addTask(
|
||||
{required BuildContext context,
|
||||
required Task newTask,
|
||||
required int listId}) {
|
||||
var globalState = VikunjaGlobal.of(context);
|
||||
if (newTask.bucketId == null) pageStatus = PageStatus.loading;
|
||||
notifyListeners();
|
||||
|
@ -136,107 +145,129 @@ class ListProvider with ChangeNotifier {
|
|||
pageStatus = PageStatus.error;
|
||||
return;
|
||||
}
|
||||
if (_tasks.isNotEmpty)
|
||||
_tasks.insert(0, task);
|
||||
if (_tasks.isNotEmpty) _tasks.insert(0, task);
|
||||
if (_buckets.isNotEmpty) {
|
||||
final bucket = _buckets[_buckets.indexWhere((b) => task.bucketId == b.id)];
|
||||
final bucket =
|
||||
_buckets[_buckets.indexWhere((b) => task.bucketId == b.id)];
|
||||
bucket.tasks.add(task);
|
||||
}
|
||||
pageStatus = PageStatus.success;
|
||||
});
|
||||
}
|
||||
|
||||
Future<Task?> updateTask({required BuildContext context, required Task task}) {
|
||||
Future<Task?> 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;
|
||||
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;
|
||||
}
|
||||
}));
|
||||
if (task.id == t.id) {
|
||||
_buckets[i].tasks[v] = task;
|
||||
}
|
||||
}));
|
||||
notifyListeners();
|
||||
return task;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> addBucket({required BuildContext context, required Bucket newBucket, required int listId}) {
|
||||
Future<void> addBucket(
|
||||
{required BuildContext context,
|
||||
required Bucket newBucket,
|
||||
required int listId}) {
|
||||
notifyListeners();
|
||||
return VikunjaGlobal.of(context).bucketService.add(listId, newBucket)
|
||||
return VikunjaGlobal.of(context)
|
||||
.bucketService
|
||||
.add(listId, newBucket)
|
||||
.then((bucket) {
|
||||
if(bucket == null)
|
||||
return null;
|
||||
_buckets.add(bucket);
|
||||
notifyListeners();
|
||||
});
|
||||
if (bucket == null) return null;
|
||||
_buckets.add(bucket);
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> updateBucket({required BuildContext context, required Bucket bucket}) {
|
||||
return VikunjaGlobal.of(context).bucketService.update(bucket)
|
||||
Future<void> 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<void> 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();
|
||||
});
|
||||
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<void> 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");
|
||||
Future<void> 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<void> 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--;
|
||||
if (sameBucket &&
|
||||
index >
|
||||
_buckets[newBucketIndex].tasks.indexWhere((t) => t.id == task?.id))
|
||||
index--;
|
||||
|
||||
_buckets[_buckets.indexWhere((b) => b.id == task?.bucketId)].tasks.remove(task);
|
||||
_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;
|
||||
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 (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) {
|
||||
|
@ -245,8 +276,12 @@ class ListProvider with ChangeNotifier {
|
|||
_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!));
|
||||
_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();
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ class ProjectProvider with ChangeNotifier {
|
|||
List<Task> _tasks = [];
|
||||
List<Bucket> _buckets = [];
|
||||
|
||||
|
||||
bool get taskDragging => _taskDragging;
|
||||
|
||||
set taskDragging(bool value) {
|
||||
|
@ -38,7 +37,6 @@ class ProjectProvider with ChangeNotifier {
|
|||
|
||||
List<Bucket> get buckets => _buckets;
|
||||
|
||||
|
||||
PageStatus _pageStatus = PageStatus.built;
|
||||
|
||||
PageStatus get pageStatus => _pageStatus;
|
||||
|
@ -49,7 +47,11 @@ class ProjectProvider with ChangeNotifier {
|
|||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> loadTasks({required BuildContext context, required int listId, int page = 1, bool displayDoneTasks = true}) {
|
||||
Future<void> loadTasks(
|
||||
{required BuildContext context,
|
||||
required int listId,
|
||||
int page = 1,
|
||||
bool displayDoneTasks = true}) {
|
||||
_tasks = [];
|
||||
notifyListeners();
|
||||
|
||||
|
@ -59,15 +61,18 @@ class ProjectProvider with ChangeNotifier {
|
|||
"page": [page.toString()]
|
||||
};
|
||||
|
||||
if(!displayDoneTasks) {
|
||||
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) {
|
||||
return VikunjaGlobal.of(context)
|
||||
.taskService
|
||||
.getAllByProject(listId, queryParams)
|
||||
.then((response) {
|
||||
if (response == null) {
|
||||
pageStatus = PageStatus.error;
|
||||
return;
|
||||
}
|
||||
|
@ -79,7 +84,8 @@ class ProjectProvider with ChangeNotifier {
|
|||
});
|
||||
}
|
||||
|
||||
Future<void> loadBuckets({required BuildContext context, required int listId, int page = 1}) {
|
||||
Future<void> loadBuckets(
|
||||
{required BuildContext context, required int listId, int page = 1}) {
|
||||
_buckets = [];
|
||||
pageStatus = PageStatus.loading;
|
||||
notifyListeners();
|
||||
|
@ -88,8 +94,11 @@ class ProjectProvider with ChangeNotifier {
|
|||
"page": [page.toString()]
|
||||
};
|
||||
|
||||
return VikunjaGlobal.of(context).bucketService.getAllByList(listId, queryParams).then((response) {
|
||||
if(response == null) {
|
||||
return VikunjaGlobal.of(context)
|
||||
.bucketService
|
||||
.getAllByList(listId, queryParams)
|
||||
.then((response) {
|
||||
if (response == null) {
|
||||
pageStatus = PageStatus.error;
|
||||
return;
|
||||
}
|
||||
|
@ -103,7 +112,9 @@ class ProjectProvider with ChangeNotifier {
|
|||
}
|
||||
|
||||
Future<void> addTaskByTitle(
|
||||
{required BuildContext context, required String title, required int projectId}) async{
|
||||
{required BuildContext context,
|
||||
required String title,
|
||||
required int projectId}) async {
|
||||
final globalState = VikunjaGlobal.of(context);
|
||||
if (globalState.currentUser == null) {
|
||||
return;
|
||||
|
@ -118,13 +129,15 @@ class ProjectProvider with ChangeNotifier {
|
|||
pageStatus = PageStatus.loading;
|
||||
|
||||
return globalState.taskService.add(projectId, newTask).then((task) {
|
||||
if(task != null)
|
||||
_tasks.insert(0, task);
|
||||
if (task != null) _tasks.insert(0, task);
|
||||
pageStatus = PageStatus.success;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> addTask({required BuildContext context, required Task newTask, required int listId}) {
|
||||
Future<void> addTask(
|
||||
{required BuildContext context,
|
||||
required Task newTask,
|
||||
required int listId}) {
|
||||
var globalState = VikunjaGlobal.of(context);
|
||||
if (newTask.bucketId == null) pageStatus = PageStatus.loading;
|
||||
notifyListeners();
|
||||
|
@ -134,107 +147,129 @@ class ProjectProvider with ChangeNotifier {
|
|||
pageStatus = PageStatus.error;
|
||||
return;
|
||||
}
|
||||
if (_tasks.isNotEmpty)
|
||||
_tasks.insert(0, task);
|
||||
if (_tasks.isNotEmpty) _tasks.insert(0, task);
|
||||
if (_buckets.isNotEmpty) {
|
||||
final bucket = _buckets[_buckets.indexWhere((b) => task.bucketId == b.id)];
|
||||
final bucket =
|
||||
_buckets[_buckets.indexWhere((b) => task.bucketId == b.id)];
|
||||
bucket.tasks.add(task);
|
||||
}
|
||||
pageStatus = PageStatus.success;
|
||||
});
|
||||
}
|
||||
|
||||
Future<Task?> updateTask({required BuildContext context, required Task task}) {
|
||||
Future<Task?> 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;
|
||||
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;
|
||||
}
|
||||
}));
|
||||
if (task.id == t.id) {
|
||||
_buckets[i].tasks[v] = task;
|
||||
}
|
||||
}));
|
||||
notifyListeners();
|
||||
return task;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> addBucket({required BuildContext context, required Bucket newBucket, required int listId}) {
|
||||
Future<void> addBucket(
|
||||
{required BuildContext context,
|
||||
required Bucket newBucket,
|
||||
required int listId}) {
|
||||
notifyListeners();
|
||||
return VikunjaGlobal.of(context).bucketService.add(listId, newBucket)
|
||||
return VikunjaGlobal.of(context)
|
||||
.bucketService
|
||||
.add(listId, newBucket)
|
||||
.then((bucket) {
|
||||
if(bucket == null)
|
||||
return null;
|
||||
if (bucket == null) return null;
|
||||
_buckets.add(bucket);
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> updateBucket({required BuildContext context, required Bucket bucket}) {
|
||||
return VikunjaGlobal.of(context).bucketService.update(bucket)
|
||||
Future<void> updateBucket(
|
||||
{required BuildContext context, required Bucket bucket}) {
|
||||
return VikunjaGlobal.of(context)
|
||||
.bucketService
|
||||
.update(bucket)
|
||||
.then((rBucket) {
|
||||
if(rBucket == null)
|
||||
return null;
|
||||
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<void> deleteBucket({required BuildContext context, required int listId, required int bucketId}) {
|
||||
return VikunjaGlobal.of(context).bucketService.delete(listId, bucketId)
|
||||
Future<void> 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<void> 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");
|
||||
Future<void> 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--;
|
||||
if (sameBucket &&
|
||||
index >
|
||||
_buckets[newBucketIndex].tasks.indexWhere((t) => t.id == task?.id))
|
||||
index--;
|
||||
|
||||
_buckets[_buckets.indexWhere((b) => b.id == task?.bucketId)].tasks.remove(task);
|
||||
_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;
|
||||
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 (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) {
|
||||
|
@ -243,8 +278,12 @@ class ProjectProvider with ChangeNotifier {
|
|||
_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!));
|
||||
_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();
|
||||
}
|
||||
|
|
|
@ -17,11 +17,13 @@ class FancyButton extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ElevatedButton(onPressed: onPressed,
|
||||
return ElevatedButton(
|
||||
onPressed: onPressed,
|
||||
child: SizedBox(
|
||||
width: width,
|
||||
child: Center(child: child),
|
||||
),);
|
||||
),
|
||||
);
|
||||
return Padding(
|
||||
padding: vStandardVerticalPadding,
|
||||
child: Container(
|
||||
|
|
|
@ -14,7 +14,9 @@ class VikunjaButtonText extends StatelessWidget {
|
|||
return Text(text);
|
||||
return Text(
|
||||
text,
|
||||
style: TextStyle(color: Theme.of(context).primaryTextTheme.labelMedium?.color, fontWeight: FontWeight.w600),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryTextTheme.labelMedium?.color,
|
||||
fontWeight: FontWeight.w600),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,4 +30,5 @@ const vStandardVerticalPadding = EdgeInsets.symmetric(vertical: 5.0);
|
|||
const vStandardHorizontalPadding = EdgeInsets.symmetric(horizontal: 5.0);
|
||||
const vStandardPadding = EdgeInsets.symmetric(horizontal: 5.0, vertical: 5.0);
|
||||
|
||||
var vDateFormatLong = DateFormat("EEEE, MMMM d, yyyy 'at' H:mm");
|
||||
var vDateFormatLong = DateFormat("EEEE, MMMM d, yyyy 'at' H:mm");
|
||||
var vDateFormatShort = DateFormat("d MMM yyyy, H:mm");
|
||||
|
|
|
@ -4,13 +4,15 @@ import 'package:flutter/material.dart';
|
|||
import 'package:vikunja_app/theme/constants.dart';
|
||||
|
||||
ThemeData buildVikunjaTheme() => _buildVikunjaTheme(ThemeData.light());
|
||||
ThemeData buildVikunjaDarkTheme() => _buildVikunjaTheme(ThemeData.dark(), isDark: true);
|
||||
ThemeData buildVikunjaDarkTheme() =>
|
||||
_buildVikunjaTheme(ThemeData.dark(), isDark: true);
|
||||
|
||||
ThemeData buildVikunjaMaterialLightTheme() {
|
||||
return ThemeData.light().copyWith(
|
||||
useMaterial3: true,
|
||||
);
|
||||
}
|
||||
|
||||
ThemeData buildVikunjaMaterialDarkTheme() {
|
||||
return ThemeData.dark().copyWith(
|
||||
useMaterial3: true,
|
||||
|
@ -45,11 +47,8 @@ ThemeData _buildVikunjaTheme(ThemeData base, {bool isDark = false}) {
|
|||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
enabledBorder: UnderlineInputBorder(
|
||||
borderSide: const BorderSide(color: Colors.grey, width: 1)
|
||||
),
|
||||
|
||||
borderSide: const BorderSide(color: Colors.grey, width: 1)),
|
||||
),
|
||||
|
||||
dividerTheme: DividerThemeData(
|
||||
color: () {
|
||||
return isDark ? Colors.white10 : Colors.black12;
|
||||
|
@ -60,10 +59,11 @@ ThemeData _buildVikunjaTheme(ThemeData base, {bool isDark = false}) {
|
|||
// Make bottomNavigationBar backgroundColor darker to provide more separation
|
||||
backgroundColor: () {
|
||||
final _hslColor = HSLColor.fromColor(
|
||||
base.bottomNavigationBarTheme.backgroundColor
|
||||
?? base.scaffoldBackgroundColor
|
||||
);
|
||||
return _hslColor.withLightness(max(_hslColor.lightness - 0.03, 0)).toColor();
|
||||
base.bottomNavigationBarTheme.backgroundColor ??
|
||||
base.scaffoldBackgroundColor);
|
||||
return _hslColor
|
||||
.withLightness(max(_hslColor.lightness - 0.03, 0))
|
||||
.toColor();
|
||||
}(),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -18,4 +18,4 @@ double calculateItemPosition({double? positionBefore, double? positionAfter}) {
|
|||
|
||||
// in the middle (positionBefore != null && positionAfter != null)
|
||||
return (positionBefore! + positionAfter!) / 2;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,4 +46,3 @@ CheckboxStatistics getCheckboxStatistics(String text) {
|
|||
checked: checkboxes.checked.length,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,21 +1,20 @@
|
|||
String durationToHumanReadable(Duration dur) {
|
||||
var durString = '';
|
||||
if(dur.inDays.abs() > 1)
|
||||
if (dur.inDays.abs() > 1)
|
||||
durString = dur.inDays.abs().toString() + " days";
|
||||
else if(dur.inDays.abs() == 1)
|
||||
else if (dur.inDays.abs() == 1)
|
||||
durString = dur.inDays.abs().toString() + " day";
|
||||
|
||||
else if(dur.inHours.abs() > 1)
|
||||
else if (dur.inHours.abs() > 1)
|
||||
durString = dur.inHours.abs().toString() + " hours";
|
||||
else if(dur.inHours.abs() == 1)
|
||||
else if (dur.inHours.abs() == 1)
|
||||
durString = dur.inHours.abs().toString() + " hour";
|
||||
|
||||
else if(dur.inMinutes.abs() > 1)
|
||||
else if (dur.inMinutes.abs() > 1)
|
||||
durString = dur.inMinutes.abs().toString() + " minutes";
|
||||
else if(dur.inMinutes.abs() == 1)
|
||||
else if (dur.inMinutes.abs() == 1)
|
||||
durString = dur.inMinutes.abs().toString() + " minute";
|
||||
else durString = "less than a minute";
|
||||
else
|
||||
durString = "less than a minute";
|
||||
|
||||
if (dur.isNegative) return durString + " ago";
|
||||
return "in " + durString;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,8 +31,7 @@ priorityFromString(String? priority) {
|
|||
case 'DO NOW':
|
||||
return 5;
|
||||
default:
|
||||
// unset
|
||||
// unset
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -66,6 +66,6 @@ Duration? getDurationFromType(String? value, String? type) {
|
|||
case 'Years':
|
||||
return Duration(days: val * 365);
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
422
pubspec.lock
422
pubspec.lock
|
@ -5,10 +5,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051
|
||||
sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "64.0.0"
|
||||
version: "67.0.0"
|
||||
after_layout:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -21,10 +21,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893"
|
||||
sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.2.0"
|
||||
version: "6.4.1"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -122,21 +122,21 @@ packages:
|
|||
source: hosted
|
||||
version: "2.0.3"
|
||||
chewie:
|
||||
dependency: "direct main"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: chewie
|
||||
sha256: "745e81e84c6d7f3835f89f85bb49771c0a66099e4caf8f8e9e9a372bc66fb2c1"
|
||||
sha256: "8bc4ac4cf3f316e50a25958c0f5eb9bb12cf7e8308bb1d74a43b230da2cfc144"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
version: "1.7.5"
|
||||
cli_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cli_util
|
||||
sha256: "66f86e916d285c1a93d3b79587d94bd71984a66aac4ff74e524cfa7877f1395c"
|
||||
sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.5"
|
||||
version: "0.4.1"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -146,7 +146,7 @@ packages:
|
|||
source: hosted
|
||||
version: "1.1.1"
|
||||
collection:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: collection
|
||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||
|
@ -169,6 +169,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.7.2"
|
||||
cronet_http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cronet_http
|
||||
sha256: "9b9f00ae48971bc8a8cbdd4528bd35511adce00fb79d1ebf9f9907667056640f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -185,6 +193,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
cupertino_http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cupertino_http
|
||||
sha256: "20c167fd843c9ff6fc25cc4a0e8efa4180dfe119fb6d18c3c55104113e9cfc6f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -197,10 +213,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368"
|
||||
sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.4"
|
||||
version: "2.3.6"
|
||||
datetime_picker_formfield:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -229,10 +245,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: dynamic_color
|
||||
sha256: a866f1f8947bfdaf674d7928e769eac7230388a2e7a2542824fad4bb5b87be3b
|
||||
sha256: eae98052fa6e2826bdac3dd2e921c6ce2903be15c6b7f8b6d8a5d49b5086298d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.6.9"
|
||||
version: "1.7.0"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -245,18 +261,26 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878"
|
||||
sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "2.1.2"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d"
|
||||
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.4"
|
||||
version: "7.0.0"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
|
@ -290,10 +314,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_keyboard_visibility
|
||||
sha256: "183ef857869a790aaff0219327a31783017fcc54b895fcdda1909645bb6ab965"
|
||||
sha256: "98664be7be0e3ffca00de50f7f6a287ab62c763fc8c762e0a21584584a3ff4f8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.4.2"
|
||||
version: "6.0.0"
|
||||
flutter_keyboard_visibility_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -338,42 +362,42 @@ packages:
|
|||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_launcher_icons
|
||||
sha256: a9de6706cd844668beac27c0aed5910fa0534832b3c2cad61a5fd977fce82a5d
|
||||
sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.10.0"
|
||||
version: "0.13.1"
|
||||
flutter_local_notifications:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_local_notifications
|
||||
sha256: "57d0012730780fe137260dd180e072c18a73fbeeb924cdc029c18aaa0f338d64"
|
||||
sha256: f9a05409385b77b06c18f200a41c7c2711ebf7415669350bb0f8474c07bd40d1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.9.1"
|
||||
version: "17.0.0"
|
||||
flutter_local_notifications_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_linux
|
||||
sha256: b472bfc173791b59ede323661eae20f7fff0b6908fea33dd720a6ef5d576bae8
|
||||
sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.1"
|
||||
version: "4.0.0+1"
|
||||
flutter_local_notifications_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_platform_interface
|
||||
sha256: "21bceee103a66a53b30ea9daf677f990e5b9e89b62f222e60dd241cd08d63d3a"
|
||||
sha256: "7cf643d6d5022f3baed0be777b0662cce5919c0a7b86e700299f22dc4ae660ef"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.0"
|
||||
version: "7.0.0+1"
|
||||
flutter_secure_storage:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_secure_storage
|
||||
sha256: "22dbf16f23a4bcf9d35e51be1c84ad5bb6f627750565edd70dab70f3ff5fff8f"
|
||||
sha256: ffdbb60130e4665d2af814a0267c481bcf522c41ae2e43caf69fa0146876d685
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.0"
|
||||
version: "9.0.0"
|
||||
flutter_secure_storage_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -410,18 +434,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_windows
|
||||
sha256: "38f9501c7cb6f38961ef0e1eacacee2b2d4715c63cc83fe56449c4d3d0b47255"
|
||||
sha256: "5809c66f9dd3b4b93b0a6e2e8561539405322ee767ac2f64d084e2ab5429d108"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "3.0.0"
|
||||
flutter_svg:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_svg
|
||||
sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c
|
||||
sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.9"
|
||||
version: "2.0.10+1"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
|
@ -439,10 +463,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_typeahead
|
||||
sha256: "1f6b248bb4f3ebb4cf1ee0354aa23c77be457fb2d26d6847ecc33a917f65e58e"
|
||||
sha256: d64712c65db240b1057559b952398ebb6e498077baeebf9b0731dade62438a6d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.1"
|
||||
version: "5.2.0"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
|
@ -452,18 +476,18 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_widget_from_html
|
||||
sha256: d2ad036fb34a4f6ab84c21196788a5b1620f34d7930d16b32daadb511be24bb2
|
||||
sha256: "22c911b6ccf82b83e0c457d987bac4e703440fea0fc88dab24f4dfe995a5f33f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.14.10+1"
|
||||
version: "0.14.11"
|
||||
flutter_widget_from_html_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_widget_from_html_core
|
||||
sha256: "0e281196f962fd951da5b9d3fa50e0674fabf8fda92eafd8745d050d70877c68"
|
||||
sha256: "028f4989b9ff4907466af233d50146d807772600d98a3e895662fbdb09c39225"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.14.10+1"
|
||||
version: "0.14.11"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -540,10 +564,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2"
|
||||
sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.13.6"
|
||||
version: "1.2.1"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -564,18 +588,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: image
|
||||
sha256: "8e9d133755c3e84c73288363e6343157c383a0c6c56fc51afcc5d4d7180306d6"
|
||||
sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.0"
|
||||
version: "4.1.7"
|
||||
intl:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: intl
|
||||
sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91"
|
||||
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.17.0"
|
||||
version: "0.19.0"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -584,6 +608,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
jni:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: jni
|
||||
sha256: "499558e919997adfc45809a66caf0b95b91393e23289dd2826b152f8f04e6611"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.3"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -593,7 +625,7 @@ packages:
|
|||
source: hosted
|
||||
version: "0.6.7"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467
|
||||
|
@ -632,6 +664,30 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.9"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.0"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -644,34 +700,34 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
|
||||
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.16"
|
||||
version: "0.12.16+1"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
|
||||
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
version: "0.8.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: meta
|
||||
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
|
||||
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
version: "1.11.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e
|
||||
sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
version: "1.0.5"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -708,10 +764,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: "10259b111176fba5c505b102e3a5b022b51dd97e30522e906d6922c745584745"
|
||||
sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
version: "4.2.0"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -724,10 +780,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
|
||||
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.8.3"
|
||||
version: "1.9.0"
|
||||
path_drawing:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -748,10 +804,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa
|
||||
sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.1.2"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -764,10 +820,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d"
|
||||
sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
version: "2.3.2"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -780,10 +836,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c"
|
||||
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.1.2"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -796,50 +852,58 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: permission_handler
|
||||
sha256: bc56bfe9d3f44c3c612d8d393bd9b174eb796d706759f9b495ac254e4294baa5
|
||||
sha256: "74e962b7fad7ff75959161bb2c0ad8fe7f2568ee82621c9c2660b751146bfe44"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.4.5"
|
||||
version: "11.3.0"
|
||||
permission_handler_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_android
|
||||
sha256: "59c6322171c29df93a22d150ad95f3aa19ed86542eaec409ab2691b8f35f9a47"
|
||||
sha256: "1acac6bae58144b442f11e66621c062aead9c99841093c38f5bcdcc24c1c3474"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.3.6"
|
||||
version: "12.0.5"
|
||||
permission_handler_apple:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_apple
|
||||
sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5"
|
||||
sha256: e9ad66020b89ff1b63908f247c2c6f931c6e62699b756ef8b3c4569350cd8662
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.1.4"
|
||||
version: "9.4.4"
|
||||
permission_handler_html:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_html
|
||||
sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.1"
|
||||
permission_handler_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_platform_interface
|
||||
sha256: "6760eb5ef34589224771010805bea6054ad28453906936f843a8cc4d3a55c4a4"
|
||||
sha256: "23dfba8447c076ab5be3dee9ceb66aad345c4a648f0cac292c77b1eb0e800b78"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.12.0"
|
||||
version: "4.2.0"
|
||||
permission_handler_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_windows
|
||||
sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098
|
||||
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.3"
|
||||
version: "0.2.1"
|
||||
petitparser:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750
|
||||
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.4.0"
|
||||
version: "6.0.2"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -860,18 +924,42 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: pointer_interceptor
|
||||
sha256: adf7a637f97c077041d36801b43be08559fd4322d2127b3f20bb7be1b9eebc22
|
||||
sha256: bd18321519718678d5fa98ad3a3359cbc7a31f018554eab80b73d08a7f0c165a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.3+7"
|
||||
version: "0.10.1"
|
||||
pointer_interceptor_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pointer_interceptor_ios
|
||||
sha256: "2e73c39452830adc4695757130676a39412a3b7f3c34e3f752791b5384770877"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.10.0+2"
|
||||
pointer_interceptor_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pointer_interceptor_platform_interface
|
||||
sha256: "0597b0560e14354baeb23f8375cd612e8bd4841bf8306ecb71fcd0bb78552506"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.10.0+1"
|
||||
pointer_interceptor_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pointer_interceptor_web
|
||||
sha256: a6237528b46c411d8d55cdfad8fcb3269fc4cbb26060b14bff94879165887d1e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.10.2"
|
||||
pointycastle:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pointycastle
|
||||
sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c"
|
||||
sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.7.3"
|
||||
version: "3.7.4"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -880,22 +968,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.1"
|
||||
process:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: process
|
||||
sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.4"
|
||||
provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: provider
|
||||
sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096"
|
||||
sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.1"
|
||||
version: "6.1.2"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1009,18 +1089,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: sqflite
|
||||
sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a"
|
||||
sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
version: "2.3.2"
|
||||
sqflite_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_common
|
||||
sha256: bb4738f15b23352822f4c42a531677e5c6f522e079461fd240ead29d8d8a54a6
|
||||
sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.0+2"
|
||||
version: "2.5.4"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1086,13 +1166,13 @@ packages:
|
|||
source: hosted
|
||||
version: "0.5.9"
|
||||
timezone:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: timezone
|
||||
sha256: "57b35f6e8ef731f18529695bffc62f92c6189fac2e52c12d478dec1931afb66e"
|
||||
sha256: "1cfd8ddc2d1cfd836bc93e67b9be88c3adaeca6f40a00ca999104c30693cdca0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.0"
|
||||
version: "0.9.2"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1105,26 +1185,26 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: e9aa5ea75c84cf46b3db4eea212523591211c3cf2e13099ee4ec147f54201c86
|
||||
sha256: "0ecc004c62fd3ed36a2ffcbe0dd9700aee63bd7532d0b642a488b1ec310f492e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.2.2"
|
||||
version: "6.2.5"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: c0766a55ab42cefaa728cabc951e82919ab41a3a4fee0aaa96176ca82da8cc51
|
||||
sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.2.1"
|
||||
version: "6.3.0"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "46b81e3109cbb2d6b81702ad3077540789a3e74e22795eb9f0b7d494dbaa72ea"
|
||||
sha256: "9149d493b075ed740901f3ee844a38a00b33116c7c5c10d7fb27df8987fb51d5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.2.2"
|
||||
version: "6.2.5"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1145,18 +1225,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_platform_interface
|
||||
sha256: "4aca1e060978e19b2998ee28503f40b5ba6226819c2b5e3e4d1821e8ccd92198"
|
||||
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
version: "2.3.2"
|
||||
url_launcher_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b
|
||||
sha256: "3692a459204a33e04bc94f5fb91158faf4f2c8903281ddd82915adecdb1a901d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.3"
|
||||
version: "2.3.0"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1169,34 +1249,34 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: uuid
|
||||
sha256: "22c94e5ad1e75f9934b766b53c742572ee2677c56bc871d850a57dad0f82127f"
|
||||
sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.2"
|
||||
version: "4.3.3"
|
||||
vector_graphics:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics
|
||||
sha256: "0f0c746dd2d6254a0057218ff980fc7f5670fd0fcf5e4db38a490d31eed4ad43"
|
||||
sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.9+1"
|
||||
version: "1.1.11+1"
|
||||
vector_graphics_codec:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics_codec
|
||||
sha256: "0edf6d630d1bfd5589114138ed8fada3234deacc37966bec033d3047c29248b7"
|
||||
sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.9+1"
|
||||
version: "1.1.11+1"
|
||||
vector_graphics_compiler:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics_compiler
|
||||
sha256: d24333727332d9bd20990f1483af4e09abdb9b1fc7c3db940b56ab5c42790c26
|
||||
sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.9+1"
|
||||
version: "1.1.11+1"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1209,42 +1289,42 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: video_player
|
||||
sha256: fbf28ce8bcfe709ad91b5789166c832cb7a684d14f571a81891858fefb5bb1c2
|
||||
sha256: afc65f4b8bcb2c188f64a591f84fb471f4f2e19fc607c65fd8d2f8fedb3dec23
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.8.2"
|
||||
version: "2.8.3"
|
||||
video_player_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_android
|
||||
sha256: "7f8f25d7ad56819a82b2948357f3c3af071f6a678db33833b26ec36bbc221316"
|
||||
sha256: "4dd9b8b86d70d65eecf3dcabfcdfbb9c9115d244d022654aba49a00336d540c2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.11"
|
||||
version: "2.4.12"
|
||||
video_player_avfoundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_avfoundation
|
||||
sha256: "752b783a00748684312dd6a68e22f795281d295921e69ae5ad36f90b7fb39cdb"
|
||||
sha256: "309e3962795e761be010869bae65c0b0e45b5230c5cee1bec72197ca7db040ed"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.5"
|
||||
version: "2.5.6"
|
||||
video_player_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_platform_interface
|
||||
sha256: be72301bf2c0150ab35a8c34d66e5a99de525f6de1e8d27c0672b836fe48f73a
|
||||
sha256: "236454725fafcacf98f0f39af0d7c7ab2ce84762e3b63f2cbb3ef9a7e0550bc6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.2.1"
|
||||
version: "6.2.2"
|
||||
video_player_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_web
|
||||
sha256: "34beb3a07d4331a24f7e7b2f75b8e2b103289038e07e65529699a671b6a6e2cb"
|
||||
sha256: "41245cef5ef29c4585dbabcbcbe9b209e34376642c7576cabf11b4ad9289d6e4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
version: "2.3.0"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1253,46 +1333,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "13.0.0"
|
||||
wakelock:
|
||||
wakelock_plus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: wakelock
|
||||
sha256: "769ecf42eb2d07128407b50cb93d7c10bd2ee48f0276ef0119db1d25cc2f87db"
|
||||
name: wakelock_plus
|
||||
sha256: f268ca2116db22e57577fb99d52515a24bdc1d570f12ac18bb762361d43b043d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.2"
|
||||
wakelock_macos:
|
||||
version: "1.1.4"
|
||||
wakelock_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: wakelock_macos
|
||||
sha256: "047c6be2f88cb6b76d02553bca5a3a3b95323b15d30867eca53a19a0a319d4cd"
|
||||
name: wakelock_plus_platform_interface
|
||||
sha256: "40fabed5da06caff0796dc638e1f07ee395fb18801fbff3255a2372db2d80385"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.0"
|
||||
wakelock_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: wakelock_platform_interface
|
||||
sha256: "1f4aeb81fb592b863da83d2d0f7b8196067451e4df91046c26b54a403f9de621"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.0"
|
||||
wakelock_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: wakelock_web
|
||||
sha256: "1b256b811ee3f0834888efddfe03da8d18d0819317f20f6193e2922b41a501b5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.0"
|
||||
wakelock_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: wakelock_windows
|
||||
sha256: "857f77b3fe6ae82dd045455baa626bc4b93cb9bb6c86bf3f27c182167c3a5567"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1"
|
||||
version: "1.1.0"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1305,18 +1361,26 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
|
||||
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.0"
|
||||
version: "0.5.1"
|
||||
web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket
|
||||
sha256: "3f81fde6fbc799d03c0fb3f2c3ac84368ee267012a4beb876685c029946da4e0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.0"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b
|
||||
sha256: "1d8e795e2a8b3730c41b8a98a2dff2e0fb57ae6f0764a1c46ec5915387d257b2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
version: "2.4.4"
|
||||
webkit_inspection_protocol:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1329,42 +1393,42 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: webview_flutter
|
||||
sha256: "60e23976834e995c404c0b21d3b9db37ecd77d3303ef74f8b8d7a7b19947fc04"
|
||||
sha256: "25e1b6e839e8cbfbd708abc6f85ed09d1727e24e08e08c6b8590d7c65c9a8932"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.4.3"
|
||||
version: "4.7.0"
|
||||
webview_flutter_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_android
|
||||
sha256: "161af93c2abaf94ef2192bffb53a3658b2d721a3bf99b69aa1e47814ee18cc96"
|
||||
sha256: f038ee2fae73b509dde1bc9d2c5a50ca92054282de17631a9a3d515883740934
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.13.2"
|
||||
version: "3.16.0"
|
||||
webview_flutter_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_platform_interface
|
||||
sha256: dbe745ee459a16b6fec296f7565a8ef430d0d681001d8ae521898b9361854943
|
||||
sha256: d937581d6e558908d7ae3dc1989c4f87b786891ab47bb9df7de548a151779d8d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.9.0"
|
||||
version: "2.10.0"
|
||||
webview_flutter_wkwebview:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_wkwebview
|
||||
sha256: "02d8f3ebbc842704b2b662377b3ee11c0f8f1bbaa8eab6398262f40049819160"
|
||||
sha256: f12f8d8a99784b863e8b85e4a9a5e3cf1839d6803d2c0c3e0533a8f3c5a992a7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.10.1"
|
||||
version: "3.13.0"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4
|
||||
sha256: "8cb58b45c47dcb42ab3651533626161d6b67a2921917d8d429791f76972b3480"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.4"
|
||||
version: "5.3.0"
|
||||
workmanager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1377,18 +1441,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86
|
||||
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0+3"
|
||||
version: "1.0.4"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84"
|
||||
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.0"
|
||||
version: "6.5.0"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1398,5 +1462,5 @@ packages:
|
|||
source: hosted
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=3.2.0 <4.0.0"
|
||||
flutter: ">=3.16.0"
|
||||
dart: ">=3.3.0 <4.0.0"
|
||||
flutter: ">=3.19.0"
|
||||
|
|
56
pubspec.yaml
56
pubspec.yaml
|
@ -1,7 +1,7 @@
|
|||
name: vikunja_app
|
||||
description: Vikunja as Flutter cross platform app
|
||||
|
||||
version: 0.1.3-beta
|
||||
version: 0.1.5-beta
|
||||
|
||||
environment:
|
||||
sdk: ">=2.18.0 <3.0.0"
|
||||
|
@ -9,40 +9,44 @@ environment:
|
|||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
cupertino_icons: ^1.0.5
|
||||
http: ^0.13.5
|
||||
cupertino_icons: ^1.0.6
|
||||
http: ^1.2.1
|
||||
after_layout: ^1.2.0
|
||||
intl: ^0.17.0
|
||||
flutter_local_notifications: ^9.8.0+1
|
||||
rxdart: ^0.27.5
|
||||
flutter_timezone: ^1.0.4
|
||||
flutter_secure_storage: ^8.0.0
|
||||
intl: ^0.19.0
|
||||
flutter_local_notifications: ^17.0.0
|
||||
rxdart: ^0.27.7
|
||||
flutter_timezone: ^1.0.8
|
||||
flutter_secure_storage: ^9.0.0
|
||||
datetime_picker_formfield: ^2.0.1
|
||||
flutter_typeahead: ^5.0.1
|
||||
build: ^2.3.0
|
||||
json_serializable: ^6.3.1
|
||||
petitparser: ^5.0.0
|
||||
provider: ^6.0.3
|
||||
webview_flutter: ^4.4.3
|
||||
flutter_typeahead: ^5.2.0
|
||||
build: ^2.4.1
|
||||
json_serializable: ^6.7.1
|
||||
petitparser: ^6.0.2
|
||||
provider: ^6.1.2
|
||||
webview_flutter: ^4.7.0
|
||||
flutter_colorpicker: ^1.0.3
|
||||
flutter_keyboard_visibility: ^5.4.2
|
||||
dotted_border: ^2.0.0+2
|
||||
package_info_plus: ^3.0.2
|
||||
url_launcher: ^6.1.7
|
||||
workmanager: ^0.5.1
|
||||
permission_handler: ^10.2.0
|
||||
dynamic_color: ^1.6.6
|
||||
chewie: ^1.5.0
|
||||
flutter_widget_from_html: ^0.14.10
|
||||
flutter_keyboard_visibility: ^6.0.0
|
||||
dotted_border: ^2.1.0
|
||||
url_launcher: ^6.2.5
|
||||
workmanager: ^0.5.2
|
||||
permission_handler: ^11.3.0
|
||||
dynamic_color: ^1.7.0
|
||||
flutter_widget_from_html: ^0.14.11
|
||||
flutter_downloader: ^1.11.6
|
||||
|
||||
|
||||
meta: ^1.11.0
|
||||
timezone: ^0.9.2
|
||||
json_annotation: ^4.8.1
|
||||
collection: ^1.18.0
|
||||
cupertino_http: ^1.4.0
|
||||
cronet_http: ^1.2.0
|
||||
package_info_plus: any
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
version: any
|
||||
test: ^1.21.1
|
||||
flutter_launcher_icons: ^0.10.0
|
||||
test: ^1.24.9
|
||||
flutter_launcher_icons: ^0.13.1
|
||||
|
||||
flutter_icons:
|
||||
image_path: "assets/vikunja_logo.png"
|
||||
|
|
|
@ -7,7 +7,8 @@ import 'package:vikunja_app/models/user.dart';
|
|||
|
||||
void main() {
|
||||
test('label color from json', () {
|
||||
final String json = '{"TaskID": 123,"id": 1,"title": "this","description": "","hex_color": "e8e8e8","created_by":{"id": 1,"username": "user","email": "test@example.com","created": 1537855131,"updated": 1545233325},"created": 1552903790,"updated": 1552903790}';
|
||||
final String json =
|
||||
'{"TaskID": 123,"id": 1,"title": "this","description": "","hex_color": "e8e8e8","created_by":{"id": 1,"username": "user","email": "test@example.com","created": 1537855131,"updated": 1545233325},"created": 1552903790,"updated": 1552903790}';
|
||||
final JsonDecoder _decoder = new JsonDecoder();
|
||||
Label label = Label.fromJson(_decoder.convert(json));
|
||||
|
||||
|
@ -15,9 +16,14 @@ void main() {
|
|||
});
|
||||
|
||||
test('hex color string from object', () {
|
||||
Label label = Label(id: 1, title: '', color: Color(0xFFe8e8e8), createdBy: User(id: 0, username: ''));
|
||||
Label label = Label(
|
||||
id: 1,
|
||||
title: '',
|
||||
color: Color(0xFFe8e8e8),
|
||||
createdBy: User(id: 0, username: ''));
|
||||
var json = label.toJSON();
|
||||
|
||||
expect(json.toString(), '{id: 1, title: , description: null, hex_color: e8e8e8, created_by: {id: 0, username: ,}, updated: null, created: null}');
|
||||
expect(json.toString(),
|
||||
'{id: 1, title: , description: null, hex_color: e8e8e8, created_by: {id: 0, username: ,}, updated: null, created: null}');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,8 @@ import 'package:test/test.dart';
|
|||
|
||||
void main() {
|
||||
test('Check encoding with all values set', () {
|
||||
final String json = '{"id": 1,"text": "test","description": "Lorem Ipsum","done": true,"dueDate": 1543834800,"reminderDates": [1543834800,1544612400],"repeatAfter": 3600,"parentTaskID": 0,"priority": 100,"startDate": 1543834800,"endDate": 1543835000,"assignees": null,"labels": null,"subtasks": null,"created": 1542465818,"updated": 1552771527,"createdBy": {"id": 1,"username": "user","email": "test@example.com","created": 1537855131,"updated": 1545233325}}';
|
||||
final String json =
|
||||
'{"id": 1,"text": "test","description": "Lorem Ipsum","done": true,"dueDate": 1543834800,"reminderDates": [1543834800,1544612400],"repeatAfter": 3600,"parentTaskID": 0,"priority": 100,"startDate": 1543834800,"endDate": 1543835000,"assignees": null,"labels": null,"subtasks": null,"created": 1542465818,"updated": 1552771527,"createdBy": {"id": 1,"username": "user","email": "test@example.com","created": 1537855131,"updated": 1545233325}}';
|
||||
final JsonDecoder _decoder = new JsonDecoder();
|
||||
final task = Task.fromJson(_decoder.convert(json));
|
||||
|
||||
|
@ -17,19 +18,25 @@ void main() {
|
|||
DateTime.fromMillisecondsSinceEpoch(1543834800 * 1000),
|
||||
DateTime.fromMillisecondsSinceEpoch(1544612400 * 1000),
|
||||
]);
|
||||
expect(task.dueDate, DateTime.fromMillisecondsSinceEpoch(1543834800 * 1000));
|
||||
expect(
|
||||
task.dueDate, DateTime.fromMillisecondsSinceEpoch(1543834800 * 1000));
|
||||
expect(task.repeatAfter, Duration(seconds: 3600));
|
||||
expect(task.parentTaskId, 0);
|
||||
expect(task.priority, 100);
|
||||
expect(task.startDate, DateTime.fromMillisecondsSinceEpoch(1543834800 * 1000));
|
||||
expect(task.endDate, DateTime.fromMillisecondsSinceEpoch(1543835000 * 1000));
|
||||
expect(
|
||||
task.startDate, DateTime.fromMillisecondsSinceEpoch(1543834800 * 1000));
|
||||
expect(
|
||||
task.endDate, DateTime.fromMillisecondsSinceEpoch(1543835000 * 1000));
|
||||
expect(task.labels, null);
|
||||
expect(task.subtasks, null);
|
||||
expect(task.created, DateTime.fromMillisecondsSinceEpoch(1542465818 * 1000));
|
||||
expect(task.updated, DateTime.fromMillisecondsSinceEpoch(1552771527 * 1000));
|
||||
expect(
|
||||
task.created, DateTime.fromMillisecondsSinceEpoch(1542465818 * 1000));
|
||||
expect(
|
||||
task.updated, DateTime.fromMillisecondsSinceEpoch(1552771527 * 1000));
|
||||
});
|
||||
test('Check encoding with reminder dates as null', () {
|
||||
final String json = '{"id": 1,"text": "test","description": "Lorem Ipsum","done": true,"dueDate": 1543834800,"reminderDates": null,"repeatAfter": 3600,"parentTaskID": 0,"priority": 100,"startDate": 1543834800,"endDate": 1543835000,"assignees": null,"labels": null,"subtasks": null,"created": 1542465818,"updated": 1552771527,"createdBy": {"id": 1,"username": "user","email": "test@example.com","created": 1537855131,"updated": 1545233325}}';
|
||||
final String json =
|
||||
'{"id": 1,"text": "test","description": "Lorem Ipsum","done": true,"dueDate": 1543834800,"reminderDates": null,"repeatAfter": 3600,"parentTaskID": 0,"priority": 100,"startDate": 1543834800,"endDate": 1543835000,"assignees": null,"labels": null,"subtasks": null,"created": 1542465818,"updated": 1552771527,"createdBy": {"id": 1,"username": "user","email": "test@example.com","created": 1537855131,"updated": 1545233325}}';
|
||||
final JsonDecoder _decoder = new JsonDecoder();
|
||||
final task = Task.fromJson(_decoder.convert(json));
|
||||
|
||||
|
@ -38,15 +45,20 @@ void main() {
|
|||
expect(task.description, 'Lorem Ipsum');
|
||||
expect(task.done, true);
|
||||
expect(task.reminderDates, null);
|
||||
expect(task.dueDate, DateTime.fromMillisecondsSinceEpoch(1543834800 * 1000));
|
||||
expect(
|
||||
task.dueDate, DateTime.fromMillisecondsSinceEpoch(1543834800 * 1000));
|
||||
expect(task.repeatAfter, Duration(seconds: 3600));
|
||||
expect(task.parentTaskId, 0);
|
||||
expect(task.priority, 100);
|
||||
expect(task.startDate, DateTime.fromMillisecondsSinceEpoch(1543834800 * 1000));
|
||||
expect(task.endDate, DateTime.fromMillisecondsSinceEpoch(1543835000 * 1000));
|
||||
expect(
|
||||
task.startDate, DateTime.fromMillisecondsSinceEpoch(1543834800 * 1000));
|
||||
expect(
|
||||
task.endDate, DateTime.fromMillisecondsSinceEpoch(1543835000 * 1000));
|
||||
expect(task.labels, null);
|
||||
expect(task.subtasks, null);
|
||||
expect(task.created, DateTime.fromMillisecondsSinceEpoch(1542465818 * 1000));
|
||||
expect(task.updated, DateTime.fromMillisecondsSinceEpoch(1552771527 * 1000));
|
||||
expect(
|
||||
task.created, DateTime.fromMillisecondsSinceEpoch(1542465818 * 1000));
|
||||
expect(
|
||||
task.updated, DateTime.fromMillisecondsSinceEpoch(1552771527 * 1000));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
BIN
web/favicon.png
Normal file
BIN
web/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 917 B |
BIN
web/icons/Icon-192.png
Normal file
BIN
web/icons/Icon-192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.2 KiB |
BIN
web/icons/Icon-512.png
Normal file
BIN
web/icons/Icon-512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.1 KiB |
BIN
web/icons/Icon-maskable-192.png
Normal file
BIN
web/icons/Icon-maskable-192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.5 KiB |
BIN
web/icons/Icon-maskable-512.png
Normal file
BIN
web/icons/Icon-maskable-512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
59
web/index.html
Normal file
59
web/index.html
Normal file
|
@ -0,0 +1,59 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!--
|
||||
If you are serving your web app in a path other than the root, change the
|
||||
href value below to reflect the base path you are serving from.
|
||||
|
||||
The path provided below has to start and end with a slash "/" in order for
|
||||
it to work correctly.
|
||||
|
||||
For more details:
|
||||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
|
||||
|
||||
This is a placeholder for base href that will be replaced by the value of
|
||||
the `--base-href` argument provided to `flutter build`.
|
||||
-->
|
||||
<base href="$FLUTTER_BASE_HREF">
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<meta name="description" content="A new Flutter project.">
|
||||
|
||||
<!-- iOS meta tags & icons -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="vikunja_app">
|
||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
|
||||
<title>vikunja_app</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
|
||||
<script>
|
||||
// The value below is injected by flutter build, do not touch.
|
||||
const serviceWorkerVersion = null;
|
||||
</script>
|
||||
<!-- This script adds the flutter initialization JS code -->
|
||||
<script src="flutter.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
window.addEventListener('load', function(ev) {
|
||||
// Download main.dart.js
|
||||
_flutter.loader.loadEntrypoint({
|
||||
serviceWorker: {
|
||||
serviceWorkerVersion: serviceWorkerVersion,
|
||||
},
|
||||
onEntrypointLoaded: function(engineInitializer) {
|
||||
engineInitializer.initializeEngine().then(function(appRunner) {
|
||||
appRunner.runApp();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
35
web/manifest.json
Normal file
35
web/manifest.json
Normal file
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"name": "vikunja_app",
|
||||
"short_name": "vikunja_app",
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"background_color": "#0175C2",
|
||||
"theme_color": "#0175C2",
|
||||
"description": "A new Flutter project.",
|
||||
"orientation": "portrait-primary",
|
||||
"prefer_related_applications": false,
|
||||
"icons": [
|
||||
{
|
||||
"src": "icons/Icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-maskable-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-maskable-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue
Block a user