diff --git a/sites/docs/lib/_sass/components/_misc.scss b/sites/docs/lib/_sass/components/_misc.scss index 9eb5f6ce177..aa1d74bf290 100644 --- a/sites/docs/lib/_sass/components/_misc.scss +++ b/sites/docs/lib/_sass/components/_misc.scss @@ -1,3 +1,23 @@ +.copy-page-toast { + position: fixed; + bottom: 1.5rem; + left: 50%; + transform: translateX(-50%); + background-color: var(--site-primary-color, #0175c2); + color: #fff; + padding: 0.65rem 1.25rem; + border-radius: var(--site-radius, 0.5rem); + font-size: 0.9rem; + box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.2); + z-index: 1000; + animation: copy-page-toast-in 0.2s ease; +} + +@keyframes copy-page-toast-in { + from { opacity: 0; transform: translateX(-50%) translateY(0.5rem); } + to { opacity: 1; transform: translateX(-50%) translateY(0); } +} + #page-content p.install-help { text-align: right; margin-block-start: -2.5rem; diff --git a/sites/docs/lib/main.client.options.dart b/sites/docs/lib/main.client.options.dart index 6ebbe20949f..f7550f26749 100644 --- a/sites/docs/lib/main.client.options.dart +++ b/sites/docs/lib/main.client.options.dart @@ -116,6 +116,7 @@ ClientOptions get defaultClientOptions => ClientOptions( title: p['title'] as String, sourceUrl: p['sourceUrl'] as String?, issueUrl: p['issueUrl'] as String?, + showCopyPage: p['showCopyPage'] as bool, ), loader: _page_header_options.loadLibrary, ), diff --git a/sites/docs/lib/main.server.options.dart b/sites/docs/lib/main.server.options.dart index 6262d37d117..0787d09c20e 100644 --- a/sites/docs/lib/main.server.options.dart +++ b/sites/docs/lib/main.server.options.dart @@ -169,7 +169,12 @@ Map __feedbackFeedbackComponent( ) => {'issueUrl': c.issueUrl}; Map __page_header_optionsPageHeaderOptions( _page_header_options.PageHeaderOptions c, -) => {'title': c.title, 'sourceUrl': c.sourceUrl, 'issueUrl': c.issueUrl}; +) => { + 'title': c.title, + 'sourceUrl': c.sourceUrl, + 'issueUrl': c.issueUrl, + 'showCopyPage': c.showCopyPage, +}; Map __simple_tooltipSimpleTooltip( _simple_tooltip.SimpleTooltip c, ) => {'target': c.target.toId(), 'content': c.content.toId()}; diff --git a/sites/docs/lib/src/components/common/client/page_header_options.dart b/sites/docs/lib/src/components/common/client/page_header_options.dart index 79115c68104..8fccb8a63ed 100644 --- a/sites/docs/lib/src/components/common/client/page_header_options.dart +++ b/sites/docs/lib/src/components/common/client/page_header_options.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:http/http.dart' as http; import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; import 'package:universal_web/js_interop.dart'; @@ -16,12 +17,14 @@ final class PageHeaderOptions extends StatefulComponent { required this.title, this.sourceUrl, this.issueUrl, + this.showCopyPage = true, super.key, }); final String title; final String? sourceUrl; final String? issueUrl; + final bool showCopyPage; @override State createState() => _PageHeaderOptionsState(); @@ -29,6 +32,7 @@ final class PageHeaderOptions extends StatefulComponent { final class _PageHeaderOptionsState extends State { bool _isShareSupported = false; + bool _copied = false; @override void initState() { @@ -51,13 +55,35 @@ final class _PageHeaderOptionsState extends State { String get _currentBaseUrl => web.window.location.origin + web.window.location.pathname; + String? get _rawMarkdownUrl { + final sourceUrl = component.sourceUrl; + if (sourceUrl == null) return null; + return sourceUrl + .replaceFirst('github.com', 'raw.githubusercontent.com') + .replaceFirst('/blob/', '/'); + } + + Future _copyPageContent() async { + final rawUrl = _rawMarkdownUrl; + if (rawUrl == null) return; + final response = await http.get(Uri.parse(rawUrl)); + if (response.statusCode == 200) { + await web.window.navigator.clipboard.writeText(response.body).toDart; + setState(() => _copied = true); + Future.delayed(const Duration(seconds: 2), () { + if (mounted) setState(() => _copied = false); + }); + } + } + web.ShareData get _shareData => web.ShareData( url: _currentBaseUrl, title: component.title, ); @override - Component build(BuildContext _) => Dropdown( + Component build(BuildContext _) => .fragment([ + Dropdown( id: 'page-header-options', toggle: const Button(icon: 'more_vert', title: 'View page options.'), content: nav( @@ -91,6 +117,16 @@ final class _PageHeaderOptionsState extends State { ), ], ), + if (component.showCopyPage && _rawMarkdownUrl != null) + li( + [ + Button( + icon: _copied ? 'check' : 'content_copy', + content: _copied ? 'Copied!' : 'Copy page', + onClick: () => _copyPageContent().ignore(), + ), + ], + ), if (component.sourceUrl case final sourceUrl?) li( [ @@ -123,5 +159,11 @@ final class _PageHeaderOptionsState extends State { ), ], ), - ); + ), + if (_copied) + div( + classes: 'copy-page-toast', + [.text('Page copied to clipboard!')], + ), +]); } diff --git a/sites/docs/lib/src/components/common/page_header.dart b/sites/docs/lib/src/components/common/page_header.dart index 74178855586..a13e6d7c4ee 100644 --- a/sites/docs/lib/src/components/common/page_header.dart +++ b/sites/docs/lib/src/components/common/page_header.dart @@ -19,12 +19,14 @@ final class PageHeader extends StatelessComponent { this.description, this.wrap = true, this.showBreadcrumbs = true, + this.showCopyPage = true, }); final String title; final String? description; final bool wrap; final bool showBreadcrumbs; + final bool showCopyPage; @override Component build(BuildContext context) { @@ -49,6 +51,7 @@ final class PageHeader extends StatelessComponent { title: title, sourceUrl: sourceInfo.sourceUrl, issueUrl: sourceInfo.issueUrl, + showCopyPage: showCopyPage, ), ], ); diff --git a/sites/docs/lib/src/layouts/doc_layout.dart b/sites/docs/lib/src/layouts/doc_layout.dart index 35974839360..6a48cb0acf7 100644 --- a/sites/docs/lib/src/layouts/doc_layout.dart +++ b/sites/docs/lib/src/layouts/doc_layout.dart @@ -45,6 +45,14 @@ class DocLayout extends FlutterDocsLayout { ); } + static final _htmlTagPattern = RegExp(r'<[^>]+>'); + + bool _hasSubstantialContent(Page page) { + final text = page.content.replaceAll(_htmlTagPattern, ' '); + final wordCount = text.trim().split(RegExp(r'\s+')).where((w) => w.isNotEmpty).length; + return wordCount > 150; + } + @override Component buildBody(Page page, Component child) { final pageData = page.data.page; @@ -94,6 +102,7 @@ class DocLayout extends FlutterDocsLayout { showBreadcrumbs: allowBreadcrumbs && (pageData['showBreadcrumbs'] as bool? ?? true), + showCopyPage: _hasSubstantialContent(page), ), child,