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

feat: add auth-token

This commit is contained in:
Denys Vitali 2024-03-26 23:16:40 +01:00
parent fb5b2de493
commit 58202352aa
No known key found for this signature in database
GPG Key ID: 5227C664145098BC
12 changed files with 362 additions and 267 deletions

View File

@ -18,22 +18,37 @@ class Client {
final JsonEncoder _encoder = new JsonEncoder();
String _token = '';
String _base = '';
String _xClientToken = '';
bool authenticated = false;
bool ignoreCertificates = false;
bool showSnackBar = true;
String get base => _base;
String get token => _token;
String get xClientToken => _xClientToken;
String? post_body;
bool operator ==(dynamic otherClient) {
return otherClient._token == _token;
@override
bool operator ==(Object otherClient) {
if (otherClient is! Client) return false;
return otherClient._token == _token &&
otherClient._xClientToken == _xClientToken;
}
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? xClientToken,
String? base,
bool authenticated = false,
}) {
configure(
token: token,
xClientToken: xClientToken,
base: base,
authenticated: authenticated,
);
}
http.Client get httpClient {
@ -65,7 +80,8 @@ class Client {
get _headers => {
'Authorization': _token != '' ? 'Bearer $_token' : '',
'Content-Type': 'application/json',
'User-Agent': 'Vikunja Mobile App'
'User-Agent': 'Vikunja Mobile App',
'X-Client-Token': _xClientToken
};
get headers => _headers;
@ -73,8 +89,14 @@ class Client {
@override
int get hashCode => _token.hashCode;
void configure({String? token, String? base, bool? authenticated}) {
void configure({
String? token,
String? base,
bool? authenticated,
String? xClientToken,
}) {
if (token != null) _token = token;
if (xClientToken != null) _xClientToken = xClientToken;
if (base != null) {
base = base.replaceAll(" ", "");
if (base.endsWith("/")) base = base.substring(0, base.length - 1);
@ -84,7 +106,7 @@ class Client {
}
void reset() {
_token = _base = '';
_token = _base = _xClientToken = '';
authenticated = false;
}
@ -201,7 +223,7 @@ class Client {
}
Response? _handleResponse(http.Response response) {
Error? error = _handleResponseErrors(response);
_handleResponseErrors(response);
return Response(
_decoder.convert(response.body), response.statusCode, response.headers);
}

View File

@ -9,22 +9,29 @@ 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,
String? xClientToken,
}) 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"));
client.configure(token: token);
errorString:
response != null ? response.body["message"] : "Login error"));
client.configure(token: token, xClientToken: xClientToken);
return UserAPIService(client)
.getCurrentUser()
.then((user) => UserTokenPair(user, token));
@ -46,9 +53,12 @@ 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;
});
}

View File

@ -46,95 +46,109 @@ class TaskBottomSheetState extends State<TaskBottomSheet> {
Widget build(BuildContext context) {
ThemeData theme = Theme.of(context);
return Container(
height: MediaQuery.of(context).size.height * 0.9,
child: Padding(
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"),
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)),
],
),
// 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"),
],
),
],
),
)
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"),
],
),
],
),
));
}
}
}

1
lib/constants.dart Normal file
View File

@ -0,0 +1 @@
const ErrorCodeOtpRequired = 1017;

View File

@ -98,7 +98,8 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
initialDelay: Duration(seconds: 15),
inputData: {
"client_token": client.token,
"client_base": client.base
"client_base": client.base,
"x_client_token": client.xClientToken,
});
}

View File

