diff --git a/lib/main.dart b/lib/main.dart index 8fdf1cb..191968a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -33,6 +33,7 @@ class MyApp extends StatelessWidget { return DynamicColorBuilder( builder: (lightDynamic, darkDynamic) => MaterialApp( title: 'Voyage Handbook', + debugShowCheckedModeBanner: false, theme: ThemeData( useMaterial3: true, colorScheme: lightDynamic ?? lightColorScheme), darkTheme: ThemeData( diff --git a/lib/util/drawer.dart b/lib/util/drawer.dart index aec9494..43becab 100644 --- a/lib/util/drawer.dart +++ b/lib/util/drawer.dart @@ -34,7 +34,7 @@ Drawer genDrawer(int page, BuildContext context) => Drawer( style: TextStyle(fontWeight: FontWeight.bold), ), Text("Created by Matyáš Caras"), - Text("Thanks to WikiVoyage") + Text("Not affiliated with WikiVoyage") ], ), ), diff --git a/lib/util/render.dart b/lib/util/render.dart index e09a830..e0ed7fc 100644 --- a/lib/util/render.dart +++ b/lib/util/render.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:html/parser.dart'; import 'package:html/dom.dart' as dom; import 'package:url_launcher/url_launcher.dart'; +import 'package:url_launcher/url_launcher_string.dart'; import 'package:voyagehandbook/api/classes.dart'; import 'package:voyagehandbook/util/styles.dart'; import 'package:voyagehandbook/util/widgets/warning.dart'; @@ -44,7 +45,7 @@ class PageRenderer { const SizedBox( height: 10, ), - Text( + SelectableText( page.title, textAlign: TextAlign.center, style: PageStyles.h1, @@ -55,38 +56,34 @@ class PageRenderer { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text("From"), + const SelectableText("From"), const SizedBox( width: 5, ), - GestureDetector( + SelectableText( + "WikiVoyage", onTap: () => launchUrl( Uri.parse("https://en.wikivoyage.org/wiki/${page.key}"), mode: LaunchMode.externalApplication), - child: const Text( - "WikiVoyage", - style: TextStyle( - decoration: TextDecoration.underline, - fontWeight: FontWeight.bold, - ), + style: const TextStyle( + decoration: TextDecoration.underline, + fontWeight: FontWeight.bold, ), ), ], ), Row( children: [ - const Text("under"), + const SelectableText("under"), const SizedBox( width: 5, ), - GestureDetector( + SelectableText( + page.license.title, onTap: () => launchUrl(Uri.parse(page.license.url), mode: LaunchMode.externalApplication), - child: Text( - page.license.title, - style: const TextStyle( - decoration: TextDecoration.underline, - ), + style: const TextStyle( + decoration: TextDecoration.underline, ), ) ], @@ -121,7 +118,7 @@ class PageRenderer { height: 10, ), ); - out.add(Text( + out.add(SelectableText( sectionTitle.text, style: PageStyles.h2, textAlign: TextAlign.center, @@ -131,7 +128,7 @@ class PageRenderer { out.add( Align( alignment: Alignment.centerLeft, - child: Text( + child: SelectableText( sectionTitle.text, style: PageStyles.h3, ), @@ -142,7 +139,7 @@ class PageRenderer { out.add( Align( alignment: Alignment.centerLeft, - child: Text(sectionTitle.text, style: PageStyles.h4), + child: SelectableText(sectionTitle.text, style: PageStyles.h4), ), ); break; @@ -150,7 +147,7 @@ class PageRenderer { out.add( Align( alignment: Alignment.centerLeft, - child: Text(sectionTitle.text, style: PageStyles.h5), + child: SelectableText(sectionTitle.text, style: PageStyles.h5), ), ); break; @@ -158,7 +155,7 @@ class PageRenderer { out.add( Align( alignment: Alignment.centerLeft, - child: Text(sectionTitle.text), + child: SelectableText(sectionTitle.text), ), ); break; @@ -190,8 +187,11 @@ class PageRenderer { .getElementsByClassName("mw-kartographer-maplink") .isNotEmpty) break; out.add( - RichText( - text: TextSpan(children: _renderText(element.innerHtml)), + SelectableText.rich( + TextSpan( + children: _renderText(element.innerHtml), + ), + style: const TextStyle(height: 1.2), textAlign: TextAlign.justify, ), ); // add paragraph spans as single rich text @@ -222,26 +222,24 @@ class PageRenderer { out.add( Row( children: [ - const Text( + const SelectableText( "See also:", style: TextStyle(), ), const SizedBox( width: 5, ), - GestureDetector( + SelectableText( + link.group(2)!, onTap: () => Navigator.of(context).push( MaterialPageRoute( builder: (_) => ArticleView( pageKey: link.group(1)!, name: link.group(2)!), ), ), - child: Text( - link.group(2)!, - style: const TextStyle( - fontStyle: FontStyle.italic, - decoration: TextDecoration.underline), - ), + style: const TextStyle( + fontStyle: FontStyle.italic, + decoration: TextDecoration.underline), ) ], ), @@ -284,7 +282,7 @@ class PageRenderer { ); } else if (e.localName == "table") { Color? boxColor; - var text = []; + var text = []; for (var td in e .getElementsByTagName("tr") .first @@ -316,8 +314,8 @@ class PageRenderer { width: 10, ), Flexible( - child: RichText( - text: TextSpan(children: text), + child: SelectableText.rich( + TextSpan(children: text), ), ) ], @@ -360,7 +358,7 @@ class PageRenderer { ), ); out.add( - Text( + SelectableText( cap.text, textAlign: TextAlign.center, ), @@ -429,7 +427,7 @@ class PageRenderer { ), ); out.add( - Text( + SelectableText( caption, textAlign: TextAlign.center, ), @@ -441,7 +439,7 @@ class PageRenderer { // used to render warning box Widget _renderWarning(dom.Element element) { - var content = []; + var content = []; for (var tr in element .getElementsByTagName("table") .first @@ -460,15 +458,75 @@ class PageRenderer { return Warning(content: content); } - /// Used to render basic text with bold and italic formatting - List _renderText(String innerHtml) { + /// Used to render basic text with links, bold and italic text + List _renderText(String innerHtml) { var unescape = HtmlUnescape(); innerHtml = unescape.convert(innerHtml); - var content = []; + var content = []; var input = innerHtml - .replaceAll(RegExp(r"<(?!(?:\/b)|(?:\/i)|b|i).+?>", dotAll: true), "") + .replaceAll( + RegExp(r"<(?!(?:\/b)|(?:\/i)|(?:\/a)|(?:b)|(?:i)|(?:a )).+?>", + dotAll: true), + "") .replaceAll("\\n", "\n") .replaceAll("
", "\n"); + + // first find links + var linkMatch = RegExp( + r'(?:<(?:i|b).*?>)*(.+?)<\/a>(?:\/<(?:i|b)>)*', + dotAll: true) + .allMatches(input) + .toList(); + var nonLink = input.split(RegExp( + r'(?:<(?:i|b).*?>)*(.+?)<\/a>(?:\/<(?:i|b)>)*', + dotAll: true)); + + for (var match in linkMatch) { + // format text before link first + content.addAll(_formatText(nonLink[linkMatch.indexOf(match)])); + + // create link + content.add( + WidgetSpan( + child: SelectableText( + match.group(2)!, + onTap: () { + if (match.group(0)!.contains('rel="mw:ExtLink')) { + // handle as an external link + launchUrlString(match.group(1)!, + mode: LaunchMode.externalApplication); + } else { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => ArticleView( + pageKey: match.group(1)!.replaceAll("./", ""), + name: match.group(2)!), + ), + ); + } + }, + style: TextStyle( + decoration: TextDecoration.underline, + fontWeight: (match.group(0)!.contains("")) + ? FontWeight.bold + : null, + fontStyle: (match.group(0)!.contains("")) + ? FontStyle.italic + : null), + ), + ), + ); + } + + content.addAll(_formatText(nonLink.last)); // add last + return content; + } + + /// Formats text (bold, italics) + List _formatText(String input) { + var content = []; var noFormatting = input.split(RegExp(r"(?:.+?<\/b>)|.+?<\/i>", dotAll: true)); var needToFormat = @@ -482,15 +540,24 @@ class PageRenderer { var raw = s.group(0)!; content.add( TextSpan( - text: RegExp(r">(?!<)(.+?)<").firstMatch(raw)!.group(1), + text: RegExp(r">(?!<)(.+?)<").firstMatch(raw)!.group(1)!.replaceAll( + RegExp(r'<(?:(?:\/b)|(?:\/i)|(?:\/a)|(?:b)|(?:i)|(?:a )).*?>'), + ""), // replace stray tags style: TextStyle( - fontWeight: (raw.contains("")) + ? FontWeight.bold + : null, + fontStyle: (raw.contains("")) + ? FontStyle.italic + : null, ), ), ); // add styled } - content.add(TextSpan(text: noFormatting.last)); // add last + content.add(TextSpan( + text: noFormatting.last.replaceAll( + RegExp(r'<(?:(?:\/b)|(?:\/i)|(?:\/a)|(?:b)|(?:i)|(?:a )).*?>'), + ""))); // add last return content; } @@ -543,7 +610,7 @@ class PageRenderer { borderRadius: BorderRadius.circular(8), color: bubbleColor), width: 30, height: 30, - child: Text( + child: SelectableText( i.toString(), style: TextStyle( color: scheme.background, @@ -555,8 +622,8 @@ class PageRenderer { const SizedBox( width: 10, ), - RichText( - text: TextSpan( + SelectableText.rich( + TextSpan( children: [ if (title != null) TextSpan( diff --git a/lib/util/widgets/warning.dart b/lib/util/widgets/warning.dart index 5dcaf70..c524947 100644 --- a/lib/util/widgets/warning.dart +++ b/lib/util/widgets/warning.dart @@ -19,7 +19,7 @@ import 'package:flutter/material.dart'; class Warning extends StatelessWidget { const Warning({super.key, required this.content}); - final List content; + final List content; @override Widget build(BuildContext context) { diff --git a/lib/views/pageview.dart b/lib/views/pageview.dart index ee9debe..9c688ea 100644 --- a/lib/views/pageview.dart +++ b/lib/views/pageview.dart @@ -82,7 +82,7 @@ class _ArticleViewState extends State { MediaQuery.of(context).size.height, MediaQuery.of(context).size.width, context); - try { + if (kDebugMode) { _content = [ SizedBox( width: MediaQuery.of(context).size.width * 0.9, @@ -91,18 +91,28 @@ class _ArticleViewState extends State { .renderFromPageHTML(await WikiApi.getRawPage(widget.pageKey)), ) ]; - } catch (e) { - if (kDebugMode) print(e); - _content = [ - const Text( - "Error while rendering:", - style: PageStyles.h1, - ), - const SizedBox( - height: 10, - ), - Text(e.toString()) - ]; + } else { + try { + _content = [ + SizedBox( + width: MediaQuery.of(context).size.width * 0.9, + height: MediaQuery.of(context).size.height, + child: renderer + .renderFromPageHTML(await WikiApi.getRawPage(widget.pageKey)), + ) + ]; + } catch (e) { + _content = [ + const Text( + "Error while rendering:", + style: PageStyles.h1, + ), + const SizedBox( + height: 10, + ), + Text(e.toString()) + ]; + } } setState(() {});