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