@ -41,6 +41,7 @@ void callbackDispatcher() {
Client client = Client(null,
token: inputData["client_token"],
base: inputData["client_base"],
xClientToken: inputData["x_client_token"],
authenticated: true);
tz.initializeTimeZones();
@ -66,13 +67,19 @@ void callbackDispatcher() {
return Future.value(true);
}
var token = await _storage.read(key: currentUser);
var base = await _storage.read(key: '${currentUser}_base');
var xClientToken =
await _storage.read(key: '${currentUser}_x_client_token');
if (token == null || base == null) {
return Future.value(true);
}
Client client = Client(null);
client.configure(token: token, base: base, authenticated: true);
client.configure(
token: token,
base: base,
xClientToken: xClientToken,
authenticated: true,
);
// load new token from server to avoid expiration
String? newToken = await UserAPIService(client).getToken();
if (newToken != null) {

View File

@ -41,36 +41,35 @@ class _NamespaceOverviewPageState extends State<NamespaceOverviewPage>
onTap: () => _onSelectItem(i),
)));
if(_selectedDrawerIndex > -1) {
if (_selectedDrawerIndex > -1) {
return new WillPopScope(
child: NamespacePage(namespace: _namespaces[_selectedDrawerIndex]),
onWillPop: () async {setState(() {
_selectedDrawerIndex = -2;
onWillPop: () async {
setState(() {
_selectedDrawerIndex = -2;
});
return false;
});
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(
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"),
),
),
);
}
@ -84,11 +83,13 @@ class _NamespaceOverviewPageState extends State<NamespaceOverviewPage>
}
_onSelectItem(int index) {
Navigator.push(context,
Navigator.push(
context,
MaterialPageRoute(
builder: (buildContext) => NamespacePage(
namespace: _namespaces[index],
),));
),
));
//setState(() => _selectedDrawerIndex = index);
}

View File

@ -153,7 +153,11 @@ class _ProjectOverviewPageState extends State<ProjectOverviewPage>
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('The project was created successfully!'),
));
}).catchError((error) => showDialog(
context: context, builder: (context) => ErrorDialog(error: error)));
}).catchError(
(error) => showDialog(
context: context,
builder: (context) => ErrorDialog(error: error),
),
);
}
}

View File

