Compare commits

...

6 commits

Author SHA1 Message Date
Matyáš Caras 9994197579 docs: privacy 2023-04-03 19:48:52 +02:00
Matyáš Caras afb921c153 chore: release configuration 2023-04-03 19:13:37 +02:00
Matyáš Caras 2214eb68e7 fix: switch to selectabletext and render links 2023-04-03 19:09:51 +02:00
Matyáš Caras e6989dc264 feat: render more stuff better 2023-04-03 17:07:27 +02:00
Matyáš Caras 27dbbb9221 fix: replace image zoom packages 2023-04-03 15:48:07 +02:00
Matyáš Caras 9f1f6ad0ff fix: add about to drawer 2023-04-03 15:47:53 +02:00
11 changed files with 386 additions and 78 deletions

11
PRIVACY.md Normal file
View file

@ -0,0 +1,11 @@
# 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`

View file

@ -21,6 +21,12 @@ 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"
@ -43,7 +49,6 @@ 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.
@ -53,13 +58,19 @@ android {
versionName flutterVersionName versionName flutterVersionName
} }
buildTypes { signingConfigs {
release { release {
// TODO: Add your own signing config for the release build. keyAlias keystoreProperties['keyAlias']
// Signing with the debug keys for now, so `flutter run --release` works. keyPassword keystoreProperties['keyPassword']
signingConfig signingConfigs.debug storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
} storePassword keystoreProperties['storePassword']
} }
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
} }
flutter { flutter {

View file

@ -1,5 +1,6 @@
<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}"

View file

@ -33,6 +33,7 @@ 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(

View file

@ -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("Thanks to WikiVoyage") Text("Not affiliated with WikiVoyage")
], ],
), ),
), ),
@ -62,6 +62,22 @@ 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",
),
),
),
),
], ],
), ),
); );

View file

