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": {
|
||||
"mobilesdk_app_id": "1:136771801992:android:d20e0361057e815197203d",
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
// 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:authentication_bloc_example/constants.dart';
|
||||
import 'package:authentication_bloc_example/home/home_page.dart';
|
||||
import 'package:authentication_bloc_example/login/login_page.dart';
|
||||
@@ -55,14 +57,32 @@ class App extends StatelessWidget {
|
||||
if (user.isNotEmpty && !user.isAnonymous) {
|
||||
// Check if user is register in Firesore.
|
||||
DocumentSnapshot firestoreUser = await FirebaseFirestore.instance
|
||||
.collection('users')
|
||||
.collection(firestoreCollectionUsers)
|
||||
.doc(user.uid)
|
||||
.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 {
|
||||
'user':
|
||||
UserFirestore.fromMap(firestoreUser.data() as Map<String, dynamic>),
|
||||
'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 {
|
||||
return {};
|
||||
}
|
||||
@@ -73,7 +93,11 @@ class App extends StatelessWidget {
|
||||
if (uid != null) {
|
||||
final data = state.data.toMap();
|
||||
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 formFieldSiren = 'siren';
|
||||
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 {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -182,6 +200,8 @@ class LoginForm extends StatelessWidget {
|
||||
const SizedBox(height: 8),
|
||||
_LoginAnonButton(),
|
||||
const SizedBox(height: 8),
|
||||
_LoginWithGoogleButton(),
|
||||
const SizedBox(height: 8),
|
||||
_SignUpButton(),
|
||||
const SizedBox(height: 8),
|
||||
_SignUpAsProButton(),
|
||||
|
||||
@@ -255,7 +255,8 @@ class _DebugButton extends StatelessWidget {
|
||||
builder: (context, state) {
|
||||
return ElevatedButton(
|
||||
onPressed: () {
|
||||
log(state.toString());
|
||||
// log(state.toString());
|
||||
log(state.data.toMap().toString());
|
||||
},
|
||||
child: const Text('DEBUG'),
|
||||
);
|
||||
|
||||
+37
-3
@@ -106,10 +106,12 @@ class SignInAnonymouslyFailureFirebase
|
||||
}
|
||||
}
|
||||
|
||||
class SignInWithGoogleFailureFirebase extends SignInWithGoogleFailureInterface {
|
||||
SignInWithGoogleFailureFirebase([String? code, String? message])
|
||||
class SignInWithCredentialFailureFirebase
|
||||
extends SignInWithCredentialFailureInterface {
|
||||
SignInWithCredentialFailureFirebase([String? code, String? message])
|
||||
: super(code ?? 'unknown', message ?? 'An unknown error occurred.');
|
||||
SignInWithGoogleFailureFirebase.fromCode(String code) : super.fromCode(code) {
|
||||
SignInWithCredentialFailureFirebase.fromCode(String code)
|
||||
: super.fromCode(code) {
|
||||
switch (code) {
|
||||
case 'account-exists-with-different-credential':
|
||||
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
|
||||
extends SignInWithEmailLinkFailureInterface {
|
||||
SignInWithEmailLinkFailureFirebase([String? code, String? message])
|
||||
|
||||
+55
@@ -64,6 +64,20 @@ abstract class FetchSignInMethodsForEmailFailureInterface
|
||||
: 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}
|
||||
/// Thrown during the sign in process if a failure occurs.
|
||||
/// {@endtemplate}
|
||||
@@ -91,6 +105,47 @@ abstract class SignInWithGoogleFailureInterface
|
||||
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}
|
||||
/// Thrown during the sign in process if a failure occurs.
|
||||
/// {@endtemplate}
|
||||
|
||||
@@ -64,6 +64,9 @@ class UserFirebase implements UserInterface {
|
||||
@override
|
||||
String get uid => _user?.uid ?? '';
|
||||
|
||||
@override
|
||||
String? get providerId => _user?.providerData.first.providerId;
|
||||
|
||||
@override
|
||||
bool? get isNewUser {
|
||||
if (_user?.metadata.lastSignInTime == null ||
|
||||
|
||||
@@ -71,6 +71,9 @@ abstract class UserInterface {
|
||||
/// The user's unique ID.
|
||||
String get uid;
|
||||
|
||||
/// The provider ID for the user.
|
||||
String? get providerId;
|
||||
|
||||
/// Whether the user account has been recently created.
|
||||
bool? get isNewUser;
|
||||
|
||||
|
||||
+111
-5
@@ -15,19 +15,28 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
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/user/user_firebase.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/utils/cryptography.dart';
|
||||
|
||||
class AuthenticationRepositoryFirebase
|
||||
implements AuthenticationRepositoryInterface {
|
||||
final FirebaseAuth _firebaseAuth;
|
||||
final TwitterLogin? _twitterLogin;
|
||||
|
||||
UserFirebase _userCache = const UserFirebase.empty();
|
||||
|
||||
AuthenticationRepositoryFirebase({FirebaseAuth? firebaseAuth})
|
||||
: _firebaseAuth = firebaseAuth ?? FirebaseAuth.instance;
|
||||
AuthenticationRepositoryFirebase({
|
||||
FirebaseAuth? firebaseAuth,
|
||||
TwitterLogin? twitterLogin,
|
||||
}) : _firebaseAuth = firebaseAuth ?? FirebaseAuth.instance,
|
||||
_twitterLogin = twitterLogin;
|
||||
|
||||
@override
|
||||
Stream<UserInterface> get user {
|
||||
@@ -99,9 +108,106 @@ class AuthenticationRepositoryFirebase
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> signInWithGoogle() {
|
||||
// TODO(hpcl): implement signInWithGoogle
|
||||
throw UnimplementedError();
|
||||
Future<void> signInWithGoogle() async {
|
||||
// Trigger the authentication flow
|
||||
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
|
||||
|
||||
+15
@@ -61,6 +61,21 @@ abstract class AuthenticationRepositoryInterface {
|
||||
/// Throws a [SignInWithGoogleFailureInterface] if an exception occurs.
|
||||
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.
|
||||
///
|
||||
/// 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 {
|
||||
if (!state.status.isValidated) return;
|
||||
emit(state.copyWith(status: FormStatus.submissionInProgress));
|
||||
|
||||
@@ -45,4 +45,14 @@ class SignInState extends Equatable {
|
||||
|
||||
@override
|
||||
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:
|
||||
sdk: flutter
|
||||
|
||||
crypto: ^3.0.2
|
||||
flutter_bloc: ^8.0.1
|
||||
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:
|
||||
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