@ -4,10 +4,13 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:vikunja_app/api/client.dart';
import 'package:vikunja_app/api/user_implementation.dart';
import 'package:vikunja_app/constants.dart';
import 'package:vikunja_app/global.dart';
import 'package:vikunja_app/models/user.dart';
import 'package:vikunja_app/pages/user/login_webview.dart';
import 'package:vikunja_app/pages/user/register.dart';
import 'package:vikunja_app/service/services.dart';
import 'package:vikunja_app/theme/button.dart';
import 'package:vikunja_app/theme/buttonText.dart';
import 'package:vikunja_app/theme/constants.dart';
@ -26,10 +29,14 @@ class _LoginPageState extends State<LoginPage> {
bool _rememberMe = false;
bool init = false;
List<String> pastServers = [];
int amountTaps = 0;
DateTime? lastTap;
bool _showXClientTokent = false;
final _serverController = TextEditingController();
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
final _xClientTokenController = TextEditingController();
@override
void initState() {
@ -80,12 +87,15 @@ class _LoginPageState extends State<LoginPage> {
children: <Widget>[
Padding(
padding: EdgeInsets.symmetric(vertical: 30),
child: Image(
image: Theme.of(context).brightness == Brightness.dark
? AssetImage('assets/vikunja_logo_full_white.png')
: AssetImage('assets/vikunja_logo_full.png'),
height: 85.0,
semanticLabel: 'Vikunja Logo',
child: GestureDetector(
onTap: _handleLogoTap,
child: Image(
image: Theme.of(context).brightness == Brightness.dark
? AssetImage('assets/vikunja_logo_full_white.png')
: AssetImage('assets/vikunja_logo_full.png'),
height: 80.0,
semanticLabel: 'Vikunja Logo',
),
),
),
Padding(
@ -93,9 +103,6 @@ class _LoginPageState extends State<LoginPage> {
child: Row(children: [
Expanded(
child: TypeAheadField(
//suggestionsBoxController: _serverSuggestionController,
//getImmediateSuggestions: true,
//enabled: !_loading,
controller: _serverController,
builder: (context, controller, focusnode) {
return TextFormField(
@ -114,13 +121,6 @@ class _LoginPageState extends State<LoginPage> {
labelText: 'Server Address'),
);
},
/*
textFieldConfiguration: TextFieldConfiguration(
controller: _serverController,
decoration: new InputDecoration(
border: OutlineInputBorder(),
labelText: 'Server Address'),
),*/
onSelected: (suggestion) {
_serverController.text = suggestion;
setState(
@ -164,22 +164,6 @@ class _LoginPageState extends State<LoginPage> {
},
),
),
/*
DropdownButton<String>(
onChanged: (String? value) {
// This is called when the user selects an item.
setState(() {
if (value != null) _serverController.text = value;
});
},
items: pastServers
.map<DropdownMenuItem<String>>((dynamic value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
),*/
]),
),
Padding(
@ -205,6 +189,17 @@ class _LoginPageState extends State<LoginPage> {
obscureText: true,
),
),
if (_showXClientTokent)
Padding(
padding: vStandardVerticalPadding,
child: TextFormField(
enabled: !_loading,
controller: _xClientTokenController,
decoration: new InputDecoration(
border: OutlineInputBorder(),
labelText: 'X-Client-Token'),
),
),
Padding(
padding: vStandardVerticalPadding,
child: CheckboxListTile(
@ -216,14 +211,7 @@ class _LoginPageState extends State<LoginPage> {
),
Builder(
builder: (context) => FancyButton(
onPressed: !_loading
? () {
if (_formKey.currentState!.validate()) {
Form.of(context).save();
_loginUser(context);
}
}
: null,
onPressed: !_loading ? _doLogin(context) : null,
child: _loading
? CircularProgressIndicator()
: VikunjaButtonText('Login'),
@ -237,26 +225,11 @@ class _LoginPageState extends State<LoginPage> {
child: VikunjaButtonText('Register'),
)),
Builder(
builder: (context) => FancyButton(
onPressed: () {
if (_formKey.currentState!.validate() &&
_serverController.text.isNotEmpty) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LoginWithWebView(
_serverController.text))).then(
(btp) {
if (btp != null) _loginUserByClientToken(btp);
});
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
"Please enter your frontend url")));
}
},
child: VikunjaButtonText("Login with Frontend"))),
builder: (context) => FancyButton(
onPressed: _loginWithFrontend,
child: VikunjaButtonText("Login with Frontend"),
),
),
CheckboxListTile(
title: Text("Ignore Certificates"),
value: client.ignoreCertificates,
@ -279,12 +252,41 @@ class _LoginPageState extends State<LoginPage> {
);
}
_doLogin(BuildContext context) {
return () {
if (_formKey.currentState!.validate()) {
Form.of(context).save();
_loginUser(context);
}
};
}
_loginWithFrontend() {
if (_formKey.currentState!.validate() &&
_serverController.text.isNotEmpty) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
LoginWithWebView(_serverController.text))).then((btp) {
if (btp != null) _loginUserByClientToken(btp);
});
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Please enter your frontend url"),
),
);
}
}
_loginUser(BuildContext context) async {
String _server = _serverController.text;
String _username = _usernameController.text;
String _password = _passwordController.text;
if (_server.isEmpty) return;
String _xClientToken = _xClientTokenController.text;
if (_server.isEmpty) return;
if (!pastServers.contains(_server)) pastServers.add(_server);
await VikunjaGlobal.of(context).settingsManager.setPastServers(pastServers);
@ -292,16 +294,26 @@ class _LoginPageState extends State<LoginPage> {
try {
var vGlobal = VikunjaGlobal.of(context);
vGlobal.client.showSnackBar = false;
vGlobal.client.configure(base: _server);
vGlobal.client.configure(base: _server, xClientToken: _xClientToken);
Server? info = await vGlobal.serverService.getInfo();
if (info == null) throw Exception("Getting server info failed");
UserTokenPair newUser;
newUser = await vGlobal.newUserService!
.login(_username, _password, rememberMe: this._rememberMe);
Client client = Client(
vGlobal.snackbarKey,
base: _server,
xClientToken: _xClientToken,
);
UserService userService = UserAPIService(client);
newUser = await userService.login(
_username,
_password,
rememberMe: this._rememberMe,
xClientToken: _xClientToken,
);
if (newUser.error == 1017) {
if (newUser.error == ErrorCodeOtpRequired) {
TextEditingController totpController = TextEditingController();
bool dismissed = true;
await showDialog(
@ -326,35 +338,28 @@ class _LoginPageState extends State<LoginPage> {
),
);
if (!dismissed) {
newUser = await vGlobal.newUserService!.login(_username, _password,
rememberMe: this._rememberMe, totp: totpController.text);
newUser = await userService.login(
_username,
_password,
rememberMe: this._rememberMe,
totp: totpController.text,
);
} else {
throw Exception();
}
}
if (newUser.error > 0) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(newUser.errorString)));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(newUser.errorString),
),
);
}
if (newUser.error == 0)
vGlobal.changeUser(newUser.user!, token: newUser.token, base: _server);
} catch (ex) {
print(ex);
/* log(stacktrace.toString());
showDialog(
context: context,
builder: (context) => new AlertDialog(
title: Text(
'Login failed! Please check your server url and credentials. ' +
ex.toString()),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Close'))
],
));
*/
} finally {
VikunjaGlobal.of(context).client.showSnackBar = true;
setState(() {
@ -382,4 +387,30 @@ class _LoginPageState extends State<LoginPage> {
}
setState(() => _loading = false);
}
void _handleLogoTap() {
if (lastTap != null &&
DateTime.now().difference(lastTap!) < Duration(seconds: 2)) {
amountTaps++;
} else {
amountTaps = 1;
}
lastTap = DateTime.now();
if (amountTaps == 5) {
// Show X-Client-Token field
ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
"X-Client-Token " + (_showXClientTokent ? "hidden" : "shown"),
),
),
);
setState(() {
_showXClientTokent = !_showXClientTokent;
});
amountTaps = 0;
lastTap = null;
}
}
}

