Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c787b065ad | ||
|
|
12f9cf6aa5 |
@@ -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();
|
||||||
return {
|
|
||||||
'user':
|
if (!firestoreUser.exists) {
|
||||||
UserFirestore.fromMap(firestoreUser.data() as Map<String, dynamic>),
|
// Register user in Firestore when sign in with social account.
|
||||||
...firestoreUser.data() as Map<String, dynamic>? ?? {}
|
final uid = user.uid;
|
||||||
};
|
final u = {'uid': uid, 'email': user.email};
|
||||||
|
await FirebaseFirestore.instance
|
||||||
|
.collection(firestoreCollectionUsers)
|
||||||
|
.doc(uid)
|
||||||
|
.set(u);
|
||||||
|
return {
|
||||||
|
'user': UserFirestore(
|
||||||
|
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>? ?? {}
|
||||||
|
};
|
||||||
|
}
|
||||||
} 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'),
|
||||||
);
|
);
|
||||||
|
|||||||
+37
-3
@@ -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])
|
||||||
|
|||||||
+55
@@ -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;
|
||||||
|
|
||||||
|
|||||||
+111
-5
@@ -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
|
||||||
|
|||||||
+15
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,9 +11,14 @@ 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:
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
## 1.0.0
|
||||||
|
|
||||||
|
- Initial version.
|
||||||
@@ -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,48 @@
|
|||||||
|
// ignore_for_file: avoid_print
|
||||||
|
|
||||||
|
import 'package:wyatt_medium_feeds/wyatt_medium_feeds.dart';
|
||||||
|
|
||||||
|
Future<void> main() async {
|
||||||
|
final mediumFeed = MediumFeed.fromPublicationName('flutter');
|
||||||
|
await mediumFeed.parse();
|
||||||
|
|
||||||
|
print(mediumFeed.url);
|
||||||
|
final feed = mediumFeed.feed;
|
||||||
|
|
||||||
|
print(feed.title);
|
||||||
|
print('${feed.image?.url}\n');
|
||||||
|
|
||||||
|
// if ((feed.items?.length ?? 0) > 0) {
|
||||||
|
// for (final item in feed.items!) {
|
||||||
|
// print(item.title);
|
||||||
|
// print(item.pubDate?.toIso8601String());
|
||||||
|
// print(item.guid);
|
||||||
|
// print(item.dc?.creator);
|
||||||
|
// print('${item.content?.value.readingTime().inMinutes} min read');
|
||||||
|
|
||||||
|
// if ((item.categories?.length ?? 0) > 0) {
|
||||||
|
// final StringBuffer categories = StringBuffer('[');
|
||||||
|
// for (final category in item.categories!) {
|
||||||
|
// categories.write('"${category.value}"');
|
||||||
|
// if (item.categories!.indexOf(category) <
|
||||||
|
// item.categories!.length - 1) {
|
||||||
|
// categories.write(', ');
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// categories.write(']');
|
||||||
|
// print(categories);
|
||||||
|
// }
|
||||||
|
// print('\n');
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
for (final MediumArticle a in mediumFeed.articles) {
|
||||||
|
print(a.title);
|
||||||
|
print(a.author);
|
||||||
|
print(a.guid);
|
||||||
|
print(a.summary);
|
||||||
|
print(a.publishDate);
|
||||||
|
print(a.readingTime);
|
||||||
|
print('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
@@ -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.0
|
||||||
|
|
||||||
|
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
|
||||||
Reference in New Issue
Block a user