diff --git a/README.md b/README.md index 1b7d9ca..1e8ac8d 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Pub -A flutter plugin (_under development_) to render text widgets as html elements for SEO purpose. +A flutter plugin (_under development_) to render text, link, image widgets as html elements for SEO purpose. Created specifically for issue It will automatic detect the crawler using regex and navigator userAgent and add the `HtmlElement` you choose to the DOM. @@ -33,90 +33,82 @@ All PR's are welcome :) ## Usage -First we need to add a `RouteObserver` to automatically remove Html Elements when popped from the Navigation Stack. -To do this simply add this line in `MaterialApp` +It's required to add a `RobotDetector` to detect if page is visited by a robot and add `seoRouteObserver` to observe when widgets change their visibility. To do this simply wrap `MaterialApp` within `RobotDetector` and add `seoRouteObserver` in navigatorObservers: ```dart -navigatorObservers: >>[ routeObserver ], +runApp( + RobotDetector( + debug: true, // you can set true to enable robot mode + child: MaterialApp( + home: MyApp(), + navigatorObservers: [seoRouteObserver], + ), + ), +); ``` -ps : routeObserver is an object, which can be found in utils.dart file. - -There are 3 Widgets, `TextRenderer`, `LinkRenderer` & `ImageRenderer` - ### TextRenderer -**TextRenderer** -Just pass the element `new ParagraphElement()`, `new HeadingElement()` or one of other HtmlElement and your `Text`/`RichText` Widget. - -#### Paragraph +To render html text element above a child you pass `Text`, `RichText` as the child or simply set the `text`. ```dart TextRenderer( - element: new ParagraphElement(), // This is ParagraphElement by default - text: Text( - 'Paragraph: Lorem Ipsum is simply dummy text of the printing and typesetting industry.'), -), + child: Text( + 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.', + ), +) + +TextRenderer( + text: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.', + child: CustomWidget(), +) ``` -#### Heading +Optionally you can change the html element between `

` to `

` and `