@ -1,13 +1,17 @@
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:zoom_pinch_overlay/zoom_pinch_overlay.dart'; import 'package:voyagehandbook/views/pageview.dart';
import 'package:widget_zoom/widget_zoom.dart';
/* /*
Voyage Handbook - The open-source WikiVoyage reader Voyage Handbook - The open-source WikiVoyage reader
@ -31,8 +35,9 @@ 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); PageRenderer(this.scheme, this.height, this.width, this.context);
/// 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) {
@ -40,7 +45,7 @@ class PageRenderer {
const SizedBox( const SizedBox(
height: 10, height: 10,
), ),
Text( SelectableText(
page.title, page.title,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: PageStyles.h1, style: PageStyles.h1,
@ -51,38 +56,34 @@ class PageRenderer {
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const Text("From"), const SelectableText("From"),
const SizedBox( const SizedBox(
width: 5, width: 5,
), ),
GestureDetector( SelectableText(
"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),
child: const Text( style: const TextStyle(
"WikiVoyage", decoration: TextDecoration.underline,
style: TextStyle( fontWeight: FontWeight.bold,
decoration: TextDecoration.underline,
fontWeight: FontWeight.bold,
),
), ),
), ),
], ],
), ),
Row( Row(
children: [ children: [
const Text("under"), const SelectableText("under"),
const SizedBox( const SizedBox(
width: 5, width: 5,
), ),
GestureDetector( SelectableText(
page.license.title,
onTap: () => launchUrl(Uri.parse(page.license.url), onTap: () => launchUrl(Uri.parse(page.license.url),
mode: LaunchMode.externalApplication), mode: LaunchMode.externalApplication),
child: Text( style: const TextStyle(
page.license.title, decoration: TextDecoration.underline,
style: const TextStyle(
decoration: TextDecoration.underline,
),
), ),
) )
], ],
@ -117,7 +118,7 @@ class PageRenderer {
height: 10, height: 10,
), ),
); );
out.add(Text( out.add(SelectableText(
sectionTitle.text, sectionTitle.text,
style: PageStyles.h2, style: PageStyles.h2,
textAlign: TextAlign.center, textAlign: TextAlign.center,
@ -127,7 +128,7 @@ class PageRenderer {
out.add( out.add(
Align( Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Text( child: SelectableText(
sectionTitle.text, sectionTitle.text,
style: PageStyles.h3, style: PageStyles.h3,
), ),
@ -138,7 +139,7 @@ class PageRenderer {
out.add( out.add(
Align( Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Text(sectionTitle.text, style: PageStyles.h4), child: SelectableText(sectionTitle.text, style: PageStyles.h4),
), ),
); );
break; break;
@ -146,7 +147,7 @@ class PageRenderer {
out.add( out.add(
Align( Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Text(sectionTitle.text, style: PageStyles.h5), child: SelectableText(sectionTitle.text, style: PageStyles.h5),
), ),
); );
break; break;
@ -154,7 +155,7 @@ class PageRenderer {
out.add( out.add(
Align( Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Text(sectionTitle.text), child: SelectableText(sectionTitle.text),
), ),
); );
break; break;
@ -182,9 +183,15 @@ 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(
RichText( SelectableText.rich(
text: TextSpan(children: _renderText(element.innerHtml)), TextSpan(
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
@ -205,6 +212,52 @@ 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") {
@ -229,7 +282,7 @@ class PageRenderer {
); );
} else if (e.localName == "table") { } else if (e.localName == "table") {
Color? boxColor; Color? boxColor;
var text = <TextSpan>[]; var text = <InlineSpan>[];
for (var td in e for (var td in e
.getElementsByTagName("tr") .getElementsByTagName("tr")
.first .first
@ -261,8 +314,8 @@ class PageRenderer {
width: 10, width: 10,
), ),
Flexible( Flexible(
child: RichText( child: SelectableText.rich(
text: TextSpan(children: text), TextSpan(children: text),
), ),
) )
], ],
@ -275,6 +328,46 @@ 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:
@ -312,8 +405,9 @@ 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: ZoomOverlay( child: WidgetZoom(
child: CachedNetworkImage( heroAnimationTag: 'img${Random().nextInt(999)}',
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),
@ -333,7 +427,7 @@ class PageRenderer {
), ),
); );
out.add( out.add(
Text( SelectableText(
caption, caption,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@ -345,7 +439,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 = <TextSpan>[]; var content = <InlineSpan>[];
for (var tr in element for (var tr in element
.getElementsByTagName("table") .getElementsByTagName("table")
.first .first
@ -364,15 +458,75 @@ class PageRenderer {
return Warning(content: content); return Warning(content: content);
} }
/// Used to render basic text with bold and italic formatting /// Used to render basic text with links, bold and italic text
List<TextSpan> _renderText(String innerHtml) { List<InlineSpan> _renderText(String innerHtml) {
var unescape = HtmlUnescape(); var unescape = HtmlUnescape();
innerHtml = unescape.convert(innerHtml); innerHtml = unescape.convert(innerHtml);
var content = <TextSpan>[]; var content = <InlineSpan>[];
var input = innerHtml 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", "\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 =
@ -386,15 +540,111 @@ 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), text: RegExp(r">(?!<)(.+?)<").firstMatch(raw)!.group(1)!.replaceAll(
RegExp(r'<(?:(?:\/b)|(?:\/i)|(?:\/a)|(?:b)|(?:i)|(?:a )).*?>'),
""), // replace stray tags
style: TextStyle( style: TextStyle(
fontWeight: (raw.contains("<b")) ? FontWeight.bold : null, fontWeight: (raw.contains("<b") || raw.contains("</b>"))
fontStyle: (raw.contains("<i")) ? FontStyle.italic : null, ? FontWeight.bold
: null,
fontStyle: (raw.contains("<i") || raw.contains("</i>"))
? FontStyle.italic
: null,
), ),
), ),
); // add styled ); // 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; 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,
),
);
}
} }

View file

@ -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<TextSpan> content; final List<InlineSpan> content;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View file

@ -95,9 +95,11 @@ 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(MaterialPageRoute( onTap: () => Navigator.of(context).push(
builder: (c) => MaterialPageRoute(
ArticleView(pageKey: r["key"], name: r["name"]))), builder: (c) => ArticleView(pageKey: r["key"], name: r["name"]),
),
)..then((_) => load()),
child: Align( child: Align(
alignment: Alignment.center, alignment: Alignment.center,
child: Text( child: Text(

View file

@ -77,9 +77,12 @@ class _ArticleViewState extends State<ArticleView> {
} }
void loadPage() async { void loadPage() async {
var renderer = PageRenderer(Theme.of(context).colorScheme, var renderer = PageRenderer(
MediaQuery.of(context).size.height, MediaQuery.of(context).size.width); Theme.of(context).colorScheme,
try { MediaQuery.of(context).size.height,
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,
@ -88,18 +91,28 @@ class _ArticleViewState extends State<ArticleView> {
.renderFromPageHTML(await WikiApi.getRawPage(widget.pageKey)), .renderFromPageHTML(await WikiApi.getRawPage(widget.pageKey)),
) )
]; ];
} catch (e) { } else {
if (kDebugMode) print(e); try {
_content = [ _content = [
const Text( SizedBox(
"Error while rendering:", width: MediaQuery.of(context).size.width * 0.9,
style: PageStyles.h1, height: MediaQuery.of(context).size.height,
), child: renderer
const SizedBox( .renderFromPageHTML(await WikiApi.getRawPage(widget.pageKey)),
height: 10, )
), ];
Text(e.toString()) } catch (e) {
]; _content = [
const Text(
"Error while rendering:",
style: PageStyles.h1,
),
const SizedBox(
height: 10,
),
Text(e.toString())
];
}
} }
setState(() {}); setState(() {});
@ -107,5 +120,8 @@ 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");
}
} }
} }

View file

@ -909,6 +909,14 @@ 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:
@ -941,14 +949,6 @@ 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

@ -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+1 version: 1.0.0-alpha.1+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
zoom_pinch_overlay: ^1.4.1+3 widget_zoom: ^0.0.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: