Compare commits

...
Author SHA1 Message Date
hugo dffa91eb23 chore(release): publish packages
- wyatt_medium_feeds@1.0.1
2022-05-05 21:29:48 +02:00
hugo 164877bb0b fix(medium): remove useless example file 2022-05-05 21:29:25 +02:00
hugo c787b065ad chore(medium): add new package 2022-05-05 21:26:04 +02:00
hugo 12f9cf6aa5 feat(auth): add google, facebook, apple, twitter login 2022-05-04 22:50:07 +02:00
hugo 74db784973 chore(release): publish packages
- wyatt_form_bloc@0.0.2
 - wyatt_authentication_bloc@0.2.0+1
2022-05-02 14:44:17 +02:00
hugo fe5fa692f7 feat(form): add list option validator 2022-05-02 14:41:24 +02:00
55 changed files with 1678 additions and 101 deletions
+49
View File
@@ -3,6 +3,55 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## 2022-05-05
### Changes
---
Packages with breaking changes:
- There are no breaking changes in this release.
Packages with other changes:
- [`wyatt_medium_feeds` - `v1.0.1`](#wyatt_medium_feeds---v101)
---
#### `wyatt_medium_feeds` - `v1.0.1`
- **FIX**: remove useless example file.
## 2022-05-02
### Changes
---
Packages with breaking changes:
- There are no breaking changes in this release.
Packages with other changes:
- [`wyatt_form_bloc` - `v0.0.2`](#wyatt_form_bloc---v002)
- [`wyatt_authentication_bloc` - `v0.2.0+1`](#wyatt_authentication_bloc---v0201)
Packages with dependency updates only:
> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.
- `wyatt_authentication_bloc` - `v0.2.0+1`
---
#### `wyatt_form_bloc` - `v0.0.2`
- **FEAT**: add list option validator.
## 2022-04-20 ## 2022-04-20
### Changes ### Changes
@@ -1,3 +1,7 @@
## 0.2.0+1
- Update a dependency to the latest release.
## 0.2.0 ## 0.2.0
- Graduate package to a stable release. See pre-releases prior to this version for changelog entries. - Graduate package to a stable release. See pre-releases prior to this version for changelog entries.
@@ -42,6 +42,42 @@
} }
} }
}, },
{
"client_info": {
"mobilesdk_app_id": "1:136771801992:android:8482c9b90bc29de697203d",
"android_client_info": {
"package_name": "com.example.crud_bloc_example"
}
},
"oauth_client": [
{
"client_id": "136771801992-ncuib3rbu7p4ro4eo5su4vaudn2u4qrv.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyAYS14uXupkS158Q5QAFP1864UrUN_yDSk"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "136771801992-ncuib3rbu7p4ro4eo5su4vaudn2u4qrv.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "136771801992-e585bm1n9b3lv89t4phrl9u0glsg52ua.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "com.example.example"
}
}
]
}
}
},
{ {
"client_info": { "client_info": {
"mobilesdk_app_id": "1:136771801992:android:d20e0361057e815197203d", "mobilesdk_app_id": "1:136771801992:android:d20e0361057e815197203d",
@@ -14,6 +14,8 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'dart:developer';
import 'package:authentication_bloc_example/constants.dart'; import 'package:authentication_bloc_example/constants.dart';
import 'package:authentication_bloc_example/home/home_page.dart'; import 'package:authentication_bloc_example/home/home_page.dart';
import 'package:authentication_bloc_example/login/login_page.dart'; import 'package:authentication_bloc_example/login/login_page.dart';
@@ -55,14 +57,32 @@ class App extends StatelessWidget {
if (user.isNotEmpty && !user.isAnonymous) { if (user.isNotEmpty && !user.isAnonymous) {
// Check if user is register in Firesore. // Check if user is register in Firesore.
DocumentSnapshot firestoreUser = await FirebaseFirestore.instance DocumentSnapshot firestoreUser = await FirebaseFirestore.instance
.collection('users') .collection(firestoreCollectionUsers)
.doc(user.uid) .doc(user.uid)
.get(); .get();
if (!firestoreUser.exists) {
// Register user in Firestore when sign in with social account.
final uid = user.uid;
final u = {'uid': uid, 'email': user.email};
await FirebaseFirestore.instance
.collection(firestoreCollectionUsers)
.doc(uid)
.set(u);
return { return {
'user': 'user': UserFirestore(
UserFirestore.fromMap(firestoreUser.data() as Map<String, dynamic>), uid: uid,
email: user.email ?? '',
name: user.displayName ?? '',
phone: user.phoneNumber ?? ''),
};
} else {
return {
'user': UserFirestore.fromMap(
firestoreUser.data() as Map<String, dynamic>),
...firestoreUser.data() as Map<String, dynamic>? ?? {} ...firestoreUser.data() as Map<String, dynamic>? ?? {}
}; };
}
} else { } else {
return {}; return {};
} }
@@ -73,7 +93,11 @@ class App extends StatelessWidget {
if (uid != null) { if (uid != null) {
final data = state.data.toMap(); final data = state.data.toMap();
final user = {'uid': uid, 'email': state.email.value, ...data}; final user = {'uid': uid, 'email': state.email.value, ...data};
await FirebaseFirestore.instance.collection('users').doc(uid).set(user); log('onSignUpSuccess: $user');
await FirebaseFirestore.instance
.collection(firestoreCollectionUsers)
.doc(uid)
.set(user);
} }
} }
@@ -20,3 +20,5 @@ const String formFieldPro = 'isPro';
const String formFieldConfirmedPassword = 'confirmedPassword'; const String formFieldConfirmedPassword = 'confirmedPassword';
const String formFieldSiren = 'siren'; const String formFieldSiren = 'siren';
const String formFieldIban = 'iban'; const String formFieldIban = 'iban';
const String firestoreCollectionUsers = 'authentication_bloc_users';
@@ -102,6 +102,24 @@ class _LoginWithPasswordButton extends StatelessWidget {
} }
} }
class _LoginWithGoogleButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<SignInCubit, SignInState>(
buildWhen: (previous, current) => previous.status != current.status,
builder: (context, state) {
return state.status.isSubmissionInProgress
? const CircularProgressIndicator()
: ElevatedButton(
onPressed: () =>
context.read<SignInCubit>().signInWithGoogle(),
child: const Text('LOGIN GOOGLE'),
);
},
);
}
}
class _SignUpButton extends StatelessWidget { class _SignUpButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -182,6 +200,8 @@ class LoginForm extends StatelessWidget {
const SizedBox(height: 8), const SizedBox(height: 8),
_LoginAnonButton(), _LoginAnonButton(),
const SizedBox(height: 8), const SizedBox(height: 8),
_LoginWithGoogleButton(),
const SizedBox(height: 8),
_SignUpButton(), _SignUpButton(),
const SizedBox(height: 8), const SizedBox(height: 8),
_SignUpAsProButton(), _SignUpAsProButton(),
@@ -255,7 +255,8 @@ class _DebugButton extends StatelessWidget {
builder: (context, state) { builder: (context, state) {
return ElevatedButton( return ElevatedButton(
onPressed: () { onPressed: () {
log(state.toString()); // log(state.toString());
log(state.data.toMap().toString());
}, },
child: const Text('DEBUG'), child: const Text('DEBUG'),
); );
@@ -38,7 +38,7 @@ dependencies:
wyatt_form_bloc: wyatt_form_bloc:
git: git:
url: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages url: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages
ref: wyatt_form_bloc-v0.0.1 ref: wyatt_form_bloc-v0.0.2
path: packages/wyatt_form_bloc path: packages/wyatt_form_bloc
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
@@ -106,10 +106,12 @@ class SignInAnonymouslyFailureFirebase
} }
} }
class SignInWithGoogleFailureFirebase extends SignInWithGoogleFailureInterface { class SignInWithCredentialFailureFirebase
SignInWithGoogleFailureFirebase([String? code, String? message]) extends SignInWithCredentialFailureInterface {
SignInWithCredentialFailureFirebase([String? code, String? message])
: super(code ?? 'unknown', message ?? 'An unknown error occurred.'); : super(code ?? 'unknown', message ?? 'An unknown error occurred.');
SignInWithGoogleFailureFirebase.fromCode(String code) : super.fromCode(code) { SignInWithCredentialFailureFirebase.fromCode(String code)
: super.fromCode(code) {
switch (code) { switch (code) {
case 'account-exists-with-different-credential': case 'account-exists-with-different-credential':
message = 'Account exists with different credentials.'; message = 'Account exists with different credentials.';
@@ -143,6 +145,38 @@ class SignInWithGoogleFailureFirebase extends SignInWithGoogleFailureInterface {
} }
} }
class SignInWithGoogleFailureFirebase
extends SignInWithCredentialFailureFirebase
implements SignInWithGoogleFailureInterface {
SignInWithGoogleFailureFirebase([String? code, String? message])
: super(code, message);
SignInWithGoogleFailureFirebase.fromCode(String code) : super.fromCode(code);
}
class SignInWithFacebookFailureFirebase
extends SignInWithCredentialFailureFirebase
implements SignInWithFacebookFailureInterface {
SignInWithFacebookFailureFirebase([String? code, String? message])
: super(code, message);
SignInWithFacebookFailureFirebase.fromCode(String code)
: super.fromCode(code);
}
class SignInWithAppleFailureFirebase extends SignInWithCredentialFailureFirebase
implements SignInWithAppleFailureInterface {
SignInWithAppleFailureFirebase([String? code, String? message])
: super(code, message);
SignInWithAppleFailureFirebase.fromCode(String code) : super.fromCode(code);
}
class SignInWithTwitterFailureFirebase extends SignInWithCredentialFailureFirebase
implements SignInWithAppleFailureInterface {
SignInWithTwitterFailureFirebase([String? code, String? message])
: super(code, message);
SignInWithTwitterFailureFirebase.fromCode(String code) : super.fromCode(code);
}
class SignInWithEmailLinkFailureFirebase class SignInWithEmailLinkFailureFirebase
extends SignInWithEmailLinkFailureInterface { extends SignInWithEmailLinkFailureInterface {
SignInWithEmailLinkFailureFirebase([String? code, String? message]) SignInWithEmailLinkFailureFirebase([String? code, String? message])
@@ -64,6 +64,20 @@ abstract class FetchSignInMethodsForEmailFailureInterface
: super.fromCode(code); : super.fromCode(code);
} }
/// {@template sign_in_with_credential_failure}
/// Thrown during the sign in process if a failure occurs.
/// {@endtemplate}
abstract class SignInWithCredentialFailureInterface
extends AuthenticationFailureInterface {
/// {@macro sign_in_with_credential_failure}
SignInWithCredentialFailureInterface(String code, String message)
: super(code, message);
/// {@macro sign_in_with_credential_failure}
SignInWithCredentialFailureInterface.fromCode(String code)
: super.fromCode(code);
}
/// {@template sign_in_anonymously_failure} /// {@template sign_in_anonymously_failure}
/// Thrown during the sign in process if a failure occurs. /// Thrown during the sign in process if a failure occurs.
/// {@endtemplate} /// {@endtemplate}
@@ -91,6 +105,47 @@ abstract class SignInWithGoogleFailureInterface
SignInWithGoogleFailureInterface.fromCode(String code) : super.fromCode(code); SignInWithGoogleFailureInterface.fromCode(String code) : super.fromCode(code);
} }
/// {@template sign_in_with_facebook_failure}
/// Thrown during the sign in process if a failure occurs.
/// {@endtemplate}
abstract class SignInWithFacebookFailureInterface
extends AuthenticationFailureInterface {
/// {@macro sign_in_with_facebook_failure}
SignInWithFacebookFailureInterface(String code, String message)
: super(code, message);
/// {@macro sign_in_with_facebook_failure}
SignInWithFacebookFailureInterface.fromCode(String code)
: super.fromCode(code);
}
/// {@template sign_in_with_apple_failure}
/// Thrown during the sign in process if a failure occurs.
/// {@endtemplate}
abstract class SignInWithAppleFailureInterface
extends AuthenticationFailureInterface {
/// {@macro sign_in_with_apple_failure}
SignInWithAppleFailureInterface(String code, String message)
: super(code, message);
/// {@macro sign_in_with_apple_failure}
SignInWithAppleFailureInterface.fromCode(String code) : super.fromCode(code);
}
/// {@template sign_in_with_twitter_failure}
/// Thrown during the sign in process if a failure occurs.
/// {@endtemplate}
abstract class SignInWithTwitterFailureInterface
extends AuthenticationFailureInterface {
/// {@macro sign_in_with_twitter_failure}
SignInWithTwitterFailureInterface(String code, String message)
: super(code, message);
/// {@macro sign_in_with_twitter_failure}
SignInWithTwitterFailureInterface.fromCode(String code)
: super.fromCode(code);
}
/// {@template sign_in_with_email_link_failure} /// {@template sign_in_with_email_link_failure}
/// Thrown during the sign in process if a failure occurs. /// Thrown during the sign in process if a failure occurs.
/// {@endtemplate} /// {@endtemplate}
@@ -64,6 +64,9 @@ class UserFirebase implements UserInterface {
@override @override
String get uid => _user?.uid ?? ''; String get uid => _user?.uid ?? '';
@override
String? get providerId => _user?.providerData.first.providerId;
@override @override
bool? get isNewUser { bool? get isNewUser {
if (_user?.metadata.lastSignInTime == null || if (_user?.metadata.lastSignInTime == null ||
@@ -71,6 +71,9 @@ abstract class UserInterface {
/// The user's unique ID. /// The user's unique ID.
String get uid; String get uid;
/// The provider ID for the user.
String? get providerId;
/// Whether the user account has been recently created. /// Whether the user account has been recently created.
bool? get isNewUser; bool? get isNewUser;
@@ -15,19 +15,28 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_facebook_auth/flutter_facebook_auth.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
import 'package:twitter_login/twitter_login.dart';
import 'package:wyatt_authentication_bloc/src/models/exceptions/exceptions_firebase.dart'; import 'package:wyatt_authentication_bloc/src/models/exceptions/exceptions_firebase.dart';
import 'package:wyatt_authentication_bloc/src/models/user/user_firebase.dart'; import 'package:wyatt_authentication_bloc/src/models/user/user_firebase.dart';
import 'package:wyatt_authentication_bloc/src/models/user/user_interface.dart'; import 'package:wyatt_authentication_bloc/src/models/user/user_interface.dart';
import 'package:wyatt_authentication_bloc/src/repositories/authentication_repository_interface.dart'; import 'package:wyatt_authentication_bloc/src/repositories/authentication_repository_interface.dart';
import 'package:wyatt_authentication_bloc/src/utils/cryptography.dart';
class AuthenticationRepositoryFirebase class AuthenticationRepositoryFirebase
implements AuthenticationRepositoryInterface { implements AuthenticationRepositoryInterface {
final FirebaseAuth _firebaseAuth; final FirebaseAuth _firebaseAuth;
final TwitterLogin? _twitterLogin;
UserFirebase _userCache = const UserFirebase.empty(); UserFirebase _userCache = const UserFirebase.empty();
AuthenticationRepositoryFirebase({FirebaseAuth? firebaseAuth}) AuthenticationRepositoryFirebase({
: _firebaseAuth = firebaseAuth ?? FirebaseAuth.instance; FirebaseAuth? firebaseAuth,
TwitterLogin? twitterLogin,
}) : _firebaseAuth = firebaseAuth ?? FirebaseAuth.instance,
_twitterLogin = twitterLogin;
@override @override
Stream<UserInterface> get user { Stream<UserInterface> get user {
@@ -99,9 +108,106 @@ class AuthenticationRepositoryFirebase
} }
@override @override
Future<void> signInWithGoogle() { Future<void> signInWithGoogle() async {
// TODO(hpcl): implement signInWithGoogle // Trigger the authentication flow
throw UnimplementedError(); final GoogleSignInAccount? googleUser = await GoogleSignIn().signIn();
// Obtain the auth details from the request
final GoogleSignInAuthentication? googleAuth =
await googleUser?.authentication;
// Create a new credential
final credential = GoogleAuthProvider.credential(
accessToken: googleAuth?.accessToken,
idToken: googleAuth?.idToken,
);
try {
await _firebaseAuth.signInWithCredential(credential);
} on FirebaseAuthException catch (e) {
throw SignInWithGoogleFailureFirebase.fromCode(e.code);
} catch (_) {
throw SignInWithGoogleFailureFirebase();
}
}
@override
Future<void> signInWithFacebook() async {
// Trigger the sign-in flow
final LoginResult loginResult = await FacebookAuth.instance.login();
// Create a credential from the access token
final OAuthCredential credential =
FacebookAuthProvider.credential(loginResult.accessToken?.token ?? '');
try {
await _firebaseAuth.signInWithCredential(credential);
} on FirebaseAuthException catch (e) {
throw SignInWithFacebookFailureFirebase.fromCode(e.code);
} catch (_) {
throw SignInWithFacebookFailureFirebase();
}
}
@override
Future<void> signInWithApple() async {
// To prevent replay attacks with the credential returned from Apple, we
// include a nonce in the credential request. When signing in with
// Firebase, the nonce in the id token returned by Apple, is expected to
// match the sha256 hash of `rawNonce`.
final rawNonce = Cryptography.generateNonce();
final nonce = Cryptography.sha256ofString(rawNonce);
// Request credential for the currently signed in Apple account.
final appleCredential = await SignInWithApple.getAppleIDCredential(
scopes: [
AppleIDAuthorizationScopes.email,
AppleIDAuthorizationScopes.fullName,
],
nonce: nonce,
);
// Create an `OAuthCredential` from the credential returned by Apple.
final credential = OAuthProvider('apple.com').credential(
idToken: appleCredential.identityToken,
rawNonce: rawNonce,
);
// Sign in the user with Firebase. If the nonce we generated earlier does
// not match the nonce in `appleCredential.identityToken`,
// sign in will fail.
try {
await _firebaseAuth.signInWithCredential(credential);
} on FirebaseAuthException catch (e) {
throw SignInWithAppleFailureFirebase.fromCode(e.code);
} catch (_) {
throw SignInWithAppleFailureFirebase();
}
}
@override
Future<void> signInWithTwitter() async {
final twitterLogin = _twitterLogin;
if (twitterLogin == null) {
throw SignInWithTwitterFailureFirebase();
}
// Trigger the sign-in flow
final authResult = await twitterLogin.login();
// Create a credential from the access token
final credential = TwitterAuthProvider.credential(
accessToken: authResult.authToken!,
secret: authResult.authTokenSecret!,
);
try {
await _firebaseAuth.signInWithCredential(credential);
} on FirebaseAuthException catch (e) {
throw SignInWithCredentialFailureFirebase.fromCode(e.code);
} catch (_) {
throw SignInWithCredentialFailureFirebase();
}
} }
@override @override
@@ -61,6 +61,21 @@ abstract class AuthenticationRepositoryInterface {
/// Throws a [SignInWithGoogleFailureInterface] if an exception occurs. /// Throws a [SignInWithGoogleFailureInterface] if an exception occurs.
Future<void> signInWithGoogle(); Future<void> signInWithGoogle();
/// Starts the Sign In with Facebook Flow.
///
/// Throws a [SignInWithFacebookFailureInterface] if an exception occurs.
Future<void> signInWithFacebook();
/// Starts the Sign In with Apple Flow.
///
/// Throws a [SignInWithAppleFailureInterface] if an exception occurs.
Future<void> signInWithApple();
/// Starts the Sign In with Twitter Flow.
///
/// Throws a [SignInWithTwitterFailureInterface] if an exception occurs.
Future<void> signInWithTwitter();
/// Signs in using an email address and email sign-in link. /// Signs in using an email address and email sign-in link.
/// ///
/// Throws a [SignInWithEmailLinkFailureInterface] if an exception occurs. /// Throws a [SignInWithEmailLinkFailureInterface] if an exception occurs.
@@ -76,6 +76,28 @@ class SignInCubit extends Cubit<SignInState> {
} }
} }
Future<void> signInWithGoogle() async {
if (state.status.isSubmissionInProgress) {
return;
}
emit(state.copyWith(status: FormStatus.submissionInProgress));
try {
await _authenticationRepository.signInWithGoogle();
_authenticationCubit.start();
emit(state.copyWith(status: FormStatus.submissionSuccess));
} on SignInWithGoogleFailureInterface catch (e) {
emit(
state.copyWith(
errorMessage: e.message,
status: FormStatus.submissionFailure,
),
);
} catch (_) {
emit(state.copyWith(status: FormStatus.submissionFailure));
}
}
Future<void> signInWithEmailAndPassword() async { Future<void> signInWithEmailAndPassword() async {
if (!state.status.isValidated) return; if (!state.status.isValidated) return;
emit(state.copyWith(status: FormStatus.submissionInProgress)); emit(state.copyWith(status: FormStatus.submissionInProgress));
@@ -45,4 +45,14 @@ class SignInState extends Equatable {
@override @override
List<Object> get props => [email, password, status]; List<Object> get props => [email, password, status];
@override
String toString() {
return '''
email: $email,
password: $password,
status: $status,
errorMessage: $errorMessage,
''';
}
} }
@@ -0,0 +1,39 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'dart:convert';
import 'dart:math';
import 'package:crypto/crypto.dart';
class Cryptography {
/// Generates a cryptographically secure random nonce, to be included in a
/// credential request.
static String generateNonce([int length = 32]) {
const charset =
'0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._';
final random = Random.secure();
return List.generate(length, (_) => charset[random.nextInt(charset.length)])
.join();
}
/// Returns the sha256 hash of [input] in hex notation.
static String sha256ofString(String input) {
final bytes = utf8.encode(input);
final digest = sha256.convert(bytes);
return digest.toString();
}
}
@@ -1,7 +1,7 @@
name: wyatt_authentication_bloc name: wyatt_authentication_bloc
description: Authentication BLoC for Flutter description: Authentication BLoC for Flutter
repository: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages/src/branch/master/packages/wyatt_authentication_bloc repository: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages/src/branch/master/packages/wyatt_authentication_bloc
version: 0.2.0 version: 0.2.0+1
environment: environment:
sdk: ">=2.15.1 <3.0.0" sdk: ">=2.15.1 <3.0.0"
@@ -11,14 +11,19 @@ dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
crypto: ^3.0.2
flutter_bloc: ^8.0.1 flutter_bloc: ^8.0.1
equatable: ^2.0.3 equatable: ^2.0.3
firebase_auth: ^3.3.14 firebase_auth: ^3.3.17
google_sign_in: ^5.3.0
flutter_facebook_auth: ^4.3.0
sign_in_with_apple: ^3.3.0
twitter_login: ^4.2.3
wyatt_form_bloc: wyatt_form_bloc:
git: git:
url: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages url: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages
ref: wyatt_form_bloc-v0.0.1 ref: wyatt_form_bloc-v0.0.2
path: packages/wyatt_form_bloc path: packages/wyatt_form_bloc
+4
View File
@@ -1,3 +1,7 @@
## 0.0.2
- **FEAT**: add list option validator.
## 0.0.1 ## 0.0.1
- Graduate package to a stable release. See pre-releases prior to this version for changelog entries. - Graduate package to a stable release. See pre-releases prior to this version for changelog entries.
@@ -14,50 +14,48 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:form_bloc_example/constants.dart'; import 'package:form_bloc_example/constants.dart';
import 'package:form_bloc_example/cubit/custom_form_cubit.dart';
import 'package:form_bloc_example/sign_up/sign_up_page.dart'; import 'package:form_bloc_example/sign_up/sign_up_page.dart';
import 'package:wyatt_form_bloc/wyatt_form_bloc.dart'; import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
class App extends StatelessWidget { class App extends StatelessWidget {
const App({Key? key}) : super(key: key); const App({Key? key}) : super(key: key);
static FormData getNormalFormData() { static List<FormEntry> getNormalEntries() {
return const FormData([ return const [
FormEntry(formFieldName, Name.pure()), FormEntry(formFieldName, Name.pure()),
FormEntry(formFieldEmail, Email.pure()), FormEntry(formFieldEmail, Email.pure()),
FormEntry(formFieldPhone, Phone.pure()), FormEntry(formFieldPhone, Phone.pure()),
FormEntry(
formFieldList, ListOption<String>.pure(defaultValue: ['checkbox3'])),
FormEntry(formFieldRadio, TextString.pure()),
FormEntry(formFieldPro, Boolean.pure(), name: 'business'), FormEntry(formFieldPro, Boolean.pure(), name: 'business'),
FormEntry(formFieldHidden, Boolean.pure(), export: false), FormEntry(formFieldHidden, Boolean.pure(), export: false),
]); ];
}
static List<FormEntry> getBusinessEntries() {
const entries = [
FormEntry(formFieldSiren, Siren.pure()),
FormEntry(formFieldIban, Iban.pure()),
];
return getNormalEntries() + entries;
}
static FormData getNormalFormData() {
return FormData(getNormalEntries());
} }
static FormData getProFormData() { static FormData getProFormData() {
return const FormData([ return FormData(getBusinessEntries());
FormEntry(formFieldName, Name.pure()),
FormEntry(formFieldEmail, Email.pure()),
FormEntry(formFieldPhone, Phone.pure()),
FormEntry(formFieldPro, Boolean.pure(), name: 'business'),
FormEntry(formFieldHidden, Boolean.pure(), export: false),
FormEntry(formFieldSiren, Siren.pure()),
FormEntry(formFieldIban, Iban.pure()),
]);
}
Future<bool> onSubmit(FormDataState state) async {
log(state.data.toMap().toString());
return true;
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
FormDataCubit _formCubit = FormDataCubit( FormDataCubit _formCubit = CustomFormCubit(entries: getNormalFormData());
entries: getNormalFormData(),
onSubmit: onSubmit,
);
return BlocProvider( return BlocProvider(
create: (context) => _formCubit, create: (context) => _formCubit,
@@ -19,6 +19,8 @@ const String formFieldPhone = 'phone';
const String formFieldEmail = 'email'; const String formFieldEmail = 'email';
const String formFieldSiren = 'siren'; const String formFieldSiren = 'siren';
const String formFieldIban = 'iban'; const String formFieldIban = 'iban';
const String formFieldList = 'list';
const String formFieldRadio = 'radio';
const String formFieldHidden = 'hidden'; const String formFieldHidden = 'hidden';
const String formFieldPro = 'isPro'; const String formFieldPro = 'isPro';
@@ -0,0 +1,33 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'dart:developer';
import 'package:meta/meta.dart';
import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
part 'custom_form_state.dart';
class CustomFormCubit extends FormDataCubit {
CustomFormCubit({required FormData entries}) : super(entries: entries);
@override
Future<void> submitForm() {
log(state.data.toMap().toString());
return Future.value();
}
}
@@ -0,0 +1,22 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
part of 'custom_form_cubit.dart';
@immutable
abstract class CustomFormState {}
class CustomFormInitial extends CustomFormState {}
@@ -135,6 +135,112 @@ class _IbanInput extends StatelessWidget {
} }
} }
class _CheckListInput extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<FormDataCubit, FormDataState>(
builder: (context, state) {
final _input =
state.data.input<List<String>>(formFieldList) as ListOption<String>;
final _options = _input.value;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: const Text('Checkbox1'),
trailing: Checkbox(
value: _options.contains('checkbox1'),
onChanged: (_) {
context.read<FormDataCubit>().dataChanged(
formFieldList,
_input.select('checkbox1'),
);
}),
),
ListTile(
title: const Text('Checkbox2'),
trailing: Checkbox(
value: _options.contains('checkbox2'),
onChanged: (_) {
context.read<FormDataCubit>().dataChanged(
formFieldList,
_input.select('checkbox2'),
);
}),
),
ListTile(
title: const Text('Checkbox3 (default)'),
trailing: Checkbox(
value: _options.contains('checkbox3'),
onChanged: (_) {
context.read<FormDataCubit>().dataChanged(
formFieldList,
_input.select('checkbox3'),
);
}),
),
],
);
},
);
}
}
class _RadioListInput extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<FormDataCubit, FormDataState>(
builder: (context, state) {
final _input =
state.data.input<String>(formFieldRadio) as TextString;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: const Text('Radio1'),
trailing: Radio<bool>(
groupValue: true,
value: _input.value == 'radio1',
onChanged: (_) {
context.read<FormDataCubit>().dataChanged(
formFieldRadio,
const TextString.dirty('radio1'),
);
}),
),
ListTile(
title: const Text('Radio2'),
trailing: Radio<bool>(
groupValue: true,
value: _input.value == 'radio2',
onChanged: (_) {
context.read<FormDataCubit>().dataChanged(
formFieldRadio,
const TextString.dirty('radio2'),
);
}),
),
ListTile(
title: const Text('Radio3'),
trailing: Radio<bool>(
groupValue: true,
value: _input.value == 'radio3',
onChanged: (_) {
context.read<FormDataCubit>().dataChanged(
formFieldRadio,
const TextString.dirty('radio3'),
);
}),
),
],
);
},
);
}
}
class _CheckHiddenInput extends StatelessWidget { class _CheckHiddenInput extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -201,8 +307,7 @@ class _SignUpButton extends StatelessWidget {
? const CircularProgressIndicator() ? const CircularProgressIndicator()
: ElevatedButton( : ElevatedButton(
onPressed: state.status.isValidated onPressed: state.status.isValidated
? () => ? () => context.read<FormDataCubit>().submitForm()
context.read<FormDataCubit>().submitForm()
: null, : null,
child: const Text('SIGN UP'), child: const Text('SIGN UP'),
); );
@@ -244,8 +349,6 @@ class SignUpForm extends StatelessWidget {
); );
} }
}, },
child: SizedBox(
height: MediaQuery.of(context).size.height,
child: Align( child: Align(
alignment: const Alignment(0, -1 / 3), alignment: const Alignment(0, -1 / 3),
child: Column( child: Column(
@@ -257,6 +360,10 @@ class SignUpForm extends StatelessWidget {
const SizedBox(height: 8), const SizedBox(height: 8),
_PhoneInput(), _PhoneInput(),
const SizedBox(height: 8), const SizedBox(height: 8),
_CheckListInput(),
const SizedBox(height: 8),
_RadioListInput(),
const SizedBox(height: 8),
_CheckHiddenInput(), _CheckHiddenInput(),
const SizedBox(height: 8), const SizedBox(height: 8),
_CheckIsProInput(), _CheckIsProInput(),
@@ -281,7 +388,6 @@ class SignUpForm extends StatelessWidget {
], ],
), ),
), ),
),
); );
} }
} }
@@ -23,16 +23,16 @@ import 'package:wyatt_form_bloc/src/form/form.dart';
part 'form_data_state.dart'; part 'form_data_state.dart';
class FormDataCubit extends Cubit<FormDataState> { abstract class FormDataCubit extends Cubit<FormDataState> {
final Future<bool> Function(FormDataState state)? _onSubmit; FormDataCubit({required FormData entries})
: super(FormDataState(data: entries));
FormDataCubit({ /// Change value of a field.
required FormData entries, ///
Future<bool> Function(FormDataState state)? onSubmit, /// Inputs:
}) : _onSubmit = onSubmit, /// - `field`: The key of the field to change.
super(FormDataState(data: entries)); /// - `dirtyValue`: The new value of the field.
void dataChanged(String field, FormInput dirtyValue) {
void dataChanged<T>(String field, FormInput dirtyValue) {
final _form = state.data.clone(); final _form = state.data.clone();
if (_form.contains(field)) { if (_form.contains(field)) {
@@ -49,6 +49,11 @@ class FormDataCubit extends Cubit<FormDataState> {
); );
} }
/// Update entries list.
///
/// Inputs:
/// - `data`: The new entries list.
/// - `operation`: The operation to perform on the entries set.
void updateFormData( void updateFormData(
FormData data, { FormData data, {
SetOperation operation = SetOperation.replace, SetOperation operation = SetOperation.replace,
@@ -78,13 +83,6 @@ class FormDataCubit extends Cubit<FormDataState> {
); );
} }
Future<void> submitForm() async { /// Submit the form.
unawaited( Future<void> submitForm();
_onSubmit?.call(state).then((bool reemit) {
if (reemit) {
emit(state);
}
}),
);
}
} }
@@ -0,0 +1,48 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:wyatt_form_bloc/src/enums/enums.dart';
import 'package:wyatt_form_bloc/src/form/form.dart';
/// {@template list_option}
/// Form input for a list input
/// {@endtemplate}
class ListOption<T> extends FormInput<List<T>, ValidationError> {
/// {@macro list_option}
const ListOption.pure({List<T>? defaultValue})
: super.pure(defaultValue ?? const []);
/// {@macro list_option}
const ListOption.dirty({List<T>? value}) : super.dirty(value ?? const []);
ListOption select(T? v) {
if (v == null) {
return this;
}
if (value.contains(v)) {
final List<T> newValue = List.from(value)..remove(v);
return ListOption<T>.dirty(value: newValue);
} else {
final List<T> newValue = List.from(value)..add(v);
return ListOption<T>.dirty(value: newValue);
}
}
@override
ValidationError? validator(List<T>? value) {
return value?.isNotEmpty == true ? null : ValidationError.invalid;
}
}
@@ -18,6 +18,7 @@ export 'boolean.dart';
export 'confirmed_password.dart'; export 'confirmed_password.dart';
export 'email.dart'; export 'email.dart';
export 'iban.dart'; export 'iban.dart';
export 'list_option.dart';
export 'name.dart'; export 'name.dart';
export 'password.dart'; export 'password.dart';
export 'phone.dart'; export 'phone.dart';
+1 -1
View File
@@ -1,7 +1,7 @@
name: wyatt_form_bloc name: wyatt_form_bloc
description: Manage forms in Dart & Flutter with Bloc description: Manage forms in Dart & Flutter with Bloc
repository: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages/src/branch/master/packages/wyatt_form_bloc repository: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages/src/branch/master/packages/wyatt_form_bloc
version: 0.0.1 version: 0.0.2
environment: environment:
sdk: '>=2.16.2 <3.0.0' sdk: '>=2.16.2 <3.0.0'
+10
View File
@@ -0,0 +1,10 @@
# Files and directories created by pub.
.dart_tool/
.packages
# Conventional directory for build outputs.
build/
# Omit committing pubspec.lock for library packages; see
# https://dart.dev/guides/libraries/private-files#pubspeclock.
pubspec.lock
+24
View File
@@ -0,0 +1,24 @@
/*
* Copyright (C) 2022 WYATT GROUP
* Please see the AUTHORS file for details.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
{
"recommendations": [
"psioniq.psi-header",
"blaugold.melos-code"
]
}
+71
View File
@@ -0,0 +1,71 @@
{
"psi-header.changes-tracking": {
"isActive": true
},
"psi-header.config": {
"blankLinesAfter": 1,
"forceToTop": true
},
"psi-header.lang-config": [
{
"beforeHeader": [
"# -*- coding:utf-8 -*-",
"#!/usr/bin/env python3"
],
"begin": "###",
"end": "###",
"language": "python",
"prefix": "# "
},
{
"beforeHeader": [
"#!/usr/bin/env sh",
""
],
"language": "shellscript",
"begin": "",
"end": "",
"prefix": "# "
},
{
"begin": "",
"end": "",
"language": "dart",
"prefix": "// "
},
{
"begin": "",
"end": "",
"language": "yaml",
"prefix": "# "
},
{
"begin": "<!--",
"end": "-->",
"language": "markdown",
},
],
"psi-header.templates": [
{
"language": "*",
"template": [
"Copyright (C) <<year>> WYATT GROUP",
"Please see the AUTHORS file for details.",
"",
"This program is free software: you can redistribute it and/or modify",
"it under the terms of the GNU General Public License as published by",
"the Free Software Foundation, either version 3 of the License, or",
"any later version.",
"",
"This program is distributed in the hope that it will be useful,",
"but WITHOUT ANY WARRANTY; without even the implied warranty of",
"MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the",
"GNU General Public License for more details.",
"",
"You should have received a copy of the GNU General Public License",
"along with this program. If not, see <https://www.gnu.org/licenses/>."
],
}
],
"dart.runPubGetOnPubspecChanges": false,
}
+7
View File
@@ -0,0 +1,7 @@
## 1.0.1
- **FIX**: remove useless example file.
## 1.0.0
- Initial version.
+39
View File
@@ -0,0 +1,39 @@
<!--
This README describes the package. If you publish this package to pub.dev,
this README's contents appear on the landing page for your package.
For information about how to write a good package README, see the guide for
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
For general information about developing packages, see the Dart guide for
[creating packages](https://dart.dev/guides/libraries/create-library-packages)
and the Flutter guide for
[developing packages and plugins](https://flutter.dev/developing-packages).
-->
TODO: Put a short description of the package here that helps potential users
know whether this package might be useful for them.
## Features
TODO: List what your package can do. Maybe include images, gifs, or videos.
## Getting started
TODO: List prerequisites and provide or point to information on how to
start using the package.
## Usage
TODO: Include short and useful examples for package users. Add longer examples
to `/example` folder.
```dart
const like = 'sample';
```
## Additional information
TODO: Tell users more about the package: where to find more information, how to
contribute to the package, how to file issues, what response they can expect
from the package authors, and more.
@@ -0,0 +1 @@
include: package:wyatt_analysis/analysis_options.flutter.yaml
@@ -0,0 +1,46 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Web related
lib/generated_plugin_registrant.dart
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
@@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: 5464c5bac742001448fe4fc0597be939379f88ea
channel: stable
project_type: app
@@ -0,0 +1,16 @@
# medium_feeds_example
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)
For help getting started with Flutter, view our
[online documentation](https://flutter.dev/docs), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
@@ -0,0 +1,29 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at
# https://dart-lang.github.io/linter/lints/index.html.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
@@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
class ArticleTile extends StatelessWidget {
const ArticleTile({
Key? key,
required this.bannerUrl,
required this.title,
required this.summary,
required this.author,
required this.publishDate,
required this.readingTime,
}) : super(key: key);
final String bannerUrl;
final String title;
final String summary;
final String author;
final String publishDate;
final String readingTime;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Container(
height: 200,
decoration: BoxDecoration(
image: DecorationImage(
image: NetworkImage(bannerUrl),
fit: BoxFit.cover,
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.headline6,
),
const SizedBox(height: 8),
Text(
summary,
style: Theme.of(context).textTheme.bodyText1,
),
const SizedBox(height: 8),
Row(
children: [
Text(
author,
style: Theme.of(context).textTheme.caption,
),
const SizedBox(width: 8),
Text(
publishDate,
style: Theme.of(context).textTheme.caption,
),
const SizedBox(width: 8),
Text(
readingTime,
style: Theme.of(context).textTheme.caption,
),
],
),
],
),
),
],
),
);
}
}
@@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import 'package:medium_feeds_example/article_tile.dart';
import 'package:wyatt_medium_feeds/wyatt_medium_feeds.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const WidgetTree(),
);
}
}
class ArticleGrid extends StatelessWidget {
const ArticleGrid({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final mediumFeed = MediumFeed.fromPublicationName('flutter');
return FutureBuilder(
future: mediumFeed.parse(),
builder: (context, snapshot) {
if (snapshot.hasData) {
final articles = mediumFeed.articles;
return GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 1,
),
itemCount: articles.length,
itemBuilder: (context, index) {
final article = articles[index];
return ArticleTile(
bannerUrl: article.images.isNotEmpty ? article.images[0] : '',
author: article.author,
title: article.title,
summary: article.summary,
readingTime:
article.readingTime.inMinutes.toString() + ' min read',
publishDate: article.publishDate!.day.toString() +
'/' +
article.publishDate!.month.toString() +
'/' +
article.publishDate!.year.toString(),
);
},
);
} else {
return const Center(child: CircularProgressIndicator());
}
});
}
}
class WidgetTree extends StatelessWidget {
const WidgetTree({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Medium Feeds Example'),
),
body: const ArticleGrid(),
);
}
}
@@ -0,0 +1,93 @@
name: medium_feeds_example
description: A new Flutter project.
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.0.0+1
environment:
sdk: ">=2.16.2 <3.0.0"
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
wyatt_medium_feeds:
path: ../
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^1.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware.
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages
@@ -0,0 +1,30 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility that Flutter provides. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:medium_feeds_example/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@@ -0,0 +1,104 @@
<!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="medium_feeds_example">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<title>medium_feeds_example</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
<!-- This script installs service_worker.js to provide PWA functionality to
application. For more information, see:
https://developers.google.com/web/fundamentals/primers/service-workers -->
<script>
var serviceWorkerVersion = null;
var scriptLoaded = false;
function loadMainDartJs() {
if (scriptLoaded) {
return;
}
scriptLoaded = true;
var scriptTag = document.createElement('script');
scriptTag.src = 'main.dart.js';
scriptTag.type = 'application/javascript';
document.body.append(scriptTag);
}
if ('serviceWorker' in navigator) {
// Service workers are supported. Use them.
window.addEventListener('load', function () {
// Wait for registration to finish before dropping the <script> tag.
// Otherwise, the browser will load the script multiple times,
// potentially different versions.
var serviceWorkerUrl = 'flutter_service_worker.js?v=' + serviceWorkerVersion;
navigator.serviceWorker.register(serviceWorkerUrl)
.then((reg) => {
function waitForActivation(serviceWorker) {
serviceWorker.addEventListener('statechange', () => {
if (serviceWorker.state == 'activated') {
console.log('Installed new service worker.');
loadMainDartJs();
}
});
}
if (!reg.active && (reg.installing || reg.waiting)) {
// No active web worker and we have installed or are installing
// one for the first time. Simply wait for it to activate.
waitForActivation(reg.installing || reg.waiting);
} else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {
// When the app updates the serviceWorkerVersion changes, so we
// need to ask the service worker to update.
console.log('New service worker available.');
reg.update();
waitForActivation(reg.installing);
} else {
// Existing service worker is still good.
console.log('Loading app from service worker.');
loadMainDartJs();
}
});
// If service worker doesn't succeed in a reasonable amount of time,
// fallback to plaint <script> tag.
setTimeout(() => {
if (!scriptLoaded) {
console.warn(
'Failed to load app from service worker. Falling back to plain <script> tag.',
);
loadMainDartJs();
}
}, 4000);
});
} else {
// Service workers not supported. Just drop the <script> tag.
loadMainDartJs();
}
</script>
</body>
</html>
@@ -0,0 +1,35 @@
{
"name": "medium_feeds_example",
"short_name": "medium_feeds_example",
"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"
}
]
}
@@ -0,0 +1,67 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:html/parser.dart';
import 'package:webfeed/webfeed.dart';
import 'package:wyatt_medium_feeds/src/reading_time.dart';
class MediumArticle {
final RssItem? _item;
RssItem get item {
if (_item == null) {
throw StateError('Feed has not been parsed yet.');
}
return _item!;
}
MediumArticle(this._item);
String get title => item.title ?? '';
String get link => item.link ?? '';
String get guid => item.guid ?? '';
String get author => item.dc?.creator ?? '';
DateTime? get publishDate => item.pubDate;
String get content => item.content?.value ?? '';
List<String> get images {
final images = <String>[];
for (final i in item.content?.images ?? <String>[]) {
images.add(i);
}
return images;
}
String get decoded {
final document = parse(content);
return document.firstChild?.text ?? '';
}
String get summary {
final regex = RegExp(r'^.*?[\.!\?](?:\s|$)');
final match = regex.firstMatch(decoded);
return match?.group(0) ?? '';
}
Duration get readingTime {
return decoded.readingTime();
}
}
@@ -0,0 +1,113 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:http/http.dart';
import 'package:webfeed/webfeed.dart';
import 'package:wyatt_medium_feeds/src/medium_article.dart';
class MediumFeed {
final String? username;
final bool mediumSubdomain;
final String? publicationName;
final String? customDomain;
final String? tagName;
final String? topicName;
final String url;
RssFeed? _feed;
RssFeed get feed {
if (_feed == null) {
throw StateError('Feed has not been parsed yet.');
}
return _feed!;
}
String get title => feed.title ?? '';
String get description => feed.description ?? '';
String get image => feed.image?.url ?? '';
List<MediumArticle> get articles {
final articles = <MediumArticle>[];
for (final i in feed.items ?? <RssItem>[]) {
articles.add(MediumArticle(i));
}
return articles;
}
MediumFeed.fromUsername(
this.username, {
bool subdomain = false,
String proxy = 'https://cros-anywhere.herokuapp.com/',
}) : mediumSubdomain = subdomain,
publicationName = null,
customDomain = null,
tagName = null,
topicName = null,
url = subdomain
? '${proxy}https://$username.medium.com/feed'
: '${proxy}https://medium.com/feed/@$username';
MediumFeed.fromPublicationName(
this.publicationName, {
String proxy = 'https://cros-anywhere.herokuapp.com/',
}) : username = null,
mediumSubdomain = false,
customDomain = null,
tagName = null,
topicName = null,
url = '${proxy}https://medium.com/feed/$publicationName';
MediumFeed.fromCustomDomain(
this.customDomain, {
String proxy = 'https://cros-anywhere.herokuapp.com/',
}) : username = null,
mediumSubdomain = false,
publicationName = null,
tagName = null,
topicName = null,
url = '${proxy}https://$customDomain/feed';
MediumFeed.fromTagName({
this.publicationName,
this.tagName,
String proxy = 'https://cros-anywhere.herokuapp.com/',
}) : username = null,
mediumSubdomain = false,
customDomain = null,
topicName = null,
url =
'${proxy}https://medium.com/feed/$publicationName/tagged/$tagName';
MediumFeed.fromTopicName(
this.topicName, {
String proxy = 'https://cros-anywhere.herokuapp.com/',
}) : username = null,
mediumSubdomain = false,
customDomain = null,
publicationName = null,
tagName = null,
url = '${proxy}https://medium.com/feed/tag/$topicName';
Future<RssFeed> parse() async {
final xmlString = await read(Uri.parse(url));
_feed = RssFeed.parse(xmlString);
return _feed!;
}
}
@@ -0,0 +1,48 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
extension ReadingTimeX on String {
Duration readingTime({
int wpm = 200,
String suffix = 'min read',
String lessMsg = 'less than a minute',
}) {
bool _ansiWordBound(String c) {
return (' ' == c) || ('\n' == c) || ('\r' == c) || ('\t' == c);
}
int words = 0, start = 0, end = length - 1;
while (_ansiWordBound(this[start])) {
start++;
}
while (_ansiWordBound(this[end])) {
end--;
}
// Count real words
for (int i = start; i <= end;) {
for (; i <= end && !_ansiWordBound(this[i]); i++) {}
words++;
for (; i <= end && _ansiWordBound(this[i]); i++) {}
}
final minutes = words / wpm;
final seconds = (minutes - minutes.floor()) * 60;
final Duration duration = Duration(minutes: minutes.floor(), seconds: seconds.floor());
return duration;
}
}
@@ -0,0 +1,21 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
library wyatt_medium_feeds;
export 'src/medium_article.dart';
export 'src/medium_feed.dart';
export 'src/reading_time.dart';
+19
View File
@@ -0,0 +1,19 @@
name: wyatt_medium_feeds
description: Medium flux read for Flutter
repository: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages/src/branch/master/packages/wyatt_medium_feeds
version: 1.0.1
environment:
sdk: '>=2.16.2 <3.0.0'
dependencies:
http: ^0.13.4
webfeed: ^0.7.0
html: ^0.15.0
dev_dependencies:
wyatt_analysis:
git:
url: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages
ref: wyatt_analysis-v2.0.1
path: packages/wyatt_analysis