feat: add zoompinch and render as class

This commit is contained in:
Matyáš Caras 2023-03-28 20:03:21 +02:00
parent 2506eb7f4a
commit 54642131b0
4 changed files with 428 additions and 270 deletions

View file

@ -7,6 +7,7 @@ import 'package:voyagehandbook/api/classes.dart';
import 'package:voyagehandbook/util/styles.dart'; import 'package:voyagehandbook/util/styles.dart';
import 'package:voyagehandbook/util/widgets/warning.dart'; import 'package:voyagehandbook/util/widgets/warning.dart';
import 'package:html_unescape/html_unescape_small.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 Voyage Handbook - The open-source WikiVoyage reader
@ -24,11 +25,17 @@ import 'package:html_unescape/html_unescape_small.dart';
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
final _ignoredTags = ["style", "script"]; final _ignoredTags = ["style", "script"];
class PageRenderer {
final ColorScheme scheme;
final double height;
final double width;
PageRenderer(this.scheme, this.height, this.width);
/// Used to create Widgets from raw HTML from WM API /// Used to create Widgets from raw HTML from WM API
ListView renderFromPageHTML(RawPage page, double height, double width) { ListView renderFromPageHTML(RawPage page) {
var out = <Widget>[ var out = <Widget>[
const SizedBox( const SizedBox(
height: 10, height: 10,
@ -85,7 +92,7 @@ ListView renderFromPageHTML(RawPage page, double height, double width) {
var sections = document.body!.getElementsByTagName("section"); var sections = document.body!.getElementsByTagName("section");
for (var sec in sections) { for (var sec in sections) {
if (sec.localName == "section") { if (sec.localName == "section") {
out.addAll(_renderSection(sec, height, width)); out.addAll(_renderSection(sec));
} }
} }
var l = ListView.builder( var l = ListView.builder(
@ -96,11 +103,11 @@ ListView renderFromPageHTML(RawPage page, double height, double width) {
return l; return l;
} }
List<Widget> _renderSection(dom.Element sec, double height, double width) { List<Widget> _renderSection(dom.Element sec) {
var out = <Widget>[]; var out = <Widget>[];
// Get Section Title // Get Section Title
var headings = sec.children var headings = sec.children.where(
.where((element) => ["h2", "h3", "h4", "h5"].contains(element.localName)); (element) => ["h2", "h3", "h4", "h5"].contains(element.localName));
var sectionTitle = (headings.isNotEmpty) ? headings.first : null; var sectionTitle = (headings.isNotEmpty) ? headings.first : null;
if (sectionTitle != null) { if (sectionTitle != null) {
switch (sectionTitle.localName) { switch (sectionTitle.localName) {
@ -188,48 +195,7 @@ List<Widget> _renderSection(dom.Element sec, double height, double width) {
); // space paragraphs ); // space paragraphs
break; break;
case "figure": case "figure":
out.addAll(_renderImageFigure(element));
/// 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://"),
),
),
),
//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( out.add(
const SizedBox( const SizedBox(
height: 10, height: 10,
@ -237,7 +203,7 @@ List<Widget> _renderSection(dom.Element sec, double height, double width) {
); );
break; break;
case "section": case "section":
out.addAll(_renderSection(element, height, width)); out.addAll(_renderSection(element));
break; break;
case "div": case "div":
if (element.attributes["class"] != null && if (element.attributes["class"] != null &&
@ -248,6 +214,67 @@ List<Widget> _renderSection(dom.Element sec, double height, double width) {
height: 10, 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 = <TextSpan>[];
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; break;
default: default:
@ -264,6 +291,58 @@ List<Widget> _renderSection(dom.Element sec, double height, double width) {
return out; return out;
} }
List<Widget> _renderImageFigure(dom.Element element) {
var out = <Widget>[];
/// 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 out;
}
// used to render warning box // used to render warning box
Widget _renderWarning(dom.Element element) { Widget _renderWarning(dom.Element element) {
var content = <TextSpan>[]; var content = <TextSpan>[];
@ -292,15 +371,18 @@ List<TextSpan> _renderText(String innerHtml) {
var content = <TextSpan>[]; var content = <TextSpan>[];
var input = innerHtml var input = innerHtml
.replaceAll(RegExp(r"<(?!(?:\/b)|(?:\/i)|b|i).+?>", dotAll: true), "") .replaceAll(RegExp(r"<(?!(?:\/b)|(?:\/i)|b|i).+?>", dotAll: true), "")
.replaceAll("\\n", "\r"); .replaceAll("\\n", "\n")
.replaceAll("<br>", "\n");
var noFormatting = var noFormatting =
input.split(RegExp(r"(?:<b.*?>.+?<\/b>)|<i.*?>.+?<\/i>", dotAll: true)); input.split(RegExp(r"(?:<b.*?>.+?<\/b>)|<i.*?>.+?<\/i>", dotAll: true));
var needToFormat = RegExp(r"(?:<b.*?>.+?<\/b>)|<i.*?>.+?<\/i>", dotAll: true) var needToFormat =
RegExp(r"(?:<b.*?>.+?<\/b>)|<i.*?>.+?<\/i>", dotAll: true)
.allMatches(input) .allMatches(input)
.toList(); .toList();
for (var s in needToFormat) { for (var s in needToFormat) {
content.add(TextSpan( content.add(TextSpan(
text: noFormatting[needToFormat.indexOf(s)])); // add text before styled text:
noFormatting[needToFormat.indexOf(s)])); // add text before styled
var raw = s.group(0)!; var raw = s.group(0)!;
content.add( content.add(
TextSpan( TextSpan(
@ -315,3 +397,4 @@ List<TextSpan> _renderText(String innerHtml) {
content.add(TextSpan(text: noFormatting.last)); // add last content.add(TextSpan(text: noFormatting.last)); // add last
return content; return content;
} }
}

View file

@ -77,15 +77,15 @@ class _ArticleViewState extends State<ArticleView> {
} }
void loadPage() async { void loadPage() async {
var renderer = PageRenderer(Theme.of(context).colorScheme,
MediaQuery.of(context).size.height, MediaQuery.of(context).size.width);
try { try {
_content = [ _content = [
SizedBox( SizedBox(
width: MediaQuery.of(context).size.width * 0.9, width: MediaQuery.of(context).size.width * 0.9,
height: MediaQuery.of(context).size.height, height: MediaQuery.of(context).size.height,
child: renderFromPageHTML( child: renderer
await WikiApi.getRawPage(widget.pageKey), .renderFromPageHTML(await WikiApi.getRawPage(widget.pageKey)),
MediaQuery.of(context).size.height,
MediaQuery.of(context).size.width),
) )
]; ];
} catch (e) { } catch (e) {

View file

@ -17,6 +17,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.8.0" version: "5.8.0"
archive:
dependency: transitive
description:
name: archive
sha256: d6347d54a2d8028e0437e3c099f66fdb8ae02c4720c1e7534c9f24c10351f85d
url: "https://pub.dev"
source: hosted
version: "3.3.6"
args: args:
dependency: transitive dependency: transitive
description: description:
@ -145,6 +153,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.2" 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: clock:
dependency: transitive dependency: transitive
description: description:
@ -278,6 +294,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.3.0" 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: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -360,6 +384,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.2" version: "4.0.2"
image:
dependency: transitive
description:
name: image
sha256: "483a389d6ccb292b570c31b3a193779b1b0178e7eb571986d9a49904b6861227"
url: "https://pub.dev"
source: hosted
version: "4.0.15"
io: io:
dependency: transitive dependency: transitive
description: description:
@ -520,6 +552,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.11.1" version: "1.11.1"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4"
url: "https://pub.dev"
source: hosted
version: "5.1.0"
platform: platform:
dependency: transitive dependency: transitive
description: description:
@ -536,6 +576,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" version: "2.1.4"
pointycastle:
dependency: transitive
description:
name: pointycastle
sha256: c3120a968135aead39699267f4c74bc9a08e4e909e86bc1b0af5bfd78691123c
url: "https://pub.dev"
source: hosted
version: "3.7.2"
pool: pool:
dependency: transitive dependency: transitive
description: description:
@ -877,6 +925,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" version: "1.0.0"
xml:
dependency: transitive
description:
name: xml
sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5"
url: "https://pub.dev"
source: hosted
version: "6.2.2"
yaml: yaml:
dependency: transitive dependency: transitive
description: description:
@ -885,6 +941,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.1" 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: sdks:
dart: ">=2.19.4 <3.0.0" dart: ">=2.19.4 <3.0.0"
flutter: ">=3.4.0-17.0.pre" flutter: ">=3.4.0-17.0.pre"

View file

@ -45,10 +45,12 @@ dependencies:
url_launcher: ^6.1.10 url_launcher: ^6.1.10
cached_network_image: ^3.2.3 cached_network_image: ^3.2.3
html_unescape: ^2.0.0 html_unescape: ^2.0.0
zoom_pinch_overlay: ^1.4.1+3
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_launcher_icons: ^0.12.0
# The "flutter_lints" package below contains a set of recommended lints to # The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is # encourage good coding practices. The lint set provided by the package is
@ -99,3 +101,12 @@ flutter:
# #
# For details regarding fonts from package dependencies, # For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages # 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