View File

@ -136,7 +136,6 @@ class MockedListService implements ListService {
throw UnimplementedError();
}
@override
void setDefaultList(int? listId) {
// TODO: implement setDefaultList
}
@ -190,7 +189,8 @@ class MockedTaskService implements TaskService {
}
@override
Future<Response?> getAllByProject(int projectId, [Map<String, List<String>>? queryParameters]) {
Future<Response?> getAllByProject(int projectId,
[Map<String, List<String>>? queryParameters]) {
// TODO: implement getAllByProject
return Future.value(new Response(_tasks.values.toList(), 200, {}));
}
@ -198,7 +198,8 @@ class MockedTaskService implements TaskService {
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, String? xClientToken}) {
return Future.value(UserTokenPair(_users[1]!, 'abcdefg'));
}
@ -223,6 +224,4 @@ class MockedUserService implements UserService {
// TODO: implement getToken
throw UnimplementedError();
}
}

View File

@ -59,9 +59,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;
}
}
@ -81,20 +81,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
]),
@ -105,13 +99,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;
@ -121,13 +116,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;
//}
@ -151,14 +145,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();
@ -228,8 +220,13 @@ abstract class BucketService {
}
abstract class UserService {
Future<UserTokenPair> login(String username, String password,
{bool rememberMe = false, String totp});
Future<UserTokenPair> login(
String username,
String password, {
bool rememberMe = false,
String totp,
String? xClientToken,
});
Future<UserTokenPair?> register(String username, email, password);
@ -289,7 +286,6 @@ class SettingsManager {
});
}
SettingsManager(this._storage) {
applydefaults();
}
@ -297,35 +293,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")));
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.write(
key: "workmanager-duration", value: duration.inMinutes.toString());
}
Future<List<String>?> getPastServers() {
return _storage.read(key: "recent-servers").then((value) => (jsonDecode(value!) as List<dynamic>).cast<String>());
return _storage
.read(key: "recent-servers")
.then((value) => (jsonDecode(value!) as List<dynamic>).cast<String>());
}
Future<void> setPastServers(List<String>? server) {
@ -336,18 +342,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;
@ -355,9 +360,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 {
@ -366,4 +371,4 @@ enum FlutterThemeMode {
dark,
materialYouLight,
materialYouDark,
}
}

View File

@ -761,7 +761,7 @@ packages:
source: hosted
version: "2.1.0"
package_info_plus:
dependency: transitive
dependency: "direct main"
description:
name: package_info_plus
sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017"