diff --git a/packages/wyatt_medium_feeds/.gitignore b/packages/wyatt_medium_feeds/.gitignore
new file mode 100644
index 00000000..65c34dc8
--- /dev/null
+++ b/packages/wyatt_medium_feeds/.gitignore
@@ -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
diff --git a/packages/wyatt_medium_feeds/.vscode/extensions.json b/packages/wyatt_medium_feeds/.vscode/extensions.json
new file mode 100644
index 00000000..30cd2233
--- /dev/null
+++ b/packages/wyatt_medium_feeds/.vscode/extensions.json
@@ -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 .
+ */
+
+{
+ "recommendations": [
+ "psioniq.psi-header",
+ "blaugold.melos-code"
+ ]
+}
\ No newline at end of file
diff --git a/packages/wyatt_medium_feeds/.vscode/settings.json b/packages/wyatt_medium_feeds/.vscode/settings.json
new file mode 100644
index 00000000..708f5ba6
--- /dev/null
+++ b/packages/wyatt_medium_feeds/.vscode/settings.json
@@ -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": "",
+ "language": "markdown",
+ },
+ ],
+ "psi-header.templates": [
+ {
+ "language": "*",
+ "template": [
+ "Copyright (C) <> 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 ."
+ ],
+ }
+ ],
+ "dart.runPubGetOnPubspecChanges": false,
+}
\ No newline at end of file
diff --git a/packages/wyatt_medium_feeds/CHANGELOG.md b/packages/wyatt_medium_feeds/CHANGELOG.md
new file mode 100644
index 00000000..effe43c8
--- /dev/null
+++ b/packages/wyatt_medium_feeds/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 1.0.0
+
+- Initial version.
diff --git a/packages/wyatt_medium_feeds/README.md b/packages/wyatt_medium_feeds/README.md
new file mode 100644
index 00000000..8b55e735
--- /dev/null
+++ b/packages/wyatt_medium_feeds/README.md
@@ -0,0 +1,39 @@
+
+
+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.
diff --git a/packages/wyatt_medium_feeds/analysis_options.yaml b/packages/wyatt_medium_feeds/analysis_options.yaml
new file mode 100644
index 00000000..82177cd5
--- /dev/null
+++ b/packages/wyatt_medium_feeds/analysis_options.yaml
@@ -0,0 +1 @@
+include: package:wyatt_analysis/analysis_options.flutter.yaml
\ No newline at end of file
diff --git a/packages/wyatt_medium_feeds/example/.gitignore b/packages/wyatt_medium_feeds/example/.gitignore
new file mode 100644
index 00000000..0fa6b675
--- /dev/null
+++ b/packages/wyatt_medium_feeds/example/.gitignore
@@ -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
diff --git a/packages/wyatt_medium_feeds/example/.metadata b/packages/wyatt_medium_feeds/example/.metadata
new file mode 100644
index 00000000..3c3e4b52
--- /dev/null
+++ b/packages/wyatt_medium_feeds/example/.metadata
@@ -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
diff --git a/packages/wyatt_medium_feeds/example/README.md b/packages/wyatt_medium_feeds/example/README.md
new file mode 100644
index 00000000..f3e776e7
--- /dev/null
+++ b/packages/wyatt_medium_feeds/example/README.md
@@ -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.
diff --git a/packages/wyatt_medium_feeds/example/analysis_options.yaml b/packages/wyatt_medium_feeds/example/analysis_options.yaml
new file mode 100644
index 00000000..61b6c4de
--- /dev/null
+++ b/packages/wyatt_medium_feeds/example/analysis_options.yaml
@@ -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
diff --git a/packages/wyatt_medium_feeds/example/lib/article_tile.dart b/packages/wyatt_medium_feeds/example/lib/article_tile.dart
new file mode 100644
index 00000000..833a2580
--- /dev/null
+++ b/packages/wyatt_medium_feeds/example/lib/article_tile.dart
@@ -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,
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/packages/wyatt_medium_feeds/example/lib/main.dart b/packages/wyatt_medium_feeds/example/lib/main.dart
new file mode 100644
index 00000000..3e85dfca
--- /dev/null
+++ b/packages/wyatt_medium_feeds/example/lib/main.dart
@@ -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(),
+ );
+ }
+}
diff --git a/packages/wyatt_medium_feeds/example/pubspec.yaml b/packages/wyatt_medium_feeds/example/pubspec.yaml
new file mode 100644
index 00000000..bce40289
--- /dev/null
+++ b/packages/wyatt_medium_feeds/example/pubspec.yaml
@@ -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
diff --git a/packages/wyatt_medium_feeds/example/test/widget_test.dart b/packages/wyatt_medium_feeds/example/test/widget_test.dart
new file mode 100644
index 00000000..58823107
--- /dev/null
+++ b/packages/wyatt_medium_feeds/example/test/widget_test.dart
@@ -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);
+ });
+}
diff --git a/packages/wyatt_medium_feeds/example/web/favicon.png b/packages/wyatt_medium_feeds/example/web/favicon.png
new file mode 100644
index 00000000..8aaa46ac
Binary files /dev/null and b/packages/wyatt_medium_feeds/example/web/favicon.png differ
diff --git a/packages/wyatt_medium_feeds/example/web/icons/Icon-192.png b/packages/wyatt_medium_feeds/example/web/icons/Icon-192.png
new file mode 100644
index 00000000..b749bfef
Binary files /dev/null and b/packages/wyatt_medium_feeds/example/web/icons/Icon-192.png differ
diff --git a/packages/wyatt_medium_feeds/example/web/icons/Icon-512.png b/packages/wyatt_medium_feeds/example/web/icons/Icon-512.png
new file mode 100644
index 00000000..88cfd48d
Binary files /dev/null and b/packages/wyatt_medium_feeds/example/web/icons/Icon-512.png differ
diff --git a/packages/wyatt_medium_feeds/example/web/icons/Icon-maskable-192.png b/packages/wyatt_medium_feeds/example/web/icons/Icon-maskable-192.png
new file mode 100644
index 00000000..eb9b4d76
Binary files /dev/null and b/packages/wyatt_medium_feeds/example/web/icons/Icon-maskable-192.png differ
diff --git a/packages/wyatt_medium_feeds/example/web/icons/Icon-maskable-512.png b/packages/wyatt_medium_feeds/example/web/icons/Icon-maskable-512.png
new file mode 100644
index 00000000..d69c5669
Binary files /dev/null and b/packages/wyatt_medium_feeds/example/web/icons/Icon-maskable-512.png differ
diff --git a/packages/wyatt_medium_feeds/example/web/index.html b/packages/wyatt_medium_feeds/example/web/index.html
new file mode 100644
index 00000000..aa53f64a
--- /dev/null
+++ b/packages/wyatt_medium_feeds/example/web/index.html
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ medium_feeds_example
+
+
+
+
+
+
+
diff --git a/packages/wyatt_medium_feeds/example/web/manifest.json b/packages/wyatt_medium_feeds/example/web/manifest.json
new file mode 100644
index 00000000..45dfbbec
--- /dev/null
+++ b/packages/wyatt_medium_feeds/example/web/manifest.json
@@ -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"
+ }
+ ]
+}
diff --git a/packages/wyatt_medium_feeds/example/wyatt_medium_feeds_example.dart b/packages/wyatt_medium_feeds/example/wyatt_medium_feeds_example.dart
new file mode 100644
index 00000000..259b8fab
--- /dev/null
+++ b/packages/wyatt_medium_feeds/example/wyatt_medium_feeds_example.dart
@@ -0,0 +1,48 @@
+// ignore_for_file: avoid_print
+
+import 'package:wyatt_medium_feeds/wyatt_medium_feeds.dart';
+
+Future 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');
+ }
+}
diff --git a/packages/wyatt_medium_feeds/lib/src/medium_article.dart b/packages/wyatt_medium_feeds/lib/src/medium_article.dart
new file mode 100644
index 00000000..cb526ed3
--- /dev/null
+++ b/packages/wyatt_medium_feeds/lib/src/medium_article.dart
@@ -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 .
+
+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 get images {
+ final images = [];
+ for (final i in item.content?.images ?? []) {
+ 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();
+ }
+}
diff --git a/packages/wyatt_medium_feeds/lib/src/medium_feed.dart b/packages/wyatt_medium_feeds/lib/src/medium_feed.dart
new file mode 100644
index 00000000..b574fce4
--- /dev/null
+++ b/packages/wyatt_medium_feeds/lib/src/medium_feed.dart
@@ -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 .
+
+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 get articles {
+ final articles = [];
+ for (final i in feed.items ?? []) {
+ 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 parse() async {
+ final xmlString = await read(Uri.parse(url));
+ _feed = RssFeed.parse(xmlString);
+ return _feed!;
+ }
+}
diff --git a/packages/wyatt_medium_feeds/lib/src/reading_time.dart b/packages/wyatt_medium_feeds/lib/src/reading_time.dart
new file mode 100644
index 00000000..37d9ec8a
--- /dev/null
+++ b/packages/wyatt_medium_feeds/lib/src/reading_time.dart
@@ -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 .
+
+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;
+ }
+}
diff --git a/packages/wyatt_medium_feeds/lib/wyatt_medium_feeds.dart b/packages/wyatt_medium_feeds/lib/wyatt_medium_feeds.dart
new file mode 100644
index 00000000..df269550
--- /dev/null
+++ b/packages/wyatt_medium_feeds/lib/wyatt_medium_feeds.dart
@@ -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 .
+
+library wyatt_medium_feeds;
+
+export 'src/medium_article.dart';
+export 'src/medium_feed.dart';
+export 'src/reading_time.dart';
diff --git a/packages/wyatt_medium_feeds/pubspec.yaml b/packages/wyatt_medium_feeds/pubspec.yaml
new file mode 100644
index 00000000..4f068053
--- /dev/null
+++ b/packages/wyatt_medium_feeds/pubspec.yaml
@@ -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
diff --git a/packages/wyatt_medium_feeds/test/wyatt_medium_feeds_test.dart b/packages/wyatt_medium_feeds/test/wyatt_medium_feeds_test.dart
new file mode 100644
index 00000000..e69de29b