` by setting `style`. Default value is `TextRendererStyle.paragraph`. ```dart TextRenderer( - element: new HeadingElement.h1(), - text: Text( - 'Heading H1: Lorem Ipsum is simply dummy text of the printing and typesetting industry.'), -), + style: TextRendererStyle.paragraph, + child: Text( + 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.', + ), +) + +TextRenderer( + style: TextRendererStyle.header1, + child: Text( + 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.', + ), +) ``` ### LinkRenderer -Need to pass `child : Widget`, `anchorText : String`, `link : String` - -Example : +To render html link element above a child set `text` and `href`. ```dart LinkRenderer( - anchorText: 'Try Flutter', - link: 'https://www.flutter.dev', - child: OutlinedButton( - onPressed: () { - launch('https://www.flutter.dev'); - }, - child: Text('Flutter.dev'), - ), -), + text: 'Try Flutter', + href: 'https://www.flutter.dev', + child: ..., +) ``` ### ImageRenderer -Need to pass `child : Widget`, `link : String`, `alt : String` - -Example : +To render html image element above a child set `alt` and pass `Image.network(...)`, `Image.asset(...)`, `Image.memory(...)` as the child or simply set the `src`. ```dart ImageRenderer( - alt: 'Flutter logo', - link: - 'https://flutter.dev/assets/images/shared/brand/flutter/logo/flutter-lockup.png', - child: Image.network( - "https://flutter.dev/assets/images/shared/brand/flutter/logo/flutter-lockup.png" - ), -), -``` - -### RendererScrollListener + alt: 'Network Image', + child: Image.network('https://fakeimg.pl/300x300/?text=Network'), +) -In case any of your renderer widgets are inside scrollable widgets like `SingleChildScrollView`, `ListView` you should wrap it within `RendererScrollListener` just so renderer widgets can subscribe to scroll changes and reposition themselves if needed. - -Example : - -```dart -RendererScrollListener( - child: ListView.builder( - ... - ), -); +ImageRenderer( + alt: 'Network Image', + src: 'https://fakeimg.pl/300x300/?text=Network', + child: CustomWidget(), +) ``` ## ScreenShot & Example diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 6ca2344..86d5ab4 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 30 + compileSdkVersion 31 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -36,7 +36,7 @@ android { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.example.example" minSdkVersion 16 - targetSdkVersion 30 + targetSdkVersion 31 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/example/android/build.gradle b/example/android/build.gradle index 9b6ed06..9170929 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.5.32' repositories { google() jcenter() diff --git a/example/assets/asset_image.png b/example/assets/asset_image.png new file mode 100644 index 0000000..a21a45d Binary files /dev/null and b/example/assets/asset_image.png differ diff --git a/example/lib/examples/image_renderer_example.dart b/example/lib/examples/image_renderer_example.dart index 72db407..09c985d 100644 --- a/example/lib/examples/image_renderer_example.dart +++ b/example/lib/examples/image_renderer_example.dart @@ -1,5 +1,7 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; -import 'package:seo_renderer/renderers/image_renderer/image_renderer.dart'; +import 'package:seo_renderer/seo_renderer.dart'; class ImageRendererExample extends StatelessWidget { const ImageRendererExample({Key? key}) : super(key: key); @@ -8,12 +10,27 @@ class ImageRendererExample extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( body: Center( - child: ImageRenderer( - alt: 'Flutter logo', - link: - 'https://flutter.dev/assets/images/shared/brand/flutter/logo/flutter-lockup.png', - child: Image.network( - "https://flutter.dev/assets/images/shared/brand/flutter/logo/flutter-lockup.png")), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ImageRenderer( + alt: 'Network Image', + child: Image.network('https://fakeimg.pl/300x300/?text=Network'), + ), + ImageRenderer( + alt: 'Asset Image', + child: Image.asset('assets/asset_image.png'), + ), + ImageRenderer( + alt: 'Memory Image', + child: Image.memory( + base64Decode( + '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3bB3MBjBWqBZLxKXzWbl+fPnM1/HqlAoqD2VJ5FIzPQaWJYljx49CryO2aKZjLKYDgYjWAtkMsIKWhspFAry4sULo69gvyqdTsvXX38t33//faQNPuru9SjrPX6/K5fLyQ8//BB6lJjP52V3d1fu3bsX+vlMM/1iCz8m03uCFYy9hC4sy7q4CkImk4n86ew2wpo8tm3bkk6njXazZzIZefLkiTSbTalWq1Kr1aTb7YrjODIajSQej0sikZB4PC7JZFJKpZIUi8XQ6yGTDdO2bUkmk8YL/9OvVy6XM/q9k98Vi8UknU4HBjmbzcqzZ8+k2WxKpVKRjx8/SqfTEcdxZDgcim3bF1eqKBaLsra2NtMUa/I3JRKJuUynGWHNh7W3t6dvNRZQZjwey88//+w56k6lUvLjjz8u+Vnpw5QQWILRaOS7RDDPPcJfMoIFLEHQSdkEywzBApYg6KyFYrG4pGeiG8EClsDrS1VFPh26oe1bjW4KwQLmwO8bcGq1mtTrdc+fL/uEbM04rAGY0WAwkL29PSmXy1IulyWdTksqlZJYLCa1Wk329/d97x/lGLvbimABM/rw4YOMRiM5Pz/3nfq5icfjS72goHZMCYEZNZvNyPfd2trign0h8EoBM4r6LUaxWGzm04ZuG4IFzCjqVTkePnzI9w+GRLCAGUU5B/DOnTs3cvVT7QgWMKP79+8bn2htWZZ89dVX8vjx4wU/qy8TewmBGcViMXnx4oV8+PBBKpWKtNtt6ff7MhwOxbIsicfjkkqlpFQqyebm5tK+efpLRLCAObAsSzY2NmRjY+Omn8oXjSkhADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1CBYANQgWADUIFgA1/geV1rSWGtJnBgAAAABJRU5ErkJggg==', + ), + ), + ), + ], + ), ), ); } diff --git a/example/lib/examples/link_singletext_example.dart b/example/lib/examples/link_singletext_example.dart index 0681d43..4dc215a 100644 --- a/example/lib/examples/link_singletext_example.dart +++ b/example/lib/examples/link_singletext_example.dart @@ -10,8 +10,8 @@ class SingleTextLinkExample extends StatelessWidget { return Scaffold( body: Center( child: LinkRenderer( - anchorText: 'Try Flutter', - link: 'https://www.flutter.dev', + text: 'Try Flutter', + href: 'https://www.flutter.dev', child: OutlinedButton( onPressed: () { launch('https://www.flutter.dev'); diff --git a/example/lib/examples/scrollable_content.dart b/example/lib/examples/scrollable_content.dart index 47bb6fe..502b0b4 100644 --- a/example/lib/examples/scrollable_content.dart +++ b/example/lib/examples/scrollable_content.dart @@ -1,5 +1,3 @@ -import 'dart:html'; - import 'package:flutter/material.dart'; import 'package:seo_renderer/seo_renderer.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -14,20 +12,18 @@ class ScrollableContent extends StatelessWidget { title: const Text('SEO HTML Tag Creator'), ), body: Center( - child: RendererScrollListener( - child: SingleChildScrollView( - child: Column( - children: [ - for (var i = 0; i < 10; i++) ...[ - TextWidget(), - TextWidget(), - LinkWidget(), - TextWidget(), - TextWidget(), - ImageWidget(), - ] - ], - ), + child: SingleChildScrollView( + child: Column( + children: [ + for (var i = 0; i < 10; i++) ...[ + TextWidget(), + TextWidget(), + LinkWidget(), + TextWidget(), + TextWidget(), + ImageWidget(), + ] + ], ), ), ), @@ -41,8 +37,8 @@ class TextWidget extends StatelessWidget { @override Widget build(BuildContext context) { return TextRenderer( - element: ParagraphElement(), - text: Text( + style: TextRendererStyle.paragraph, + child: Text( 'Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.', ), ); @@ -56,8 +52,7 @@ class ImageWidget extends StatelessWidget { Widget build(BuildContext context) { return ImageRenderer( alt: 'Fake Image', - link: 'https://fakeimg.pl/300x300/?text=Image', - child: Image.network("https://fakeimg.pl/300x300/?text=Image"), + child: Image.network('https://fakeimg.pl/300x300/?text=Image'), ); } } @@ -68,8 +63,8 @@ class LinkWidget extends StatelessWidget { @override Widget build(BuildContext context) { return LinkRenderer( - anchorText: 'Try Flutter', - link: 'https://www.flutter.dev', + text: 'Try Flutter', + href: 'https://www.flutter.dev', child: OutlinedButton( onPressed: () => launch('https://www.flutter.dev'), child: Text('Flutter.dev'), diff --git a/example/lib/examples/single_text_item.dart b/example/lib/examples/single_text_item.dart index d50c0eb..414b4b1 100644 --- a/example/lib/examples/single_text_item.dart +++ b/example/lib/examples/single_text_item.dart @@ -1,6 +1,3 @@ -import 'dart:html'; - -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:seo_renderer/seo_renderer.dart'; @@ -14,19 +11,17 @@ class SingleTextItem extends StatelessWidget { child: Column( children: [ TextRenderer( - element: new HeadingElement.h1(), - text: Text('''Heading element

- '''), + style: TextRendererStyle.header1, + child: Text('Heading element

'), ), TextRenderer( - element: new HeadingElement.h2(), - text: Text('''Heading element

and etc to h6 - '''), + style: TextRendererStyle.header2, + child: Text('Heading element

and etc to h6'), ), TextRenderer( - element: new ParagraphElement(), - text: Text( - '''Paragraph

: Lorem Ipsum is simply dummy text of the printing and typesetting industry.''', + style: TextRendererStyle.paragraph, + child: Text( + 'Paragraph

: Lorem Ipsum is simply dummy text of the printing and typesetting industry.', ), ), ], diff --git a/example/lib/main.dart b/example/lib/main.dart index 6e1c72d..7d3de85 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -6,10 +6,14 @@ import 'package:seo_renderer_example/examples/scrollable_content.dart'; import 'package:seo_renderer_example/examples/single_text_item.dart'; void main() { - runApp(MaterialApp( - navigatorObservers: [routeObserver], - home: MyApp(), - )); + runApp( + RobotDetector( + child: MaterialApp( + home: MyApp(), + navigatorObservers: [seoRouteObserver], + ), + ), + ); } class MyApp extends StatelessWidget { @@ -22,29 +26,41 @@ class MyApp extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ OutlinedButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute(builder: (_) => SingleTextItem())); - }, - child: TextRenderer(text: Text('Single Text Item'))), + onPressed: () { + Navigator.of(context) + .push(MaterialPageRoute(builder: (_) => SingleTextItem())); + }, + child: TextRenderer( + child: Text('Single Text Item'), + ), + ), OutlinedButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute(builder: (_) => ScrollableContent())); - }, - child: TextRenderer(text: Text('Scrollable Text Content'))), + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (_) => ScrollableContent())); + }, + child: TextRenderer( + child: Text('Scrollable Text Content'), + ), + ), OutlinedButton( - onPressed: () { - Navigator.of(context).push(MaterialPageRoute( - builder: (_) => SingleTextLinkExample())); - }, - child: TextRenderer(text: Text('Single Link Text Item'))), + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (_) => SingleTextLinkExample())); + }, + child: TextRenderer( + child: Text('Single Link Text Item'), + ), + ), OutlinedButton( - onPressed: () { - Navigator.of(context).push(MaterialPageRoute( - builder: (_) => ImageRendererExample())); - }, - child: TextRenderer(text: Text('Image renderer'))), + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (_) => ImageRendererExample())); + }, + child: TextRenderer( + child: Text('Image renderer'), + ), + ), ], ), ), diff --git a/example/pubspec.lock b/example/pubspec.lock index 2310e2d..7eb7464 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -7,7 +7,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.8.1" + version: "2.8.2" boolean_selector: dependency: transitive description: @@ -21,7 +21,7 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" charcode: dependency: transitive description: @@ -85,7 +85,14 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10" + version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.3" meta: dependency: transitive description: @@ -113,7 +120,7 @@ packages: path: ".." relative: true source: path - version: "0.2.0" + version: "0.4.0" sky_engine: dependency: transitive description: flutter @@ -160,7 +167,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.2" + version: "0.4.8" typed_data: dependency: transitive description: @@ -216,7 +223,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" sdks: - dart: ">=2.12.0 <3.0.0" + dart: ">=2.14.0 <3.0.0" flutter: ">=2.0.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index b2488c4..4941640 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -39,6 +39,8 @@ flutter: # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true + assets: + - assets/ # To add assets to your application, add an assets section, like this: # assets: diff --git a/lib/helpers/robot_detector_vm.dart b/lib/helpers/robot_detector_vm.dart new file mode 100644 index 0000000..699480b --- /dev/null +++ b/lib/helpers/robot_detector_vm.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; + +class RobotDetector extends StatelessWidget { + final Widget child; + + const RobotDetector({ + Key? key, + bool debug = false, + required this.child, + }) : super(key: key); + + @override + Widget build(BuildContext context) => child; +} diff --git a/lib/helpers/robot_detector_web.dart b/lib/helpers/robot_detector_web.dart new file mode 100644 index 0000000..36d6b92 --- /dev/null +++ b/lib/helpers/robot_detector_web.dart @@ -0,0 +1,38 @@ +// ignore: avoid_web_libraries_in_flutter +import 'dart:html'; + +import 'package:flutter/material.dart'; + +class RobotDetector extends StatefulWidget { + final bool debug; + final Widget child; + + const RobotDetector({ + Key? key, + this.debug = false, + required this.child, + }) : super(key: key); + + @override + _RobotDetectorState createState() => _RobotDetectorState(); + + static bool detected(BuildContext context) { + return context.findAncestorStateOfType<_RobotDetectorState>()!._detected; + } +} + +class _RobotDetectorState extends State { + /// Regex to detect Crawler for Search Engines + final _regExp = RegExp(r'/bot|google|baidu|bing|msn|teoma|slurp|yandex/i'); + late bool _detected; + + @override + void initState() { + super.initState(); + _detected = + widget.debug || _regExp.hasMatch(window.navigator.userAgent.toString()); + } + + @override + Widget build(BuildContext context) => widget.child; +} diff --git a/lib/helpers/route_aware_state.dart b/lib/helpers/route_aware_state.dart new file mode 100644 index 0000000..6cf9227 --- /dev/null +++ b/lib/helpers/route_aware_state.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; + +final seoRouteObserver = RouteObserver>(); + +abstract class RouteAwareState extends State + implements RouteAware { + var visible = true; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final route = ModalRoute.of(context); + if (route == null) { + setState(() => visible = true); + return; + } + + seoRouteObserver.subscribe(this, route); + } + + @override + void didPop() { + // do nothing as the route will be disposed + } + + @override + void didPopNext() { + setState(() => visible = true); + } + + @override + void didPush() { + setState(() => visible = true); + } + + @override + void didPushNext() { + setState(() => visible = false); + } + + @override + void dispose() { + seoRouteObserver.unsubscribe(this); + super.dispose(); + } +} diff --git a/lib/helpers/scroll_aware.dart b/lib/helpers/scroll_aware.dart deleted file mode 100644 index 11ab8a1..0000000 --- a/lib/helpers/scroll_aware.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:seo_renderer/helpers/scroll_listener/renderer_scroll_listener.dart'; - -abstract class ScrollAware { - Listenable? _listenable; - - void subscribe(BuildContext context) { - unsubscribe(); - _listenable = RendererScrollListener.of(context); - _listenable?.addListener(didScroll); - } - - void unsubscribe() { - _listenable?.removeListener(didScroll); - } - - void didScroll(); -} diff --git a/lib/helpers/scroll_listener/renderer_scroll_listener.dart b/lib/helpers/scroll_listener/renderer_scroll_listener.dart deleted file mode 100644 index 82f225b..0000000 --- a/lib/helpers/scroll_listener/renderer_scroll_listener.dart +++ /dev/null @@ -1,6 +0,0 @@ -/// Conditional imports based on if 'dart.io' is supported. -/// -/// We export lib 'renderer_scroll_listener_web.dart', but if dart.io is supported -/// then we export 'renderer_scroll_listener_vm.dart' instead. -export 'renderer_scroll_listener_web.dart' - if (dart.library.io) 'renderer_scroll_listener_vm.dart'; diff --git a/lib/helpers/scroll_listener/renderer_scroll_listener_vm.dart b/lib/helpers/scroll_listener/renderer_scroll_listener_vm.dart deleted file mode 100644 index 957feb4..0000000 --- a/lib/helpers/scroll_listener/renderer_scroll_listener_vm.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:flutter/material.dart'; - -class RendererScrollListener extends StatelessWidget { - final Widget child; - - const RendererScrollListener({ - Key? key, - required this.child, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return child; - } -} diff --git a/lib/helpers/scroll_listener/renderer_scroll_listener_web.dart b/lib/helpers/scroll_listener/renderer_scroll_listener_web.dart deleted file mode 100644 index 88a7ad0..0000000 --- a/lib/helpers/scroll_listener/renderer_scroll_listener_web.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:flutter/material.dart'; - -class RendererScrollListener extends StatefulWidget { - final Widget child; - - const RendererScrollListener({ - Key? key, - required this.child, - }) : super(key: key); - - @override - _RendererScrollListenerState createState() => _RendererScrollListenerState(); - - static Listenable? of(BuildContext context) { - return context - .findAncestorStateOfType<_RendererScrollListenerState>() - ?._notifier; - } -} - -class _RendererScrollListenerState extends State { - final _notifier = _ScrollNotifier(); - - @override - Widget build(BuildContext context) { - return NotificationListener( - child: widget.child, - onNotification: (_) { - _notifier.didScroll(); - return false; - }, - ); - } -} - -class _ScrollNotifier extends ChangeNotifier { - void didScroll() => notifyListeners(); -} diff --git a/lib/helpers/size_widget.dart b/lib/helpers/size_widget.dart new file mode 100644 index 0000000..e056d4f --- /dev/null +++ b/lib/helpers/size_widget.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/scheduler.dart'; + +class SizeWidget extends SingleChildRenderObjectWidget { + final Function(Size) onSize; + + const SizeWidget({ + Key? key, + required this.onSize, + required Widget child, + }) : super(key: key, child: child); + + @override + RenderObject createRenderObject(BuildContext context) { + return _SizeWidgetRenderObject(onSize); + } +} + +class _SizeWidgetRenderObject extends RenderProxyBox { + final Function(Size) onSize; + + _SizeWidgetRenderObject(this.onSize); + + @override + void performLayout() { + super.performLayout(); + + final size = child?.size; + if (size == null) return; + + if (SchedulerBinding.instance?.schedulerPhase != + SchedulerPhase.persistentCallbacks) { + onSize(size); + } else { + SchedulerBinding.instance?.addPostFrameCallback((_) => onSize(size)); + } + } +} diff --git a/lib/helpers/utils.dart b/lib/helpers/utils.dart deleted file mode 100644 index d2eadd5..0000000 --- a/lib/helpers/utils.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:flutter/material.dart'; - -/// [RouteObserver] created to remove Element in case pop in [RouteAware] -final RouteObserver> routeObserver = - RouteObserver>(); - -///Regex to detect Crawler for Search Engines -RegExp regExpBots = RegExp(r'/bot|google|baidu|bing|msn|teoma|slurp|yandex/i'); - -/// A [GlobalKey] Extension to get Rect from the RenderObject from a GlobalKey -extension GlobalKeyExtension on GlobalKey { - Rect? get globalPaintBounds { - final renderObject = currentContext?.findRenderObject(); - final translation = renderObject?.getTransformTo(null).getTranslation(); - if (translation != null && renderObject?.paintBounds != null) { - return renderObject!.paintBounds - .shift(Offset(translation.x, translation.y)); - } else { - return null; - } - } -} diff --git a/lib/renderers/image_renderer/image_renderer.dart b/lib/renderers/image_renderer/image_renderer.dart deleted file mode 100644 index 24142e3..0000000 --- a/lib/renderers/image_renderer/image_renderer.dart +++ /dev/null @@ -1,5 +0,0 @@ -/// Conditional imports based on if 'dart.io' is supported. -/// -/// We export lib 'image_renderer_web.dart', but if dart.io is supported -/// then we export 'image_renderer_vm.dart' instead. -export 'image_renderer_web.dart' if (dart.library.io) 'image_renderer_vm.dart'; diff --git a/lib/renderers/image_renderer/image_renderer_vm.dart b/lib/renderers/image_renderer/image_renderer_vm.dart index c90d54e..d942972 100644 --- a/lib/renderers/image_renderer/image_renderer_vm.dart +++ b/lib/renderers/image_renderer/image_renderer_vm.dart @@ -6,21 +6,19 @@ class ImageRenderer extends StatelessWidget { const ImageRenderer({ Key? key, required this.child, - required this.link, + this.src, required this.alt, }) : super(key: key); - ///Any Widget with image in it + /// Any Widget with image in it final Widget child; - ///Image source - final String link; + /// Image source + final String? src; - ///Alternative to image + /// Alternative to image final String alt; @override - Widget build(BuildContext context) { - return child; - } + Widget build(BuildContext context) => child; } diff --git a/lib/renderers/image_renderer/image_renderer_web.dart b/lib/renderers/image_renderer/image_renderer_web.dart index 990fc04..f9bba14 100644 --- a/lib/renderers/image_renderer/image_renderer_web.dart +++ b/lib/renderers/image_renderer/image_renderer_web.dart @@ -1,8 +1,12 @@ +// ignore: avoid_web_libraries_in_flutter +import 'dart:convert'; import 'dart:html'; +import 'dart:ui' as ui; import 'package:flutter/material.dart'; -import 'package:seo_renderer/helpers/scroll_aware.dart'; -import 'package:seo_renderer/helpers/utils.dart'; +import 'package:seo_renderer/helpers/robot_detector_web.dart'; +import 'package:seo_renderer/helpers/route_aware_state.dart'; +import 'package:seo_renderer/helpers/size_widget.dart'; /// This VM import stub does nothing and only returns the child. class ImageRenderer extends StatefulWidget { @@ -10,112 +14,100 @@ class ImageRenderer extends StatefulWidget { const ImageRenderer({ Key? key, required this.child, - required this.link, + this.src, required this.alt, }) : super(key: key); - ///Any Widget with image in it + /// Any Widget with image in it final Widget child; - ///Image source - final String link; + /// Image source + final String? src; - ///Alternative to image + /// Alternative to image final String alt; @override _ImageRendererState createState() => _ImageRendererState(); } -class _ImageRendererState extends State - with RouteAware, ScrollAware { - final DivElement div = DivElement(); - final key = GlobalKey(); +class _ImageRendererState extends RouteAwareState { + Size? _size; - @override - void didChangeDependencies() { - super.didChangeDependencies(); - routeObserver.subscribe(this, ModalRoute.of(context)!); - subscribe(context); - } - - @override - void dispose() { - clear(); - routeObserver.unsubscribe(this); - unsubscribe(); - super.dispose(); - } - - @override - void didPop() { - clear(); - super.didPop(); - } - - @override - void didPush() { - addDivElement(); - super.didPush(); - } + void _onSize(Size size) { + if (_size == size) return; + if (size.isEmpty) return; + _size = size; - @override - void didPopNext() { - addDivElement(); - super.didPopNext(); + if (!mounted) return; + setState(() {}); } - @override - void didPushNext() { - clear(); - super.didPushNext(); - } + String get _src { + final src = widget.src; + if (src != null) { + return src; + } + + final child = widget.child; + if (child is Image) { + final image = (child.image is ResizeImage) + ? (child.image as ResizeImage).imageProvider + : child.image; + + if (image is NetworkImage) { + return image.url; + } else if (image is AssetImage) { + return image.assetName; + } else if (image is ExactAssetImage) { + return image.assetName; + } else if (image is MemoryImage) { + return 'data:image/png;base64,${base64Encode(image.bytes)}'; + } - @override - void didScroll() { - refresh(); - } + throw FlutterError( + 'ImageRenderer child is ${widget.child.runtimeType}, image is ${image.runtimeType} not supported', + ); + } - void refresh() { - div.style.position = 'absolute'; - div.style.top = '${key.globalPaintBounds?.top ?? 0}px'; - div.style.left = '${key.globalPaintBounds?.left ?? 0}px'; - var imageElement = new ImageElement() - ..src = widget.link - ..alt = widget.alt - ..width = (key.globalPaintBounds?.width ?? 100).toInt() - ..height = (key.globalPaintBounds?.height ?? 100).toInt(); - div.children.removeWhere((element) => true); - div.append(imageElement); + throw FlutterError( + 'ImageRenderer child is ${widget.child.runtimeType} and src is null', + ); } @override Widget build(BuildContext context) { - return LayoutBuilder( - key: key, - builder: (_, __) { - return NotificationListener( - onNotification: (SizeChangedLayoutNotification notification) { - WidgetsBinding.instance?.addPostFrameCallback((timeStamp) { - refresh(); - }); - return true; - }, - child: SizeChangedLayoutNotifier(child: widget.child)); - }); - } - - addDivElement() { - WidgetsBinding.instance?.addPostFrameCallback((timeStamp) { - if (!regExpBots.hasMatch(window.navigator.userAgent.toString())) { - return; - } - refresh(); - if (!document.body!.contains(div)) document.body?.append(div); - }); - } - - void clear() { - div.remove(); + if (!RobotDetector.detected(context)) { + return widget.child; + } + + final viewType = 'html-image-$_src'; + // ignore: undefined_prefixed_name + ui.platformViewRegistry.registerViewFactory( + viewType, + (int viewId) => ImageElement(src: _src) + ..alt = widget.alt + ..style.margin = '0px' + ..style.padding = '0px' + ..style.width = '${_size?.width ?? 0}px' + ..style.height = '${_size?.height ?? 0}px', + ); + + return SizedBox( + width: _size?.width, + height: _size?.height, + child: Stack( + children: [ + SizeWidget( + onSize: _onSize, + child: widget.child, + ), + if (_size != null && visible) + IgnorePointer( + child: HtmlElementView(viewType: viewType), + ), + ], + ), + ); } } diff --git a/lib/renderers/link_renderer/link_renderer.dart b/lib/renderers/link_renderer/link_renderer.dart deleted file mode 100644 index 5102f72..0000000 --- a/lib/renderers/link_renderer/link_renderer.dart +++ /dev/null @@ -1,5 +0,0 @@ -/// Conditional imports based on if 'dart.io' is supported. -/// -/// We export lib 'link_renderer_web.dart', but if dart.io is supported -/// then we export 'link_renderer_vm.dart' instead. -export 'link_renderer_web.dart' if (dart.library.io) 'link_renderer_vm.dart'; diff --git a/lib/renderers/link_renderer/link_renderer_vm.dart b/lib/renderers/link_renderer/link_renderer_vm.dart index d10400e..fe2eeeb 100644 --- a/lib/renderers/link_renderer/link_renderer_vm.dart +++ b/lib/renderers/link_renderer/link_renderer_vm.dart @@ -8,22 +8,20 @@ class LinkRenderer extends StatelessWidget { const LinkRenderer({ Key? key, required this.child, - required this.anchorText, - required this.link, + required this.text, + required this.href, }) : super(key: key); ///Any Widget with link in it final Widget child; ///Anchor Text just like html, will work like a replacement to - ///provided [child] with [link] to it. - final String anchorText; + ///provided [child] with [href] to it. + final String text; ///link to put in href - final String link; + final String href; @override - Widget build(BuildContext context) { - return child; - } + Widget build(BuildContext context) => child; } diff --git a/lib/renderers/link_renderer/link_renderer_web.dart b/lib/renderers/link_renderer/link_renderer_web.dart index 0b2a6d2..24d4d74 100644 --- a/lib/renderers/link_renderer/link_renderer_web.dart +++ b/lib/renderers/link_renderer/link_renderer_web.dart @@ -1,8 +1,11 @@ +// ignore: avoid_web_libraries_in_flutter import 'dart:html'; +import 'dart:ui' as ui; import 'package:flutter/material.dart'; -import 'package:seo_renderer/helpers/scroll_aware.dart'; -import 'package:seo_renderer/helpers/utils.dart'; +import 'package:seo_renderer/helpers/robot_detector_web.dart'; +import 'package:seo_renderer/helpers/route_aware_state.dart'; +import 'package:seo_renderer/helpers/size_widget.dart'; /// A Widget to create the HTML Tags but with Link (href) from any [Widget]. class LinkRenderer extends StatefulWidget { @@ -10,105 +13,69 @@ class LinkRenderer extends StatefulWidget { const LinkRenderer({ Key? key, required this.child, - required this.anchorText, - required this.link, + required this.text, + required this.href, }) : super(key: key); ///Any Widget with link in it final Widget child; - ///Anchor Text just like html, will work like a replacement to provided [child] with [link] to it. - final String anchorText; + ///Anchor Text just like html, will work like a replacement to provided [child] with [href] to it. + final String text; ///link to put in href - final String link; + final String href; @override _LinkRendererState createState() => _LinkRendererState(); } -class _LinkRendererState extends State - with RouteAware, ScrollAware { - final DivElement div = DivElement(); - final key = GlobalKey(); +class _LinkRendererState extends RouteAwareState { + Size? _size; - @override - void didChangeDependencies() { - super.didChangeDependencies(); - routeObserver.subscribe(this, ModalRoute.of(context)!); - subscribe(context); - } - - @override - void dispose() { - clear(); - routeObserver.unsubscribe(this); - unsubscribe(); - super.dispose(); - } - - @override - void didPop() { - clear(); - super.didPop(); - } - - @override - void didPush() { - addDivElement(); - super.didPush(); - } - - @override - void didPopNext() { - addDivElement(); - super.didPopNext(); - } - - @override - void didPushNext() { - clear(); - super.didPushNext(); - } - - @override - void didScroll() { - refresh(); - } + void _onSize(Size size) { + if (_size == size) return; + _size = size; - void refresh() { - div.style.position = 'absolute'; - div.style.top = '${key.globalPaintBounds?.top ?? 0}px'; - div.style.left = '${key.globalPaintBounds?.left ?? 0}px'; - div.style.width = '${key.globalPaintBounds?.width ?? 100}px'; - div.style.color = '#ff0000'; - var anchorElement = new AnchorElement() - ..href = widget.link - ..text = widget.anchorText; - div.children.removeWhere((element) => true); - div.append(anchorElement); + if (!mounted) return; + setState(() {}); } @override Widget build(BuildContext context) { - return LayoutBuilder( - key: key, - builder: (_, __) { - return widget.child; - }); - } - - addDivElement() { - WidgetsBinding.instance?.addPostFrameCallback((timeStamp) { - if (!regExpBots.hasMatch(window.navigator.userAgent.toString())) { - return; - } - refresh(); - if (!document.body!.contains(div)) document.body?.append(div); - }); - } - - void clear() { - div.remove(); + if (!RobotDetector.detected(context)) { + return widget.child; + } + + final viewType = 'html-link-${widget.href}'; + // ignore: undefined_prefixed_name + ui.platformViewRegistry.registerViewFactory( + viewType, + (_) => AnchorElement(href: widget.href) + ..text = widget.text + ..style.fontSize = '14px' + ..style.color = '#ff0000' + ..style.margin = '0px' + ..style.padding = '0px' + ..style.width = '${_size?.width ?? 0}px' + ..style.height = '${_size?.height ?? 0}px', + ); + + return SizedBox( + width: _size?.width, + height: _size?.height, + child: Stack( + children: [ + SizeWidget( + onSize: _onSize, + child: widget.child, + ), + if (_size != null && visible) + IgnorePointer( + child: HtmlElementView(viewType: viewType), + ), + ], + ), + ); } } diff --git a/lib/renderers/text_renderer/text_renderer.dart b/lib/renderers/text_renderer/text_renderer.dart deleted file mode 100644 index 1c24b05..0000000 --- a/lib/renderers/text_renderer/text_renderer.dart +++ /dev/null @@ -1,5 +0,0 @@ -/// Conditional imports based on if 'dart.io' is supported. -/// -/// We export lib 'text_renderer_web.dart', but if dart.io is supported -/// then we export 'text_renderer_vm.dart' instead. -export 'text_renderer_web.dart' if (dart.library.io) 'text_renderer_vm.dart'; diff --git a/lib/renderers/text_renderer/text_renderer_style.dart b/lib/renderers/text_renderer/text_renderer_style.dart new file mode 100644 index 0000000..741a9d5 --- /dev/null +++ b/lib/renderers/text_renderer/text_renderer_style.dart @@ -0,0 +1,9 @@ +enum TextRendererStyle { + paragraph, + header1, + header2, + header3, + header4, + header5, + header6, +} diff --git a/lib/renderers/text_renderer/text_renderer_vm.dart b/lib/renderers/text_renderer/text_renderer_vm.dart index cff10e4..312166d 100644 --- a/lib/renderers/text_renderer/text_renderer_vm.dart +++ b/lib/renderers/text_renderer/text_renderer_vm.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:seo_renderer/renderers/text_renderer/text_renderer_style.dart'; /// A Widget to create the HTML Tags from the TEXT widget. /// @@ -7,14 +8,19 @@ class TextRenderer extends StatelessWidget { /// Default [TextRenderer] const constructor. const TextRenderer({ Key? key, - required this.text, + required this.child, + this.text, + this.style, }) : super(key: key); - /// Provide with [Text] widget to get data from it. - final Widget text; + ///Any Widget with text in it + final Widget child; + + ///Text that the child contains + final String? text; + + final TextRendererStyle? style; @override - Widget build(BuildContext context) { - return text; - } + Widget build(BuildContext context) => child; } diff --git a/lib/renderers/text_renderer/text_renderer_web.dart b/lib/renderers/text_renderer/text_renderer_web.dart index 494b8bb..cb41cb1 100644 --- a/lib/renderers/text_renderer/text_renderer_web.dart +++ b/lib/renderers/text_renderer/text_renderer_web.dart @@ -1,138 +1,129 @@ +// ignore: avoid_web_libraries_in_flutter import 'dart:html'; +import 'dart:ui' as ui; import 'package:flutter/material.dart'; -import 'package:seo_renderer/helpers/scroll_aware.dart'; -import 'package:seo_renderer/helpers/utils.dart'; +import 'package:seo_renderer/helpers/route_aware_state.dart'; +import 'package:seo_renderer/helpers/robot_detector_web.dart'; +import 'package:seo_renderer/helpers/size_widget.dart'; +import 'package:seo_renderer/renderers/text_renderer/text_renderer_style.dart'; /// A Widget to create the HtmlElement Tags from the TEXT widget. class TextRenderer extends StatefulWidget { /// Default [TextRenderer] const constructor. const TextRenderer({ Key? key, - required this.text, - this.element, + required this.child, + this.text, + this.style, }) : super(key: key); - /// Provide with [Widget] widget to get data from it. - final Widget text; + ///Any Widget with text in it + final Widget child; - /// HtmlElement freqently use for text: - /// - Default: new ParagraphElement() - /// - new ParagraphElement() - /// - new HeadingElement.h1() tp h6() - final HtmlElement? element; + ///Text that the child contains + final String? text; + + final TextRendererStyle? style; @override - _TextRendererState createState() => - _TextRendererState(element: element ?? new ParagraphElement()); + _TextRendererState createState() => _TextRendererState(); } -class _TextRendererState extends State - with RouteAware, ScrollAware { - _TextRendererState({required this.element}); +class _TextRendererState extends RouteAwareState { + Size? _size; - final HtmlElement element; - final key = GlobalKey(); + void _onSize(Size size) { + if (_size == size) return; + _size = size; - @override - void didChangeDependencies() { - super.didChangeDependencies(); - routeObserver.subscribe(this, ModalRoute.of(context)!); - subscribe(context); + if (!mounted) return; + setState(() {}); } - @override - void dispose() { - clear(); - routeObserver.unsubscribe(this); - unsubscribe(); - super.dispose(); + HtmlElement get _htmlElement { + switch (widget.style) { + case TextRendererStyle.header1: + return HeadingElement.h1(); + case TextRendererStyle.header2: + return HeadingElement.h2(); + case TextRendererStyle.header3: + return HeadingElement.h3(); + case TextRendererStyle.header4: + return HeadingElement.h4(); + case TextRendererStyle.header5: + return HeadingElement.h5(); + case TextRendererStyle.header6: + return HeadingElement.h6(); + case TextRendererStyle.paragraph: + default: + return ParagraphElement(); + } } - @override - void didPop() { - clear(); - super.didPop(); - } + String get _text { + final text = widget.text; + if (text != null) { + return text; + } - @override - void didPush() { - addElement(); - super.didPush(); - } + final child = widget.child; + if (child is Text) { + final text = child.data ?? child.textSpan?.toPlainText(); - @override - void didPopNext() { - addElement(); - super.didPopNext(); - } + if (text == null) { + throw FlutterError( + 'TextRenderer child is ${widget.child.runtimeType} and data, textSpan are null', + ); + } - @override - void didPushNext() { - clear(); - super.didPushNext(); - } + return text; + } - @override - void didScroll() { - refresh(); - } + if (child is RichText) { + return child.text.toPlainText(); + } - void refresh() { - element.style.position = 'absolute'; - element.style.fontSize = '14px'; - element.style.top = '${key.globalPaintBounds?.top ?? 0}px'; - element.style.left = '${key.globalPaintBounds?.left ?? 0}px'; - element.style.width = '${key.globalPaintBounds?.width ?? 100}px'; - element.style.margin = '0px'; - element.style.padding = '0px'; - element.text = _getTextFromWidget().toString(); - element.style.color = '#ff0000'; + throw FlutterError( + 'TextRenderer child is ${widget.child.runtimeType} and text is null', + ); } @override Widget build(BuildContext context) { - return LayoutBuilder( - key: key, - builder: (_, __) { - return widget.text; - }); - } - - addElement() { - WidgetsBinding.instance?.addPostFrameCallback((timeStamp) { - if (!regExpBots.hasMatch(window.navigator.userAgent.toString())) { - return; - } - refresh(); - if (!document.body!.contains(element)) document.body?.append(element); - }); - } - - void clear() { - element.remove(); - } - - String? _getTextFromWidget() { - if (widget.text is Text) { - Text wid = (widget.text as Text); - String? data; - data = wid.data; - if (data != null) { - return data; - } - if (wid.textSpan != null) { - data = wid.textSpan!.toPlainText(); - } - if (data != null) { - return data; - } - } - if (widget.text is RichText) { - return (widget.text as RichText).text.toPlainText(); + if (!RobotDetector.detected(context)) { + return widget.child; } - throw FlutterError( - 'Provided Widget is of Type ${widget.text.runtimeType}. Only supported widget is Text & RichText.'); + final viewType = 'html-text-$_text'; + // ignore: undefined_prefixed_name + ui.platformViewRegistry.registerViewFactory( + viewType, + (_) => _htmlElement + ..text = _text + ..style.fontSize = '14px' + ..style.color = '#ff0000' + ..style.margin = '0px' + ..style.padding = '0px' + ..style.width = '${_size?.width ?? 0}px' + ..style.height = '${_size?.height ?? 0}px', + ); + + return SizedBox( + width: _size?.width, + height: _size?.height, + child: Stack( + children: [ + SizeWidget( + onSize: _onSize, + child: widget.child, + ), + if (_size != null && visible) + IgnorePointer( + child: HtmlElementView(viewType: viewType), + ), + ], + ), + ); } } diff --git a/lib/seo_renderer.dart b/lib/seo_renderer.dart index 5acda79..ca76274 100644 --- a/lib/seo_renderer.dart +++ b/lib/seo_renderer.dart @@ -1,5 +1,10 @@ -export 'helpers/scroll_listener/renderer_scroll_listener.dart'; -export 'helpers/utils.dart'; -export 'renderers/image_renderer/image_renderer.dart'; -export 'renderers/link_renderer/link_renderer.dart'; -export 'renderers/text_renderer/text_renderer.dart'; +export 'helpers/robot_detector_web.dart' + if (dart.library.io) 'helpers/robot_detector_vm.dart'; +export 'helpers/route_aware_state.dart'; +export 'renderers/image_renderer/image_renderer_web.dart' + if (dart.library.io) 'renderers/image_renderer/image_renderer_vm.dart'; +export 'renderers/link_renderer/link_renderer_web.dart' + if (dart.library.io) 'renderers/link_renderer/link_renderer_vm.dart'; +export 'renderers/text_renderer/text_renderer_style.dart'; +export 'renderers/text_renderer/text_renderer_web.dart' + if (dart.library.io) 'renderers/text_renderer/text_renderer_vm.dart'; diff --git a/pubspec.lock b/pubspec.lock index 4935c14..b482404 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,7 +7,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.8.1" + version: "2.8.2" boolean_selector: dependency: transitive description: @@ -21,7 +21,7 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" charcode: dependency: transitive description: @@ -66,7 +66,14 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10" + version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.3" meta: dependency: transitive description: @@ -127,7 +134,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.2" + version: "0.4.8" typed_data: dependency: transitive description: @@ -141,7 +148,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" sdks: - dart: ">=2.12.0 <3.0.0" + dart: ">=2.14.0 <3.0.0" flutter: ">=1.20.0"