Compare commits
No commits in common. "999419757901cce581aef28d0eb036486cbdac66" and "5de8333859ee679ce6d4c01bdbdf2032e5cf050d" have entirely different histories.
9994197579
...
5de8333859
11 changed files with 78 additions and 386 deletions
11
PRIVACY.md
11
PRIVACY.md
|
@ -1,11 +0,0 @@
|
||||||
# Voyage Handbook Privacy Policy and Terms of Use
|
|
||||||
Last updated April 3rd, 2023
|
|
||||||
|
|
||||||
**As the app is being actively developed, it is your responsibility to check for terms and policy updates!**
|
|
||||||
|
|
||||||
- Voyage Handbook (the App) stores information about your recently browsed pages. This is only stored on your device and never sent anywhere
|
|
||||||
- The app contacts the WikiMedia API, which is governed by its own [Privacy Policy](https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:Privacy_policy) and [Terms of Use](https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:Terms_of_Use). As a user of the application you are subject to these terms and policies and are responsible for your actions.
|
|
||||||
- The app and the developers are not responsible for the content displayed through the WikiMedia API.
|
|
||||||
- The app is released and distrbuted under the GNU General Public License version 3, which can be read [here](https://git.mnau.xyz/hernik/voyagehandbook/src/branch/main/LICENSE.md).
|
|
||||||
|
|
||||||
In case of any questions please contact me through e-mail: `matyas <zavinac> caras tecka cafe`
|
|
|
@ -21,12 +21,6 @@ if (flutterVersionName == null) {
|
||||||
flutterVersionName = '1.0'
|
flutterVersionName = '1.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
def keystoreProperties = new Properties()
|
|
||||||
def keystorePropertiesFile = rootProject.file('key.properties')
|
|
||||||
if (keystorePropertiesFile.exists()) {
|
|
||||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
|
||||||
}
|
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||||
|
@ -49,6 +43,7 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
applicationId "cafe.caras.voyagehandbook"
|
applicationId "cafe.caras.voyagehandbook"
|
||||||
// You can update the following values to match your application needs.
|
// You can update the following values to match your application needs.
|
||||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
||||||
|
@ -58,19 +53,13 @@ android {
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
keyAlias keystoreProperties['keyAlias']
|
// TODO: Add your own signing config for the release build.
|
||||||
keyPassword keystoreProperties['keyPassword']
|
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||||
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
|
signingConfig signingConfigs.debug
|
||||||
storePassword keystoreProperties['storePassword']
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
signingConfig signingConfigs.release
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
flutter {
|
flutter {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="cafe.caras.voyagehandbook">
|
package="cafe.caras.voyagehandbook">
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
|
||||||
<application
|
<application
|
||||||
android:label="Voyage Handbook"
|
android:label="Voyage Handbook"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
|
|
|
@ -33,7 +33,6 @@ class MyApp extends StatelessWidget {
|
||||||
return DynamicColorBuilder(
|
return DynamicColorBuilder(
|
||||||
builder: (lightDynamic, darkDynamic) => MaterialApp(
|
builder: (lightDynamic, darkDynamic) => MaterialApp(
|
||||||
title: 'Voyage Handbook',
|
title: 'Voyage Handbook',
|
||||||
debugShowCheckedModeBanner: false,
|
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
useMaterial3: true, colorScheme: lightDynamic ?? lightColorScheme),
|
useMaterial3: true, colorScheme: lightDynamic ?? lightColorScheme),
|
||||||
darkTheme: ThemeData(
|
darkTheme: ThemeData(
|
||||||
|
|
|
@ -34,7 +34,7 @@ Drawer genDrawer(int page, BuildContext context) => Drawer(
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
Text("Created by Matyáš Caras"),
|
Text("Created by Matyáš Caras"),
|
||||||
Text("Not affiliated with WikiVoyage")
|
Text("Thanks to WikiVoyage")
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -62,22 +62,6 @@ Drawer genDrawer(int page, BuildContext context) => Drawer(
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ListTile(
|
|
||||||
selected: page == 3,
|
|
||||||
title: const Text("About"),
|
|
||||||
leading: const Icon(Icons.info_outline),
|
|
||||||
onTap: () => page == 3
|
|
||||||
? Navigator.of(context).pop()
|
|
||||||
: Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (_) => const LicensePage(
|
|
||||||
applicationName: "Voyage Handbook",
|
|
||||||
applicationLegalese:
|
|
||||||
"Copyright ©️ 2023 Matyáš Caras,\nReleased under the GNU GPL version 3",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,17 +1,13 @@
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:html/parser.dart';
|
import 'package:html/parser.dart';
|
||||||
import 'package:html/dom.dart' as dom;
|
import 'package:html/dom.dart' as dom;
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
|
||||||
import 'package:voyagehandbook/api/classes.dart';
|
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:voyagehandbook/views/pageview.dart';
|
import 'package:zoom_pinch_overlay/zoom_pinch_overlay.dart';
|
||||||
import 'package:widget_zoom/widget_zoom.dart';
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Voyage Handbook - The open-source WikiVoyage reader
|
Voyage Handbook - The open-source WikiVoyage reader
|
||||||
|
@ -35,9 +31,8 @@ class PageRenderer {
|
||||||
final ColorScheme scheme;
|
final ColorScheme scheme;
|
||||||
final double height;
|
final double height;
|
||||||
final double width;
|
final double width;
|
||||||
final BuildContext context;
|
|
||||||
|
|
||||||
PageRenderer(this.scheme, this.height, this.width, this.context);
|
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) {
|
ListView renderFromPageHTML(RawPage page) {
|
||||||
|
@ -45,7 +40,7 @@ class PageRenderer {
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 10,
|
height: 10,
|
||||||
),
|
),
|
||||||
SelectableText(
|
Text(
|
||||||
page.title,
|
page.title,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: PageStyles.h1,
|
style: PageStyles.h1,
|
||||||
|
@ -56,34 +51,38 @@ class PageRenderer {
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const SelectableText("From"),
|
const Text("From"),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 5,
|
width: 5,
|
||||||
),
|
),
|
||||||
SelectableText(
|
GestureDetector(
|
||||||
"WikiVoyage",
|
|
||||||
onTap: () => launchUrl(
|
onTap: () => launchUrl(
|
||||||
Uri.parse("https://en.wikivoyage.org/wiki/${page.key}"),
|
Uri.parse("https://en.wikivoyage.org/wiki/${page.key}"),
|
||||||
mode: LaunchMode.externalApplication),
|
mode: LaunchMode.externalApplication),
|
||||||
style: const TextStyle(
|
child: const Text(
|
||||||
decoration: TextDecoration.underline,
|
"WikiVoyage",
|
||||||
fontWeight: FontWeight.bold,
|
style: TextStyle(
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const SelectableText("under"),
|
const Text("under"),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 5,
|
width: 5,
|
||||||
),
|
),
|
||||||
SelectableText(
|
GestureDetector(
|
||||||
page.license.title,
|
|
||||||
onTap: () => launchUrl(Uri.parse(page.license.url),
|
onTap: () => launchUrl(Uri.parse(page.license.url),
|
||||||
mode: LaunchMode.externalApplication),
|
mode: LaunchMode.externalApplication),
|
||||||
style: const TextStyle(
|
child: Text(
|
||||||
decoration: TextDecoration.underline,
|
page.license.title,
|
||||||
|
style: const TextStyle(
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@ -118,7 +117,7 @@ class PageRenderer {
|
||||||
height: 10,
|
height: 10,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
out.add(SelectableText(
|
out.add(Text(
|
||||||
sectionTitle.text,
|
sectionTitle.text,
|
||||||
style: PageStyles.h2,
|
style: PageStyles.h2,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
|
@ -128,7 +127,7 @@ class PageRenderer {
|
||||||
out.add(
|
out.add(
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: SelectableText(
|
child: Text(
|
||||||
sectionTitle.text,
|
sectionTitle.text,
|
||||||
style: PageStyles.h3,
|
style: PageStyles.h3,
|
||||||
),
|
),
|
||||||
|
@ -139,7 +138,7 @@ class PageRenderer {
|
||||||
out.add(
|
out.add(
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: SelectableText(sectionTitle.text, style: PageStyles.h4),
|
child: Text(sectionTitle.text, style: PageStyles.h4),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
@ -147,7 +146,7 @@ class PageRenderer {
|
||||||
out.add(
|
out.add(
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: SelectableText(sectionTitle.text, style: PageStyles.h5),
|
child: Text(sectionTitle.text, style: PageStyles.h5),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
@ -155,7 +154,7 @@ class PageRenderer {
|
||||||
out.add(
|
out.add(
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: SelectableText(sectionTitle.text),
|
child: Text(sectionTitle.text),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
@ -183,15 +182,9 @@ class PageRenderer {
|
||||||
switch (element.localName) {
|
switch (element.localName) {
|
||||||
case "p":
|
case "p":
|
||||||
case "link":
|
case "link":
|
||||||
if (element
|
|
||||||
.getElementsByClassName("mw-kartographer-maplink")
|
|
||||||
.isNotEmpty) break;
|
|
||||||
out.add(
|
out.add(
|
||||||
SelectableText.rich(
|
RichText(
|
||||||
TextSpan(
|
text: TextSpan(children: _renderText(element.innerHtml)),
|
||||||
children: _renderText(element.innerHtml),
|
|
||||||
),
|
|
||||||
style: const TextStyle(height: 1.2),
|
|
||||||
textAlign: TextAlign.justify,
|
textAlign: TextAlign.justify,
|
||||||
),
|
),
|
||||||
); // add paragraph spans as single rich text
|
); // add paragraph spans as single rich text
|
||||||
|
@ -212,52 +205,6 @@ class PageRenderer {
|
||||||
case "section":
|
case "section":
|
||||||
out.addAll(_renderSection(element));
|
out.addAll(_renderSection(element));
|
||||||
break;
|
break;
|
||||||
case "dl":
|
|
||||||
var dd = element.getElementsByTagName("dd").first;
|
|
||||||
var link = RegExp(r'<a .+? href="\.\/(.+?)".+?>(.+?)<', dotAll: true)
|
|
||||||
.firstMatch(dd.innerHtml);
|
|
||||||
if (link == null) {
|
|
||||||
break; // TODO: handle `dd` which are not "see also" links
|
|
||||||
}
|
|
||||||
out.add(
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
const SelectableText(
|
|
||||||
"See also:",
|
|
||||||
style: TextStyle(),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: 5,
|
|
||||||
),
|
|
||||||
SelectableText(
|
|
||||||
link.group(2)!,
|
|
||||||
onTap: () => Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (_) => ArticleView(
|
|
||||||
pageKey: link.group(1)!, name: link.group(2)!),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
style: const TextStyle(
|
|
||||||
fontStyle: FontStyle.italic,
|
|
||||||
decoration: TextDecoration.underline),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
out.add(
|
|
||||||
const SizedBox(
|
|
||||||
height: 5,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case "ul":
|
|
||||||
out.add(_renderList(element));
|
|
||||||
out.add(
|
|
||||||
const SizedBox(
|
|
||||||
height: 10,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case "div":
|
case "div":
|
||||||
if (element.attributes["class"] != null &&
|
if (element.attributes["class"] != null &&
|
||||||
element.attributes["class"] == "pp_cautionbox") {
|
element.attributes["class"] == "pp_cautionbox") {
|
||||||
|
@ -282,7 +229,7 @@ class PageRenderer {
|
||||||
);
|
);
|
||||||
} else if (e.localName == "table") {
|
} else if (e.localName == "table") {
|
||||||
Color? boxColor;
|
Color? boxColor;
|
||||||
var text = <InlineSpan>[];
|
var text = <TextSpan>[];
|
||||||
for (var td in e
|
for (var td in e
|
||||||
.getElementsByTagName("tr")
|
.getElementsByTagName("tr")
|
||||||
.first
|
.first
|
||||||
|
@ -314,8 +261,8 @@ class PageRenderer {
|
||||||
width: 10,
|
width: 10,
|
||||||
),
|
),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: SelectableText.rich(
|
child: RichText(
|
||||||
TextSpan(children: text),
|
text: TextSpan(children: text),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@ -328,46 +275,6 @@ class PageRenderer {
|
||||||
height: 10,
|
height: 10,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (element.classes.contains("mw-kartographer-container")) {
|
|
||||||
var imgs = element
|
|
||||||
.getElementsByTagName("div")
|
|
||||||
.first
|
|
||||||
.getElementsByTagName("a")
|
|
||||||
.first
|
|
||||||
.getElementsByTagName("img");
|
|
||||||
if (imgs.isEmpty) break; // load maps that have a static image
|
|
||||||
var img = imgs[0];
|
|
||||||
var cap = element.getElementsByClassName("thumbcaption")[0];
|
|
||||||
out.add(const SizedBox(
|
|
||||||
height: 10,
|
|
||||||
));
|
|
||||||
out.add(
|
|
||||||
SizedBox(
|
|
||||||
width: width * 0.8,
|
|
||||||
height: height * 0.3,
|
|
||||||
child: WidgetZoom(
|
|
||||||
zoomWidget:
|
|
||||||
CachedNetworkImage(imageUrl: img.attributes["src"]!),
|
|
||||||
heroAnimationTag: 'tag',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
out.add(
|
|
||||||
const SizedBox(
|
|
||||||
height: 3,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
out.add(
|
|
||||||
SelectableText(
|
|
||||||
cap.text,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
out.add(
|
|
||||||
const SizedBox(
|
|
||||||
height: 10,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
@ -405,9 +312,8 @@ class PageRenderer {
|
||||||
// TODO: open wikimedia page?
|
// TODO: open wikimedia page?
|
||||||
width: width * 0.8,
|
width: width * 0.8,
|
||||||
height: height * 0.3,
|
height: height * 0.3,
|
||||||
child: WidgetZoom(
|
child: ZoomOverlay(
|
||||||
heroAnimationTag: 'img${Random().nextInt(999)}',
|
child: CachedNetworkImage(
|
||||||
zoomWidget: CachedNetworkImage(
|
|
||||||
imageUrl: img.attributes["src"]!.replaceAll("//", "https://"),
|
imageUrl: img.attributes["src"]!.replaceAll("//", "https://"),
|
||||||
progressIndicatorBuilder: (context, url, downloadProgress) =>
|
progressIndicatorBuilder: (context, url, downloadProgress) =>
|
||||||
LinearProgressIndicator(value: downloadProgress.progress),
|
LinearProgressIndicator(value: downloadProgress.progress),
|
||||||
|
@ -427,7 +333,7 @@ class PageRenderer {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
out.add(
|
out.add(
|
||||||
SelectableText(
|
Text(
|
||||||
caption,
|
caption,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
|
@ -439,7 +345,7 @@ class PageRenderer {
|
||||||
|
|
||||||
// used to render warning box
|
// used to render warning box
|
||||||
Widget _renderWarning(dom.Element element) {
|
Widget _renderWarning(dom.Element element) {
|
||||||
var content = <InlineSpan>[];
|
var content = <TextSpan>[];
|
||||||
for (var tr in element
|
for (var tr in element
|
||||||
.getElementsByTagName("table")
|
.getElementsByTagName("table")
|
||||||
.first
|
.first
|
||||||
|
@ -458,75 +364,15 @@ class PageRenderer {
|
||||||
return Warning(content: content);
|
return Warning(content: content);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Used to render basic text with links, bold and italic text
|
/// Used to render basic text with bold and italic formatting
|
||||||
List<InlineSpan> _renderText(String innerHtml) {
|
List<TextSpan> _renderText(String innerHtml) {
|
||||||
var unescape = HtmlUnescape();
|
var unescape = HtmlUnescape();
|
||||||
innerHtml = unescape.convert(innerHtml);
|
innerHtml = unescape.convert(innerHtml);
|
||||||
var content = <InlineSpan>[];
|
var content = <TextSpan>[];
|
||||||
var input = innerHtml
|
var input = innerHtml
|
||||||
.replaceAll(
|
.replaceAll(RegExp(r"<(?!(?:\/b)|(?:\/i)|b|i).+?>", dotAll: true), "")
|
||||||
RegExp(r"<(?!(?:\/b)|(?:\/i)|(?:\/a)|(?:b)|(?:i)|(?:a )).+?>",
|
|
||||||
dotAll: true),
|
|
||||||
"")
|
|
||||||
.replaceAll("\\n", "\n")
|
.replaceAll("\\n", "\n")
|
||||||
.replaceAll("<br>", "\n");
|
.replaceAll("<br>", "\n");
|
||||||
|
|
||||||
// first find links
|
|
||||||
var linkMatch = RegExp(
|
|
||||||
r'(?:<(?:i|b).*?>)*<a .+? href="(.+?)" .+?>(.+?)<\/a>(?:\/<(?:i|b)>)*',
|
|
||||||
dotAll: true)
|
|
||||||
.allMatches(input)
|
|
||||||
.toList();
|
|
||||||
var nonLink = input.split(RegExp(
|
|
||||||
r'(?:<(?:i|b).*?>)*<a .+? href="(.+?)" .+?>(.+?)<\/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("<b") ||
|
|
||||||
match.group(0)!.contains("</b>"))
|
|
||||||
? FontWeight.bold
|
|
||||||
: null,
|
|
||||||
fontStyle: (match.group(0)!.contains("<i") ||
|
|
||||||
match.group(0)!.contains("</i>"))
|
|
||||||
? FontStyle.italic
|
|
||||||
: null),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
content.addAll(_formatText(nonLink.last)); // add last
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Formats text (bold, italics)
|
|
||||||
List<InlineSpan> _formatText(String input) {
|
|
||||||
var content = <InlineSpan>[];
|
|
||||||
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 =
|
var needToFormat =
|
||||||
|
@ -540,111 +386,15 @@ class PageRenderer {
|
||||||
var raw = s.group(0)!;
|
var raw = s.group(0)!;
|
||||||
content.add(
|
content.add(
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: RegExp(r">(?!<)(.+?)<").firstMatch(raw)!.group(1)!.replaceAll(
|
text: RegExp(r">(?!<)(.+?)<").firstMatch(raw)!.group(1),
|
||||||
RegExp(r'<(?:(?:\/b)|(?:\/i)|(?:\/a)|(?:b)|(?:i)|(?:a )).*?>'),
|
|
||||||
""), // replace stray tags
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: (raw.contains("<b") || raw.contains("</b>"))
|
fontWeight: (raw.contains("<b")) ? FontWeight.bold : null,
|
||||||
? FontWeight.bold
|
fontStyle: (raw.contains("<i")) ? FontStyle.italic : null,
|
||||||
: null,
|
|
||||||
fontStyle: (raw.contains("<i") || raw.contains("</i>"))
|
|
||||||
? FontStyle.italic
|
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
); // add styled
|
); // add styled
|
||||||
}
|
}
|
||||||
content.add(TextSpan(
|
content.add(TextSpan(text: noFormatting.last)); // add last
|
||||||
text: noFormatting.last.replaceAll(
|
|
||||||
RegExp(r'<(?:(?:\/b)|(?:\/i)|(?:\/a)|(?:b)|(?:i)|(?:a )).*?>'),
|
|
||||||
""))); // add last
|
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
final _extraColorsLight = {
|
|
||||||
"blue": const Color.fromARGB(255, 34, 34, 157),
|
|
||||||
"red": const Color.fromARGB(255, 152, 33, 33)
|
|
||||||
};
|
|
||||||
|
|
||||||
final _extraColorsDark = {
|
|
||||||
"blue": const Color.fromARGB(255, 84, 95, 247),
|
|
||||||
"red": const Color.fromARGB(255, 242, 69, 69)
|
|
||||||
};
|
|
||||||
|
|
||||||
SingleChildScrollView _renderList(dom.Element element) {
|
|
||||||
var out = <Widget>[];
|
|
||||||
var i = 0;
|
|
||||||
for (var item in element.getElementsByTagName("li")) {
|
|
||||||
i++;
|
|
||||||
String? title = (item.getElementsByClassName("listing-name").isNotEmpty)
|
|
||||||
? item.getElementsByClassName("listing-name")[0].text
|
|
||||||
: null;
|
|
||||||
|
|
||||||
Color bubbleColor;
|
|
||||||
if (item.innerHtml.contains("background: #0000FF")) {
|
|
||||||
bubbleColor =
|
|
||||||
(MediaQuery.of(context).platformBrightness == Brightness.dark)
|
|
||||||
? _extraColorsDark["blue"]!
|
|
||||||
: _extraColorsLight["blue"]!;
|
|
||||||
} else if (item.innerHtml.contains("background: #800000")) {
|
|
||||||
bubbleColor =
|
|
||||||
(MediaQuery.of(context).platformBrightness == Brightness.dark)
|
|
||||||
? _extraColorsDark["red"]!
|
|
||||||
: _extraColorsLight["red"]!;
|
|
||||||
} else {
|
|
||||||
bubbleColor = scheme.secondary;
|
|
||||||
}
|
|
||||||
|
|
||||||
var rest = (title != null)
|
|
||||||
? item.text.replaceAll(item.getElementsByTagName("span")[0].text, "")
|
|
||||||
: item.text;
|
|
||||||
out.add(const SizedBox(
|
|
||||||
height: 5,
|
|
||||||
));
|
|
||||||
out.add(
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(8), color: bubbleColor),
|
|
||||||
width: 30,
|
|
||||||
height: 30,
|
|
||||||
child: SelectableText(
|
|
||||||
i.toString(),
|
|
||||||
style: TextStyle(
|
|
||||||
color: scheme.background,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 17),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: 10,
|
|
||||||
),
|
|
||||||
SelectableText.rich(
|
|
||||||
TextSpan(
|
|
||||||
children: [
|
|
||||||
if (title != null)
|
|
||||||
TextSpan(
|
|
||||||
text: title,
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
TextSpan(text: rest)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return SingleChildScrollView(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
children: out,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class Warning extends StatelessWidget {
|
class Warning extends StatelessWidget {
|
||||||
const Warning({super.key, required this.content});
|
const Warning({super.key, required this.content});
|
||||||
final List<InlineSpan> content;
|
final List<TextSpan> content;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
|
@ -95,11 +95,9 @@ class _HomeViewState extends State<HomeView> {
|
||||||
width: MediaQuery.of(context).size.width * 0.9,
|
width: MediaQuery.of(context).size.width * 0.9,
|
||||||
height: 50,
|
height: 50,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () => Navigator.of(context).push(
|
onTap: () => Navigator.of(context).push(MaterialPageRoute(
|
||||||
MaterialPageRoute(
|
builder: (c) =>
|
||||||
builder: (c) => ArticleView(pageKey: r["key"], name: r["name"]),
|
ArticleView(pageKey: r["key"], name: r["name"]))),
|
||||||
),
|
|
||||||
)..then((_) => load()),
|
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
|
@ -77,12 +77,9 @@ class _ArticleViewState extends State<ArticleView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void loadPage() async {
|
void loadPage() async {
|
||||||
var renderer = PageRenderer(
|
var renderer = PageRenderer(Theme.of(context).colorScheme,
|
||||||
Theme.of(context).colorScheme,
|
MediaQuery.of(context).size.height, MediaQuery.of(context).size.width);
|
||||||
MediaQuery.of(context).size.height,
|
try {
|
||||||
MediaQuery.of(context).size.width,
|
|
||||||
context);
|
|
||||||
if (kDebugMode) {
|
|
||||||
_content = [
|
_content = [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: MediaQuery.of(context).size.width * 0.9,
|
width: MediaQuery.of(context).size.width * 0.9,
|
||||||
|
@ -91,28 +88,18 @@ class _ArticleViewState extends State<ArticleView> {
|
||||||
.renderFromPageHTML(await WikiApi.getRawPage(widget.pageKey)),
|
.renderFromPageHTML(await WikiApi.getRawPage(widget.pageKey)),
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
} else {
|
} catch (e) {
|
||||||
try {
|
if (kDebugMode) print(e);
|
||||||
_content = [
|
_content = [
|
||||||
SizedBox(
|
const Text(
|
||||||
width: MediaQuery.of(context).size.width * 0.9,
|
"Error while rendering:",
|
||||||
height: MediaQuery.of(context).size.height,
|
style: PageStyles.h1,
|
||||||
child: renderer
|
),
|
||||||
.renderFromPageHTML(await WikiApi.getRawPage(widget.pageKey)),
|
const SizedBox(
|
||||||
)
|
height: 10,
|
||||||
];
|
),
|
||||||
} catch (e) {
|
Text(e.toString())
|
||||||
_content = [
|
];
|
||||||
const Text(
|
|
||||||
"Error while rendering:",
|
|
||||||
style: PageStyles.h1,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 10,
|
|
||||||
),
|
|
||||||
Text(e.toString())
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {});
|
setState(() {});
|
||||||
|
@ -120,8 +107,5 @@ class _ArticleViewState extends State<ArticleView> {
|
||||||
|
|
||||||
void addToRecents() {
|
void addToRecents() {
|
||||||
StorageAccess.addToRecents(widget.name, widget.pageKey);
|
StorageAccess.addToRecents(widget.name, widget.pageKey);
|
||||||
if (kDebugMode) {
|
|
||||||
print("Added ${widget.name} to recent");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
16
pubspec.lock
16
pubspec.lock
|
@ -909,14 +909,6 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.3.0"
|
||||||
widget_zoom:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: widget_zoom
|
|
||||||
sha256: f7c7352b5ea1f08e9419edddd938852239ad4c94ee70d29b5b5f142b04cb8eca
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.0.1"
|
|
||||||
win32:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -949,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"
|
||||||
|
|
|
@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 1.0.0-alpha.1+1
|
version: 1.0.0+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=2.19.4 <3.0.0'
|
sdk: '>=2.19.4 <3.0.0'
|
||||||
|
@ -45,7 +45,7 @@ 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
|
||||||
widget_zoom: ^0.0.1
|
zoom_pinch_overlay: ^1.4.1+3
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
Loading…
Reference in a new issue