Skip to content
This repository has been archived by the owner on Sep 14, 2024. It is now read-only.

Commit

Permalink
Merge branch 'release/0.3.3'
Browse files Browse the repository at this point in the history
  • Loading branch information
Teifun2 committed Sep 6, 2020
2 parents c59870e + 1b31363 commit 981ae56
Show file tree
Hide file tree
Showing 17 changed files with 273 additions and 154 deletions.
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
![GitHub](https://img.shields.io/github/license/Teifun2/nextcloud-cookbook-flutter) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/Teifun2/nextcloud-cookbook-flutter) ![GitHub Workflow Status (branch)](https://img.shields.io/github/workflow/status/Teifun2/nextcloud-cookbook-flutter/Build/master)

[<img src="assets/IzzyOnDroid.png" alt="IzyyDroid" height="80px" />](https://apt.izzysoft.de/fdroid/index/apk/com.nextcloud_cookbook_flutter)
# Nextcloud Cookbook Mobile Client written in Flutter

This project aims to provide a mobile client for both Android and IOs for the nextcloud plugin cookbook (https://github.com/nextcloud/cookbook)
This project aims to provide a mobile client for both Android and IOs for the nextcloud app cookbook (https://github.com/nextcloud/cookbook)

It works best with an Nextcloud installation >= 17

## Screenshots

![Screenshot_categories](https://user-images.githubusercontent.com/7461832/91664922-899d6400-eaf2-11ea-8120-3222bd5b5363.png)
![Screenshot_category_list](https://user-images.githubusercontent.com/7461832/91664920-8904cd80-eaf2-11ea-9bb3-62e0b41f85c0.png)
![Screenshot_recipe_1](https://user-images.githubusercontent.com/7461832/91664918-873b0a00-eaf2-11ea-86a6-e30fde4c98a9.png)
![Screenshot_recipe_2](https://user-images.githubusercontent.com/7461832/91664923-8a35fa80-eaf2-11ea-9bfe-6ed8edc41b49.png)
<img src="https://user-images.githubusercontent.com/7461832/91664922-899d6400-eaf2-11ea-8120-3222bd5b5363.png" alt="Screenshot_categories" width="300px" /> <img src="https://user-images.githubusercontent.com/7461832/91664920-8904cd80-eaf2-11ea-9bb3-62e0b41f85c0.png" alt="Screenshot_category_list" width="300px" />

<img src="https://user-images.githubusercontent.com/7461832/91664923-8a35fa80-eaf2-11ea-9bfe-6ed8edc41b49.png" alt="Screenshot_recipe_2" width="300px" /> <img src="https://user-images.githubusercontent.com/7461832/91664918-873b0a00-eaf2-11ea-86a6-e30fde4c98a9.png" alt="Screenshot_recipe_1" width="300px" />

Binary file added assets/IzzyOnDroid.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 4 additions & 5 deletions lib/src/blocs/login/login_bloc.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import 'dart:async';

import 'package:meta/meta.dart';
import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';
import 'package:nextcloud_cookbook_flutter/src/blocs/authentication/authentication.dart';
import '../../services/user_repository.dart';

import '../../services/user_repository.dart';
import 'login.dart';

class LoginBloc extends Bloc<LoginEvent, LoginState> {
Expand All @@ -23,9 +23,8 @@ class LoginBloc extends Bloc<LoginEvent, LoginState> {
yield LoginLoading();

try {
final appAuthentication = await userRepository.authenticate(
serverUrl: event.serverURL
);
final appAuthentication =
await userRepository.authenticate(event.serverURL);

authenticationBloc.add(LoggedIn(appAuthentication: appAuthentication));
yield LoginInitial();
Expand Down
10 changes: 6 additions & 4 deletions lib/src/blocs/login/login_event.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import 'package:meta/meta.dart';
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';

abstract class LoginEvent extends Equatable {
const LoginEvent();

@override
List<Object> get props => [];
}

class LoginButtonPressed extends LoginEvent {
Expand All @@ -16,6 +19,5 @@ class LoginButtonPressed extends LoginEvent {
List<Object> get props => [serverURL];

@override
String toString() =>
'LoginButtonPressed {serverURL: $serverURL}';
}
String toString() => 'LoginButtonPressed {serverURL: $serverURL}';
}
12 changes: 9 additions & 3 deletions lib/src/models/app_authentication.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,14 @@ class AppAuthentication {
factory AppAuthentication.fromJson(String jsonString) {
Map<String, dynamic> jsonData = json.decode(jsonString);

String basicAuth = jsonData.containsKey("basicAuth") ? jsonData['basicAuth'] :
'Basic '+base64Encode(utf8.encode('${jsonData["loginName"]}:${jsonData["appPassword"]}'));
String basicAuth = jsonData.containsKey("basicAuth")
? jsonData['basicAuth']
: 'Basic ' +
base64Encode(
utf8.encode(
'${jsonData["loginName"]}:${jsonData["appPassword"]}',
),
);

return AppAuthentication(
server: jsonData["server"],
Expand All @@ -35,4 +41,4 @@ class AppAuthentication {

@override
String toString() => 'LoggedIn { token: $server, $loginName, $basicAuth}';
}
}
2 changes: 2 additions & 0 deletions lib/src/models/category.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ class Category extends Equatable {
final int recipeCount;
String imageUrl;

Category(this.name, this.recipeCount);

Category.fromJson(Map<String, dynamic> json)
: name = json["name"],
recipeCount = json["recipe_count"] is int
Expand Down
112 changes: 72 additions & 40 deletions lib/src/screens/form/login_form.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:nextcloud_cookbook_flutter/src/services/user_repository.dart';

import '../../blocs/login/login.dart';

Expand All @@ -9,7 +11,7 @@ class LoginForm extends StatefulWidget {
State<LoginForm> createState() => _LoginFormState();
}

class _LoginFormState extends State<LoginForm> {
class _LoginFormState extends State<LoginForm> with WidgetsBindingObserver {
final _serverUrl = TextEditingController();
// Create a global key that uniquely identifies the Form widget
// and allows validation of the form.
Expand All @@ -18,8 +20,34 @@ class _LoginFormState extends State<LoginForm> {
// not a GlobalKey<MyCustomFormState>.
final _formKey = GlobalKey<FormState>();

Function authenticateInterruptCallback;

@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
authenticateInterruptCallback();
debugPrint("WAT");
}
}

@override
initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}

@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}

@override
Widget build(BuildContext context) {
authenticateInterruptCallback = () {
UserRepository().stopAuthenticate();
};

_onLoginButtonPressed() {
if (_formKey.currentState.validate()) {
BlocProvider.of<LoginBloc>(context).add(
Expand All @@ -43,45 +71,49 @@ class _LoginFormState extends State<LoginForm> {
},
child: BlocBuilder<LoginBloc, LoginState>(
builder: (context, state) {
return Form(
// Build a Form widget using the _formKey created above.
key: _formKey,
child: Column(
children: [
TextFormField(
decoration: InputDecoration(labelText: 'Server URL'),
controller: _serverUrl,
validator: (value) {
if (value.isEmpty) {
return 'Please enter a Nextcloud URL';
}
var urlPattern =
r"([-A-Z0-9.]+)(/[-A-Z0-9+&@#/%=~_|!:,.;]*)?(\?[A-Z0-9+&@#/%=~_|!:‌​,.;]*)?";
bool _match = new RegExp(urlPattern, caseSensitive: false)
.hasMatch(value);
if (!_match) {
return 'Please enter a valid URL';
}
return null;
},
onFieldSubmitted: (val) {
if (state is! LoginLoading) {
_onLoginButtonPressed();
}
},
textInputAction: TextInputAction.done,
),
RaisedButton(
onPressed:
state is! LoginLoading ? _onLoginButtonPressed : null,
child: Text('Login'),
),
Container(
child: state is LoginLoading
? SpinKitWave(color: Colors.blue, size: 50.0)
: null,
),
],
return Padding(
padding: const EdgeInsets.all(8.0),
child: Form(
// Build a Form widget using the _formKey created above.
key: _formKey,
child: Column(
children: [
TextFormField(
decoration: InputDecoration(labelText: 'Server URL'),
controller: _serverUrl,
keyboardType: TextInputType.url,
validator: (value) {
if (value.isEmpty) {
return 'Please enter a Nextcloud URL';
}
var urlPattern =
r"([-A-Z0-9.]+)(/[-A-Z0-9+&@#/%=~_|!:,.;]*)?(\?[A-Z0-9+&@#/%=~_|!:‌​,.;]*)?";
bool _match = new RegExp(urlPattern, caseSensitive: false)
.hasMatch(value);
if (!_match) {
return 'Please enter a valid URL';
}
return null;
},
onFieldSubmitted: (val) {
if (state is! LoginLoading) {
_onLoginButtonPressed();
}
},
textInputAction: TextInputAction.done,
),
RaisedButton(
onPressed:
state is! LoginLoading ? _onLoginButtonPressed : null,
child: Text('Login'),
),
Container(
child: state is LoginLoading
? SpinKitWave(color: Colors.blue, size: 50.0)
: null,
),
],
),
),
);
},
Expand Down
124 changes: 124 additions & 0 deletions lib/src/services/authentication_provider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import 'dart:async';
import 'dart:convert';

import 'package:dio/dio.dart' as dio;
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart' as http;
import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart';
import 'package:nextcloud_cookbook_flutter/src/models/intial_login.dart';
import 'package:url_launcher/url_launcher.dart';

class AuthenticationProvider {
final FlutterSecureStorage _secureStorage = new FlutterSecureStorage();
final String _appAuthenticationKey = 'appAuthentication';
AppAuthentication currentAppAuthentication;
bool resumeAuthenticate = true;

Future<AppAuthentication> authenticate({
@required String serverUrl,
}) async {
resumeAuthenticate = true;
if (serverUrl.substring(0, 4) != 'http') {
serverUrl = 'https://' + serverUrl;
}
String urlInitialCall = serverUrl + '/index.php/login/v2';
var response;
try {
response = await http.post(urlInitialCall,
headers: {"User-Agent": "Cookbook App", "Accept-Language": "en-US"});
} catch (e) {
throw ('Cannot reach: $serverUrl');
}

if (response.statusCode == 200) {
final initialLogin = InitialLogin.fromJson(json.decode(response.body));

if (await canLaunch(initialLogin.login)) {
_launchURL(initialLogin.login);

String urlLoginSuccess =
initialLogin.poll.endpoint + "?token=" + initialLogin.poll.token;

var responseLog = await http.post(urlLoginSuccess);
while (responseLog.statusCode != 200 && resumeAuthenticate) {
await Future.delayed(Duration(milliseconds: 100));
responseLog = await http.post(urlLoginSuccess);
}

await closeWebView();

if (responseLog.statusCode != 200) {
throw "Login Process was interrupted!";
} else {
return AppAuthentication.fromJson(responseLog.body);
}
} else {
throw 'Could not launch the authentication window.';
}
} else {
throw Exception('Your server Name is not correct');
}
}

void stopAuthenticate() {
resumeAuthenticate = false;
}

Future<bool> hasAppAuthentication() async {
if (currentAppAuthentication != null) {
return true;
} else {
String appAuthentication =
await _secureStorage.read(key: _appAuthenticationKey);
return appAuthentication != null;
}
}

Future<void> loadAppAuthentication() async {
String appAuthenticationString =
await _secureStorage.read(key: _appAuthenticationKey);
if (appAuthenticationString == null) {
throw ("No authentication found in Storage");
} else {
currentAppAuthentication =
AppAuthentication.fromJson(appAuthenticationString);
}
}

Future<void> persistAppAuthentication(
AppAuthentication appAuthentication) async {
currentAppAuthentication = appAuthentication;
await _secureStorage.write(
key: _appAuthenticationKey, value: appAuthentication.toJson());
}

Future<void> deleteAppAuthentication() async {
var response = await dio.Dio().delete(
"${currentAppAuthentication.server}/ocs/v2.php/core/apppassword",
options: new dio.Options(
headers: {
"OCS-APIREQUEST": "true",
"authorization": currentAppAuthentication.basicAuth
},
),
);

if (response.statusCode != 200) {
debugPrint("Failed to remove remote apppassword!");
}

//TODO Delete Appkey Serverside
currentAppAuthentication = null;
await _secureStorage.delete(key: _appAuthenticationKey);
}

Future<void> _launchURL(String url) async {
await launch(
url,
forceSafariVC: true,
forceWebView: true,
enableJavaScript: true,
);
}
}
17 changes: 14 additions & 3 deletions lib/src/services/categories_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class CategoriesProvider {

Future<List<Category>> fetchCategories() async {
AppAuthentication appAuthentication =
UserRepository().currentAppAuthentication;
UserRepository().getCurrentAppAuthentication();

final response = await client.get(
"${appAuthentication.server}/index.php/apps/cookbook/categories",
Expand All @@ -19,8 +19,19 @@ class CategoriesProvider {

if (response.statusCode == 200) {
try {
return Category.parseCategories(response.body)
..sort((a, b) => a.name.compareTo(b.name));
List<Category> categories = Category.parseCategories(response.body);
categories.sort((a, b) => a.name.compareTo(b.name));
categories.insert(
0,
Category(
"All",
categories.fold(
0,
(previousValue, element) =>
previousValue + element.recipeCount),
),
);
return categories;
} catch (e) {
throw Exception(e);
}
Expand Down
Loading

0 comments on commit 981ae56

Please sign in to comment.