diff --git a/lib/util/render.dart b/lib/util/render.dart
index 9d41aa2..f4d053e 100644
--- a/lib/util/render.dart
+++ b/lib/util/render.dart
@@ -7,6 +7,7 @@ import 'package:voyagehandbook/api/classes.dart';
import 'package:voyagehandbook/util/styles.dart';
import 'package:voyagehandbook/util/widgets/warning.dart';
import 'package:html_unescape/html_unescape_small.dart';
+import 'package:zoom_pinch_overlay/zoom_pinch_overlay.dart';
/*
Voyage Handbook - The open-source WikiVoyage reader
@@ -24,294 +25,376 @@ import 'package:html_unescape/html_unescape_small.dart';
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
-
final _ignoredTags = ["style", "script"];
-/// Used to create Widgets from raw HTML from WM API
-ListView renderFromPageHTML(RawPage page, double height, double width) {
- var out = [
- const SizedBox(
- height: 10,
- ),
- Text(
- page.title,
- textAlign: TextAlign.center,
- style: PageStyles.h1,
- ),
- const SizedBox(
- height: 10,
- ),
- Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- const Text("From"),
- const SizedBox(
- width: 5,
- ),
- GestureDetector(
- 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,
- ),
- ),
- ),
- ],
- ),
- Row(
- children: [
- const Text("under"),
- const SizedBox(
- width: 5,
- ),
- GestureDetector(
- onTap: () => launchUrl(Uri.parse(page.license.url),
- mode: LaunchMode.externalApplication),
- child: Text(
- page.license.title,
- style: const TextStyle(
- decoration: TextDecoration.underline,
- ),
- ),
- )
- ],
- )
- ];
- var document = parse(page.html);
- var sections = document.body!.getElementsByTagName("section");
- for (var sec in sections) {
- if (sec.localName == "section") {
- out.addAll(_renderSection(sec, height, width));
- }
- }
- var l = ListView.builder(
- padding: EdgeInsets.zero,
- itemBuilder: (c, i) => out[i],
- itemCount: out.length,
- );
- return l;
-}
+class PageRenderer {
+ final ColorScheme scheme;
+ final double height;
+ final double width;
-List _renderSection(dom.Element sec, double height, double width) {
- var out = [];
- // Get Section Title
- var headings = sec.children
- .where((element) => ["h2", "h3", "h4", "h5"].contains(element.localName));
- var sectionTitle = (headings.isNotEmpty) ? headings.first : null;
- if (sectionTitle != null) {
- switch (sectionTitle.localName) {
- case "h2":
- out.add(
+ PageRenderer(this.scheme, this.height, this.width);
+
+ /// Used to create Widgets from raw HTML from WM API
+ ListView renderFromPageHTML(RawPage page) {
+ var out = [
+ const SizedBox(
+ height: 10,
+ ),
+ Text(
+ page.title,
+ textAlign: TextAlign.center,
+ style: PageStyles.h1,
+ ),
+ const SizedBox(
+ height: 10,
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Text("From"),
const SizedBox(
- height: 10,
+ width: 5,
),
- );
- out.add(Text(
- sectionTitle.text,
- style: PageStyles.h2,
- textAlign: TextAlign.center,
- ));
- break;
- case "h3":
- out.add(
- Align(
- alignment: Alignment.centerLeft,
- child: Text(
- sectionTitle.text,
- style: PageStyles.h3,
- ),
- ),
- );
- break;
- case "h4":
- out.add(
- Align(
- alignment: Alignment.centerLeft,
- child: Text(sectionTitle.text, style: PageStyles.h4),
- ),
- );
- break;
- case "h5":
- out.add(
- Align(
- alignment: Alignment.centerLeft,
- child: Text(sectionTitle.text, style: PageStyles.h5),
- ),
- );
- break;
- default:
- out.add(
- Align(
- alignment: Alignment.centerLeft,
- child: Text(sectionTitle.text),
- ),
- );
- break;
- }
- // out.add(
- // const SizedBox(
- // height: 5,
- // ),
- // );
- }
-
- // create TextSpans from text content
- for (var element in sec.children) {
- // Go through all section's children
- for (var t in _ignoredTags) {
- var ignored = element.getElementsByTagName(t);
- if (ignored.isNotEmpty) {
- // Remove ignored tags
- for (var element in ignored) {
- element.remove();
- }
- }
- }
-
- switch (element.localName) {
- case "p":
- case "link":
- out.add(
- RichText(
- text: TextSpan(children: _renderText(element.innerHtml)),
- textAlign: TextAlign.justify,
- ),
- ); // add paragraph spans as single rich text
- out.add(
- const SizedBox(
- height: 5,
- ),
- ); // space paragraphs
- break;
- case "figure":
-
- /// Image figure
- var imgs = element.getElementsByTagName("img");
- if (imgs.isEmpty) break;
- var img = imgs.first; // get image element
- if (img.attributes["src"] == null) break;
- var figcap = element.getElementsByTagName("figcaption"); // get caption
- String? caption;
- if (figcap.isNotEmpty) {
- caption = figcap.first.text; // TODO: handle links
- }
- out.add(const SizedBox(
- height: 10,
- ));
- out.add(
- Container(
- // TODO: add tap detector to open wikimedia page?
- width: width * 0.8,
- height: height * 0.3,
- foregroundDecoration: BoxDecoration(
- image: DecorationImage(
- fit: BoxFit.fitHeight,
- image: CachedNetworkImageProvider(
- img.attributes["src"]!.replaceAll("//", "https://"),
- ),
+ GestureDetector(
+ 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,
),
),
- //height: height * 0.3,
),
- ); // load image
- if (caption != null) {
- // Add caption when available
- out.add(
- const SizedBox(
- height: 3,
- ),
- );
- out.add(Text(
- caption,
- textAlign: TextAlign.center,
- ));
- }
- out.add(
+ ],
+ ),
+ Row(
+ children: [
+ const Text("under"),
const SizedBox(
- height: 10,
+ width: 5,
),
- );
- break;
- case "section":
- out.addAll(_renderSection(element, height, width));
- break;
- case "div":
- if (element.attributes["class"] != null &&
- element.attributes["class"] == "pp_cautionbox") {
- out.add(_renderWarning(element));
+ GestureDetector(
+ onTap: () => launchUrl(Uri.parse(page.license.url),
+ mode: LaunchMode.externalApplication),
+ child: Text(
+ page.license.title,
+ style: const TextStyle(
+ decoration: TextDecoration.underline,
+ ),
+ ),
+ )
+ ],
+ )
+ ];
+ var document = parse(page.html);
+ var sections = document.body!.getElementsByTagName("section");
+ for (var sec in sections) {
+ if (sec.localName == "section") {
+ out.addAll(_renderSection(sec));
+ }
+ }
+ var l = ListView.builder(
+ padding: EdgeInsets.zero,
+ itemBuilder: (c, i) => out[i],
+ itemCount: out.length,
+ );
+ return l;
+ }
+
+ List _renderSection(dom.Element sec) {
+ var out = [];
+ // Get Section Title
+ var headings = sec.children.where(
+ (element) => ["h2", "h3", "h4", "h5"].contains(element.localName));
+ var sectionTitle = (headings.isNotEmpty) ? headings.first : null;
+ if (sectionTitle != null) {
+ switch (sectionTitle.localName) {
+ case "h2":
out.add(
const SizedBox(
height: 10,
),
);
- }
- break;
- default:
- break;
+ out.add(Text(
+ sectionTitle.text,
+ style: PageStyles.h2,
+ textAlign: TextAlign.center,
+ ));
+ break;
+ case "h3":
+ out.add(
+ Align(
+ alignment: Alignment.centerLeft,
+ child: Text(
+ sectionTitle.text,
+ style: PageStyles.h3,
+ ),
+ ),
+ );
+ break;
+ case "h4":
+ out.add(
+ Align(
+ alignment: Alignment.centerLeft,
+ child: Text(sectionTitle.text, style: PageStyles.h4),
+ ),
+ );
+ break;
+ case "h5":
+ out.add(
+ Align(
+ alignment: Alignment.centerLeft,
+ child: Text(sectionTitle.text, style: PageStyles.h5),
+ ),
+ );
+ break;
+ default:
+ out.add(
+ Align(
+ alignment: Alignment.centerLeft,
+ child: Text(sectionTitle.text),
+ ),
+ );
+ break;
+ }
+ // out.add(
+ // const SizedBox(
+ // height: 5,
+ // ),
+ // );
}
- element.remove();
+
+ // create TextSpans from text content
+ for (var element in sec.children) {
+ // Go through all section's children
+ for (var t in _ignoredTags) {
+ var ignored = element.getElementsByTagName(t);
+ if (ignored.isNotEmpty) {
+ // Remove ignored tags
+ for (var element in ignored) {
+ element.remove();
+ }
+ }
+ }
+
+ switch (element.localName) {
+ case "p":
+ case "link":
+ out.add(
+ RichText(
+ text: TextSpan(children: _renderText(element.innerHtml)),
+ textAlign: TextAlign.justify,
+ ),
+ ); // add paragraph spans as single rich text
+ out.add(
+ const SizedBox(
+ height: 5,
+ ),
+ ); // space paragraphs
+ break;
+ case "figure":
+ out.addAll(_renderImageFigure(element));
+ out.add(
+ const SizedBox(
+ height: 10,
+ ),
+ );
+ break;
+ case "section":
+ out.addAll(_renderSection(element));
+ break;
+ case "div":
+ if (element.attributes["class"] != null &&
+ element.attributes["class"] == "pp_cautionbox") {
+ out.add(_renderWarning(element));
+ out.add(
+ const SizedBox(
+ height: 10,
+ ),
+ );
+ } else if (element.id == "region_list") {
+ var inner = parse(
+ element.innerHtml.replaceAll(RegExp(r'<\/?span.+?>'), ""),
+ );
+ for (var e in inner.body!.children) {
+ if (e.localName == "figure") {
+ // render image
+ out.addAll(_renderImageFigure(e));
+ out.add(
+ const SizedBox(
+ height: 5,
+ ),
+ );
+ } else if (e.localName == "table") {
+ Color? boxColor;
+ var text = [];
+ for (var td in e
+ .getElementsByTagName("tr")
+ .first
+ .getElementsByTagName("td")) {
+ if (td.attributes["style"] != null) {
+ var colorMatch = RegExp(r'background-color:#(.+?);')
+ .firstMatch(td.attributes["style"]!);
+ boxColor = Color(int.parse("0xff${colorMatch!.group(1)}"));
+ } else {
+ text.addAll(_renderText(td.innerHtml));
+ }
+ }
+ out.add(
+ const SizedBox(
+ height: 10,
+ ),
+ );
+ out.add(
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ if (boxColor != null)
+ Container(
+ color: boxColor,
+ width: 25,
+ height: 25,
+ ),
+ const SizedBox(
+ width: 10,
+ ),
+ Flexible(
+ child: RichText(
+ text: TextSpan(children: text),
+ ),
+ )
+ ],
+ ),
+ );
+ }
+ }
+ out.add(
+ const SizedBox(
+ height: 10,
+ ),
+ );
+ }
+ break;
+ default:
+ break;
+ }
+ element.remove();
+ }
+
+ out.add(
+ const SizedBox(
+ height: 5,
+ ),
+ );
+ return out;
}
- out.add(
- const SizedBox(
- height: 5,
- ),
- );
- return out;
-}
+ List _renderImageFigure(dom.Element element) {
+ var out = [];
-// used to render warning box
-Widget _renderWarning(dom.Element element) {
- var content = [];
- for (var tr in element
- .getElementsByTagName("table")
- .first
- .getElementsByTagName("tbody")
- .first
- .getElementsByTagName("tr")) {
- for (var e in tr.getElementsByTagName("td")) {
- // Get to table data
- content.addAll(
- _renderText(
- e.innerHtml.replaceAll(RegExp(r''), ""),
+ /// Image figure
+ var imgs = element.getElementsByTagName("img");
+ if (imgs.isEmpty) return [];
+ var img = imgs.first; // get image element
+ if (img.attributes["src"] == null || img.attributes["src"] == "") return [];
+ var figcap = element.getElementsByTagName("figcaption"); // get caption
+ String? caption;
+ if (figcap.isNotEmpty) {
+ caption = figcap.first.text; // TODO: handle links
+ }
+ out.add(const SizedBox(
+ height: 10,
+ ));
+ out.add(
+ SizedBox(
+ // TODO: open wikimedia page?
+ width: width * 0.8,
+ height: height * 0.3,
+ child: ZoomOverlay(
+ child: CachedNetworkImage(
+ imageUrl: img.attributes["src"]!.replaceAll("//", "https://"),
+ progressIndicatorBuilder: (context, url, downloadProgress) =>
+ LinearProgressIndicator(value: downloadProgress.progress),
+ errorWidget: (context, url, error) => Icon(
+ Icons.error,
+ color: scheme.error,
+ ),
+ ),
+ ),
+ ),
+ ); // load image
+ if (caption != null) {
+ // Add caption when available
+ out.add(
+ const SizedBox(
+ height: 3,
+ ),
+ );
+ out.add(
+ Text(
+ caption,
+ textAlign: TextAlign.center,
),
);
}
- }
- return Warning(content: content);
-}
-/// Used to render basic text with bold and italic formatting
-List _renderText(String innerHtml) {
- var unescape = HtmlUnescape();
- innerHtml = unescape.convert(innerHtml);
- var content = [];
- var input = innerHtml
- .replaceAll(RegExp(r"<(?!(?:\/b)|(?:\/i)|b|i).+?>", dotAll: true), "")
- .replaceAll("\\n", "\r");
- var noFormatting =
- input.split(RegExp(r"(?:.+?<\/b>)|.+?<\/i>", dotAll: true));
- var needToFormat = RegExp(r"(?:.+?<\/b>)|.+?<\/i>", dotAll: true)
- .allMatches(input)
- .toList();
- for (var s in needToFormat) {
- content.add(TextSpan(
- text: noFormatting[needToFormat.indexOf(s)])); // add text before styled
- var raw = s.group(0)!;
- content.add(
- TextSpan(
- text: RegExp(r">(?!<)(.+?)<").firstMatch(raw)!.group(1),
- style: TextStyle(
- fontWeight: (raw.contains("[];
+ for (var tr in element
+ .getElementsByTagName("table")
+ .first
+ .getElementsByTagName("tbody")
+ .first
+ .getElementsByTagName("tr")) {
+ for (var e in tr.getElementsByTagName("td")) {
+ // Get to table data
+ content.addAll(
+ _renderText(
+ e.innerHtml.replaceAll(RegExp(r''), ""),
+ ),
+ );
+ }
+ }
+ return Warning(content: content);
+ }
+
+ /// Used to render basic text with bold and italic formatting
+ List _renderText(String innerHtml) {
+ var unescape = HtmlUnescape();
+ innerHtml = unescape.convert(innerHtml);
+ var content = [];
+ var input = innerHtml
+ .replaceAll(RegExp(r"<(?!(?:\/b)|(?:\/i)|b|i).+?>", dotAll: true), "")
+ .replaceAll("\\n", "\n")
+ .replaceAll("
", "\n");
+ var noFormatting =
+ input.split(RegExp(r"(?:.+?<\/b>)|.+?<\/i>", dotAll: true));
+ var needToFormat =
+ RegExp(r"(?:.+?<\/b>)|.+?<\/i>", dotAll: true)
+ .allMatches(input)
+ .toList();
+ for (var s in needToFormat) {
+ content.add(TextSpan(
+ text:
+ noFormatting[needToFormat.indexOf(s)])); // add text before styled
+ var raw = s.group(0)!;
+ content.add(
+ TextSpan(
+ text: RegExp(r">(?!<)(.+?)<").firstMatch(raw)!.group(1),
+ style: TextStyle(
+ fontWeight: (raw.contains(" {
}
void loadPage() async {
+ var renderer = PageRenderer(Theme.of(context).colorScheme,
+ MediaQuery.of(context).size.height, MediaQuery.of(context).size.width);
try {
_content = [
SizedBox(
width: MediaQuery.of(context).size.width * 0.9,
height: MediaQuery.of(context).size.height,
- child: renderFromPageHTML(
- await WikiApi.getRawPage(widget.pageKey),
- MediaQuery.of(context).size.height,
- MediaQuery.of(context).size.width),
+ child: renderer
+ .renderFromPageHTML(await WikiApi.getRawPage(widget.pageKey)),
)
];
} catch (e) {
diff --git a/pubspec.lock b/pubspec.lock
index 697a398..3615281 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -17,6 +17,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.8.0"
+ archive:
+ dependency: transitive
+ description:
+ name: archive
+ sha256: d6347d54a2d8028e0437e3c099f66fdb8ae02c4720c1e7534c9f24c10351f85d
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.3.6"
args:
dependency: transitive
description:
@@ -145,6 +153,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.2"
+ cli_util:
+ dependency: transitive
+ description:
+ name: cli_util
+ sha256: "66f86e916d285c1a93d3b79587d94bd71984a66aac4ff74e524cfa7877f1395c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.3.5"
clock:
dependency: transitive
description:
@@ -278,6 +294,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.3.0"
+ flutter_launcher_icons:
+ dependency: "direct dev"
+ description:
+ name: flutter_launcher_icons
+ sha256: "02dcaf49d405f652b7160e882bacfc02cb497041bb2eab2a49b1c393cf9aac12"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.12.0"
flutter_lints:
dependency: "direct dev"
description:
@@ -360,6 +384,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.2"
+ image:
+ dependency: transitive
+ description:
+ name: image
+ sha256: "483a389d6ccb292b570c31b3a193779b1b0178e7eb571986d9a49904b6861227"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.0.15"
io:
dependency: transitive
description:
@@ -520,6 +552,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.11.1"
+ petitparser:
+ dependency: transitive
+ description:
+ name: petitparser
+ sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4"
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.1.0"
platform:
dependency: transitive
description:
@@ -536,6 +576,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
+ pointycastle:
+ dependency: transitive
+ description:
+ name: pointycastle
+ sha256: c3120a968135aead39699267f4c74bc9a08e4e909e86bc1b0af5bfd78691123c
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.7.2"
pool:
dependency: transitive
description:
@@ -877,6 +925,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.0"
+ xml:
+ dependency: transitive
+ description:
+ name: xml
+ sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.2.2"
yaml:
dependency: transitive
description:
@@ -885,6 +941,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.1"
+ zoom_pinch_overlay:
+ dependency: "direct main"
+ description:
+ name: zoom_pinch_overlay
+ sha256: cad0aef0127953e3a2ad65aa51660e9c86fa11906e286297f9a70aab69163f64
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.4.1+3"
sdks:
dart: ">=2.19.4 <3.0.0"
flutter: ">=3.4.0-17.0.pre"
diff --git a/pubspec.yaml b/pubspec.yaml
index 48fd6e5..861f667 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -45,10 +45,12 @@ dependencies:
url_launcher: ^6.1.10
cached_network_image: ^3.2.3
html_unescape: ^2.0.0
+ zoom_pinch_overlay: ^1.4.1+3
dev_dependencies:
flutter_test:
sdk: flutter
+ flutter_launcher_icons: ^0.12.0
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
@@ -99,3 +101,12 @@ flutter:
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages
+
+
+flutter_icons:
+ android: "launcher_icon"
+ ios: true
+ image_path: "assets/icon.png"
+ min_sdk_android: 21 # android min sdk min:16, default 21
+ adaptive_icon_background: "#FF9C432E"
+ remove_alpha_ios: true
\ No newline at end of file