feat: support l10n and work on offline downloads
This commit is contained in:
parent
9994197579
commit
f91ef2e68f
14 changed files with 317 additions and 24 deletions
|
@ -1,13 +1,17 @@
|
|||
# Voyage Handbook
|
||||
Access [WikiVoyage](https://en.wikivoyage.org) conveniently from your phone!
|
||||
|
||||
## Install
|
||||
- [Google Play](https://play.google.com/store/apps/details?id=cafe.caras.voyagehandbook)
|
||||
- F-Droid (soon)
|
||||
|
||||
## Roadmap
|
||||
*(In no particular order)*
|
||||
|
||||
- [ ] Render articles *completely* using native widgets
|
||||
- [ ] Navigation in articles by heading
|
||||
- [ ] Settings for reader
|
||||
- [ ] Translate UI
|
||||
- [X] Translate UI
|
||||
- [ ] Support different WikiVoyage languages
|
||||
- [ ] Bookmark articles
|
||||
- [ ] Download articles
|
||||
|
|
3
l10n.yaml
Normal file
3
l10n.yaml
Normal file
|
@ -0,0 +1,3 @@
|
|||
arb-dir: lib/l10n
|
||||
template-arb-file: app_en.arb
|
||||
output-localization-file: app_localizations.dart
|
18
lib/l10n/app_cs.arb
Normal file
18
lib/l10n/app_cs.arb
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"home":"Domů",
|
||||
"noRecents":"Nemáte žádné nedávno otevřené články, potáhněte doprava a začněte hledat.",
|
||||
"recentPages":"Nedávné články",
|
||||
"searchAppBarTitle":"Prohledat WikiVoyage",
|
||||
"search":"Hledat",
|
||||
"offlineTitle":"Offline stažení",
|
||||
"offlineDialog":"Chcete stáhnout článek '{article}' pro zobrazení offline? Bude dostupný v sekci 'Stažené'.",
|
||||
"renderError":"Chyba při renderování:",
|
||||
"attrFrom":"Z",
|
||||
"attrUnder":"pod licencí",
|
||||
"downloadsTitle":"Stažené",
|
||||
"noDownloads":"Nemáte žádné stažené články. Vyhledejte nějaké, a poté je dlouhým podržením stáhněte.",
|
||||
"about":"O Aplikcai",
|
||||
"sourceCode":"Zdrojový kód",
|
||||
"creditsCreatedBy":"Vytvořil Matyáš Caras",
|
||||
"creditsAffiliation":"Není nijak spojeno s WikiVoyage"
|
||||
}
|
55
lib/l10n/app_en.arb
Normal file
55
lib/l10n/app_en.arb
Normal file
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"home":"Home",
|
||||
"@home":{
|
||||
"description":"As seen on the home page"
|
||||
},
|
||||
"noRecents":"You haven't opened anything recently, swipe right and start searching.",
|
||||
"@noRecents":{
|
||||
"description":"Shown when there are no recent pages"
|
||||
},
|
||||
"recentPages":"Recent articles",
|
||||
"@recentPages":{
|
||||
"description":"Title of the home page"
|
||||
},
|
||||
"searchAppBarTitle":"Search WikiVoyage",
|
||||
"@searchAppBarTitle":{
|
||||
"description":"Appbar title"
|
||||
},
|
||||
"search":"Search",
|
||||
"offlineTitle":"Offline download",
|
||||
"@offlineTitle":{
|
||||
"description":"Title of the dialog that appears when downloading an article offline"
|
||||
},
|
||||
"offlineDialog":"Would you like to download the article '{article}' for offline viewing? It will be available in the 'Downloads' section.",
|
||||
"@offlineDialog":{
|
||||
"description":"Offline download dialog content text",
|
||||
"placeholders":{
|
||||
"article":{
|
||||
"type":"String",
|
||||
"example":"Rail travel in Japan",
|
||||
"description":"WikiVoyage article name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"renderError":"Error while rendering:",
|
||||
"@renderError":{
|
||||
"description":"Displayed when rendering of a page throws an error"
|
||||
},
|
||||
"attrFrom":"From",
|
||||
"@pageFrom":{
|
||||
"description":"The *From* part of 'From WikiVoyage'"
|
||||
},
|
||||
"attrUnder":"under",
|
||||
"@attrUnder":{
|
||||
"description":"The *under* part of license attribution, e.g. 'under CC BY-SA 3.0'"
|
||||
},
|
||||
"downloadsTitle":"Downloads",
|
||||
"@downloadsTitle":{
|
||||
"description":"The appbar title for the Downloads page"
|
||||
},
|
||||
"noDownloads":"You don't have any articles downloaded. Search for some and then long-press them to download them offline.",
|
||||
"about":"About",
|
||||
"sourceCode":"Source code",
|
||||
"creditsCreatedBy":"Created by Matyáš Caras",
|
||||
"creditsAffiliation":"Not affiliated with WikiVoyage"
|
||||
}
|
|
@ -2,7 +2,7 @@ import 'package:dynamic_color/dynamic_color.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:voyagehandbook/util/color_schemes.g.dart';
|
||||
import 'package:voyagehandbook/views/home.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
/*
|
||||
Voyage Handbook - The open-source WikiVoyage reader
|
||||
Copyright (C) 2023 Matyáš Caras
|
||||
|
@ -33,6 +33,8 @@ class MyApp extends StatelessWidget {
|
|||
return DynamicColorBuilder(
|
||||
builder: (lightDynamic, darkDynamic) => MaterialApp(
|
||||
title: 'Voyage Handbook',
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
useMaterial3: true, colorScheme: lightDynamic ?? lightColorScheme),
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:voyagehandbook/views/downloads.dart';
|
||||
import 'package:voyagehandbook/views/home.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import '../views/search.dart';
|
||||
|
||||
/*
|
||||
|
@ -28,13 +31,13 @@ Drawer genDrawer(int page, BuildContext context) => Drawer(
|
|||
children: [
|
||||
DrawerHeader(
|
||||
child: Column(
|
||||
children: const [
|
||||
Text(
|
||||
children: [
|
||||
const Text(
|
||||
"Voyage Handbook",
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text("Created by Matyáš Caras"),
|
||||
Text("Not affiliated with WikiVoyage")
|
||||
Text(AppLocalizations.of(context)!.creditsCreatedBy),
|
||||
Text(AppLocalizations.of(context)!.creditsAffiliation)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -52,7 +55,7 @@ Drawer genDrawer(int page, BuildContext context) => Drawer(
|
|||
),
|
||||
ListTile(
|
||||
selected: page == 2,
|
||||
title: const Text("Search"),
|
||||
title: Text(AppLocalizations.of(context)!.search),
|
||||
leading: const Icon(Icons.search),
|
||||
onTap: () => page == 2
|
||||
? Navigator.of(context).pop()
|
||||
|
@ -64,9 +67,21 @@ Drawer genDrawer(int page, BuildContext context) => Drawer(
|
|||
),
|
||||
ListTile(
|
||||
selected: page == 3,
|
||||
title: const Text("About"),
|
||||
leading: const Icon(Icons.info_outline),
|
||||
title: Text(AppLocalizations.of(context)!.downloadsTitle),
|
||||
leading: const Icon(Icons.search),
|
||||
onTap: () => page == 3
|
||||
? Navigator.of(context).pop()
|
||||
: Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const DownloadsView(),
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
selected: page == 99,
|
||||
title: Text(AppLocalizations.of(context)!.about),
|
||||
leading: const Icon(Icons.info_outline),
|
||||
onTap: () => page == 99
|
||||
? Navigator.of(context).pop()
|
||||
: Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
|
@ -78,6 +93,15 @@ Drawer genDrawer(int page, BuildContext context) => Drawer(
|
|||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
selected: page == 99,
|
||||
title: Text(AppLocalizations.of(context)!.sourceCode),
|
||||
leading: const Icon(Icons.code),
|
||||
onTap: () => page == 99
|
||||
? Navigator.of(context).pop()
|
||||
: launchUrlString("https://git.mnau.xyz/hernik/voyagehandbook",
|
||||
mode: LaunchMode.externalApplication),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
@ -12,6 +12,7 @@ import 'package:voyagehandbook/util/widgets/warning.dart';
|
|||
import 'package:html_unescape/html_unescape_small.dart';
|
||||
import 'package:voyagehandbook/views/pageview.dart';
|
||||
import 'package:widget_zoom/widget_zoom.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
/*
|
||||
Voyage Handbook - The open-source WikiVoyage reader
|
||||
|
@ -36,8 +37,16 @@ class PageRenderer {
|
|||
final double height;
|
||||
final double width;
|
||||
final BuildContext context;
|
||||
final AppLocalizations loc;
|
||||
|
||||
PageRenderer(this.scheme, this.height, this.width, this.context);
|
||||
/// For offline downloads; don't bother rendering the widget tree
|
||||
final bool offline;
|
||||
|
||||
/// HTML for offline download / caching
|
||||
String outHtml = "";
|
||||
|
||||
PageRenderer(this.scheme, this.height, this.width, this.context, this.loc,
|
||||
{this.offline = false});
|
||||
|
||||
/// Used to create Widgets from raw HTML from WM API
|
||||
ListView renderFromPageHTML(RawPage page) {
|
||||
|
@ -56,7 +65,7 @@ class PageRenderer {
|
|||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SelectableText("From"),
|
||||
SelectableText(loc.attrFrom),
|
||||
const SizedBox(
|
||||
width: 5,
|
||||
),
|
||||
|
@ -74,7 +83,7 @@ class PageRenderer {
|
|||
),
|
||||
Row(
|
||||
children: [
|
||||
const SelectableText("under"),
|
||||
SelectableText(loc.attrUnder),
|
||||
const SizedBox(
|
||||
width: 5,
|
||||
),
|
||||
|
@ -89,11 +98,13 @@ class PageRenderer {
|
|||
],
|
||||
)
|
||||
];
|
||||
outHtml = "<html><head></head><body>";
|
||||
var document = parse(page.html);
|
||||
var sections = document.body!.getElementsByTagName("section");
|
||||
for (var sec in sections) {
|
||||
if (sec.localName == "section") {
|
||||
out.addAll(_renderSection(sec));
|
||||
if (!offline) out.addAll(_renderSection(sec));
|
||||
outHtml += sec.outerHtml;
|
||||
}
|
||||
}
|
||||
var l = ListView.builder(
|
||||
|
@ -373,6 +384,7 @@ class PageRenderer {
|
|||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
element.remove();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:voyagehandbook/api/wikimedia.dart';
|
||||
|
||||
/*
|
||||
Voyage Handbook - The open-source WikiVoyage reader
|
||||
|
@ -35,6 +38,19 @@ class StorageAccess {
|
|||
.toList();
|
||||
}
|
||||
|
||||
/// Get files in `offline` folder, which contains recently opened pages
|
||||
static Future<List<Map<String, dynamic>>> get offline async {
|
||||
var files =
|
||||
Directory("${(await getApplicationDocumentsDirectory()).path}/offline");
|
||||
if (!files.existsSync()) files.createSync();
|
||||
return files
|
||||
.listSync()
|
||||
.whereType<File>()
|
||||
.toList()
|
||||
.map<Map<String, dynamic>>((e) => jsonDecode(e.readAsStringSync()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
static void addToRecents(String pageName, String pageKey) async {
|
||||
var files =
|
||||
Directory("${(await getApplicationDocumentsDirectory()).path}/recent");
|
||||
|
@ -70,4 +86,49 @@ class StorageAccess {
|
|||
recent.writeAsStringSync(jsonEncode(recentContent));
|
||||
}
|
||||
}
|
||||
|
||||
static Future<bool> isDownloaded(String pageKey) async {
|
||||
var files =
|
||||
Directory("${(await getApplicationDocumentsDirectory()).path}/recent");
|
||||
var offlinePage = File("${files.path}/$pageKey");
|
||||
return offlinePage.existsSync();
|
||||
}
|
||||
|
||||
static Future<void> downloadArticle(String pageKey, String pageTitle) async {
|
||||
var files =
|
||||
Directory("${(await getApplicationDocumentsDirectory()).path}/offline");
|
||||
var offlinePage = File("${files.path}/$pageKey");
|
||||
var page = parse(await WikiApi.getRawPage(pageKey))
|
||||
.body!
|
||||
.getElementsByTagName("section");
|
||||
var out = "<html><head></head><body>";
|
||||
for (var el in page) {
|
||||
out += el.outerHtml;
|
||||
var imgMatch = RegExp(r'<img src="(.+?)">').allMatches(el.innerHtml);
|
||||
if (imgMatch.isNotEmpty) {
|
||||
// download images offline
|
||||
for (var match in imgMatch) {
|
||||
var src = match.group(1)!;
|
||||
var r = await Dio().get(
|
||||
src,
|
||||
options: Options(
|
||||
responseType: ResponseType.bytes,
|
||||
followRedirects: false,
|
||||
validateStatus: (status) {
|
||||
return (status ?? 200) < 500;
|
||||
},
|
||||
),
|
||||
);
|
||||
var img = File("${files.path}/${src.split('/').last}");
|
||||
print(img.path);
|
||||
var openImg = img.openSync(mode: FileMode.write);
|
||||
openImg.writeFromSync(r.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (page.isEmpty) return Future.error("No sections to save");
|
||||
offlinePage.writeAsStringSync(
|
||||
jsonEncode({"title": pageTitle, "key": pageKey, "content": out}),
|
||||
mode: FileMode.writeOnly);
|
||||
}
|
||||
}
|
||||
|
|
75
lib/views/downloads.dart
Normal file
75
lib/views/downloads.dart
Normal file
|
@ -0,0 +1,75 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:voyagehandbook/util/drawer.dart';
|
||||
import 'package:voyagehandbook/util/storage.dart';
|
||||
import 'package:voyagehandbook/views/pageview.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
class DownloadsView extends StatefulWidget {
|
||||
const DownloadsView({super.key});
|
||||
|
||||
@override
|
||||
State<DownloadsView> createState() => _DownloadsViewState();
|
||||
}
|
||||
|
||||
class _DownloadsViewState extends State<DownloadsView> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
loadDownloads();
|
||||
}
|
||||
|
||||
var _content = <Widget>[];
|
||||
var _isLoading = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(AppLocalizations.of(context)!.downloadsTitle),
|
||||
),
|
||||
drawer: genDrawer(3, context),
|
||||
body: SizedBox(
|
||||
height: MediaQuery.of(context).size.height,
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: _isLoading
|
||||
? [const CircularProgressIndicator()]
|
||||
: _content.isEmpty
|
||||
? [Text(AppLocalizations.of(context)!.noDownloads)]
|
||||
: _content,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void loadDownloads() async {
|
||||
var files = await StorageAccess.offline;
|
||||
_content = List.generate(
|
||||
files.length,
|
||||
(index) => SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
height: MediaQuery.of(context).size.height * 0.15,
|
||||
child: InkWell(
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ArticleView(
|
||||
pageKey: files[index]["key"],
|
||||
name: files[index]["title"],
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(files[index]["title"]),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
_isLoading = false;
|
||||
setState(() {});
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import 'package:voyagehandbook/util/storage.dart';
|
|||
import 'package:voyagehandbook/util/styles.dart';
|
||||
import 'package:voyagehandbook/views/pageview.dart';
|
||||
import 'package:voyagehandbook/views/search.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
/*
|
||||
Voyage Handbook - The open-source WikiVoyage reader
|
||||
|
@ -48,7 +49,7 @@ class _HomeViewState extends State<HomeView> {
|
|||
),
|
||||
child: const Icon(Icons.search),
|
||||
),
|
||||
appBar: AppBar(title: const Text("Home")),
|
||||
appBar: AppBar(title: Text(AppLocalizations.of(context)!.home)),
|
||||
drawer: genDrawer(1, context),
|
||||
body: Center(
|
||||
child: SizedBox(
|
||||
|
@ -58,9 +59,8 @@ class _HomeViewState extends State<HomeView> {
|
|||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: (_recents.isEmpty)
|
||||
? [
|
||||
const Flexible(
|
||||
child: Text(
|
||||
"You haven't opened anything recently, swipe right and start searching."),
|
||||
Flexible(
|
||||
child: Text(AppLocalizations.of(context)!.noRecents),
|
||||
)
|
||||
]
|
||||
: _recents),
|
||||
|
@ -79,9 +79,10 @@ class _HomeViewState extends State<HomeView> {
|
|||
DateTime.fromMillisecondsSinceEpoch(b["date"]))
|
||||
? 0
|
||||
: 1);
|
||||
if (!mounted) return;
|
||||
_recents = [
|
||||
const Text(
|
||||
"Recent pages",
|
||||
Text(
|
||||
AppLocalizations.of(context)!.recentPages,
|
||||
style: PageStyles.h1,
|
||||
),
|
||||
const SizedBox(
|
||||
|
|
|
@ -5,6 +5,7 @@ import 'package:voyagehandbook/util/drawer.dart';
|
|||
import 'package:voyagehandbook/util/render.dart';
|
||||
import 'package:voyagehandbook/util/storage.dart';
|
||||
import 'package:voyagehandbook/util/styles.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
/*
|
||||
Voyage Handbook - The open-source WikiVoyage reader
|
||||
|
@ -81,7 +82,8 @@ class _ArticleViewState extends State<ArticleView> {
|
|||
Theme.of(context).colorScheme,
|
||||
MediaQuery.of(context).size.height,
|
||||
MediaQuery.of(context).size.width,
|
||||
context);
|
||||
context,
|
||||
AppLocalizations.of(context)!);
|
||||
if (kDebugMode) {
|
||||
_content = [
|
||||
SizedBox(
|
||||
|
@ -103,8 +105,8 @@ class _ArticleViewState extends State<ArticleView> {
|
|||
];
|
||||
} catch (e) {
|
||||
_content = [
|
||||
const Text(
|
||||
"Error while rendering:",
|
||||
Text(
|
||||
AppLocalizations.of(context)!.renderError,
|
||||
style: PageStyles.h1,
|
||||
),
|
||||
const SizedBox(
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'package:voyagehandbook/api/classes.dart';
|
|||
import 'package:voyagehandbook/api/wikimedia.dart';
|
||||
import 'package:voyagehandbook/views/pageview.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import '../util/drawer.dart';
|
||||
|
||||
/*
|
||||
|
@ -36,7 +37,8 @@ class _SearchViewState extends State<SearchView> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("Search WikiVoyage")),
|
||||
appBar:
|
||||
AppBar(title: Text(AppLocalizations.of(context)!.searchAppBarTitle)),
|
||||
drawer: genDrawer(2, context),
|
||||
body: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
|
@ -59,7 +61,7 @@ class _SearchViewState extends State<SearchView> {
|
|||
_searchResults = r;
|
||||
setState(() {});
|
||||
},
|
||||
child: const Text("Search"),
|
||||
child: Text(AppLocalizations.of(context)!.search),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 15,
|
||||
|
@ -129,6 +131,23 @@ class _SearchViewState extends State<SearchView> {
|
|||
child: Card(
|
||||
elevation: 2,
|
||||
child: InkWell(
|
||||
onLongPress: () {
|
||||
// Show download dialog
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
title: Text(
|
||||
AppLocalizations.of(context)!
|
||||
.offlineTitle),
|
||||
content: Text(
|
||||
AppLocalizations.of(context)!
|
||||
.offlineDialog(
|
||||
_searchResults[index]
|
||||
.title),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
|
|
13
pubspec.lock
13
pubspec.lock
|
@ -310,6 +310,11 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
flutter_localizations:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
|
@ -392,6 +397,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.0"
|
||||
intl:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: intl
|
||||
sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.17.0"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -46,6 +46,9 @@ dependencies:
|
|||
cached_network_image: ^3.2.3
|
||||
html_unescape: ^2.0.0
|
||||
widget_zoom: ^0.0.1
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
intl: any
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
@ -65,6 +68,7 @@ dev_dependencies:
|
|||
|
||||
# The following section is specific to Flutter packages.
|
||||
flutter:
|
||||
generate: true
|
||||
|
||||
# The following line ensures that the Material Icons font is
|
||||
# included with your application, so that you can use the icons in
|
||||
|
|
Loading…
Reference in a new issue