feat: complete download
This commit is contained in:
parent
f91ef2e68f
commit
33839b6b72
11 changed files with 335 additions and 78 deletions
|
@ -39,7 +39,13 @@ cd voyagehandbook
|
|||
./flutterw doctor
|
||||
```
|
||||
|
||||
3. Run on your connected device
|
||||
3. Generate locales
|
||||
|
||||
```sh
|
||||
./flutterw gen-l10n
|
||||
```
|
||||
|
||||
4. Run on your connected device
|
||||
|
||||
```sh
|
||||
./flutterw run
|
||||
|
|
|
@ -25,8 +25,10 @@ class SearchResponse {
|
|||
final String title;
|
||||
final String excerpt;
|
||||
final String? description;
|
||||
const SearchResponse(
|
||||
this.id, this.key, this.title, this.excerpt, this.description);
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
bool downloaded;
|
||||
SearchResponse(this.id, this.key, this.title, this.excerpt, this.description,
|
||||
{this.downloaded = false});
|
||||
|
||||
/// Connect the generated function to the `fromJson`
|
||||
/// factory.
|
||||
|
@ -56,11 +58,10 @@ class RawPage {
|
|||
final String title;
|
||||
@JsonKey(fromJson: _editedFromJson, toJson: _editedToJson, name: "latest")
|
||||
final String edited;
|
||||
final String html;
|
||||
String html;
|
||||
final LicenseAttribution license;
|
||||
|
||||
const RawPage(
|
||||
this.id, this.key, this.title, this.edited, this.html, this.license);
|
||||
RawPage(this.id, this.key, this.title, this.edited, this.html, this.license);
|
||||
|
||||
factory RawPage.fromJson(Map<String, dynamic> json) =>
|
||||
_$RawPageFromJson(json);
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'dart:convert';
|
|||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:voyagehandbook/api/classes.dart';
|
||||
import 'package:voyagehandbook/util/storage.dart';
|
||||
|
||||
/*
|
||||
Voyage Handbook - The open-source WikiVoyage reader
|
||||
|
@ -39,8 +40,14 @@ class WikiApi {
|
|||
var r = await _getRequest("search/page?q=$q&limit=$limit");
|
||||
if (r.statusCode! > 399) return Future.error("API error ${r.statusCode}");
|
||||
var json = jsonDecode(r.data)["pages"];
|
||||
return List<SearchResponse>.generate(
|
||||
var list = List<SearchResponse>.generate(
|
||||
json.length, (index) => SearchResponse.fromJson(json[index]));
|
||||
for (var item in list) {
|
||||
if (await StorageAccess.isDownloaded(item.key)) {
|
||||
list[list.indexOf(item)].downloaded = true;
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
static Future<RawPage> getRawPage(String key) async {
|
||||
|
|
|
@ -14,5 +14,13 @@
|
|||
"about":"O Aplikcai",
|
||||
"sourceCode":"Zdrojový kód",
|
||||
"creditsCreatedBy":"Vytvořil Matyáš Caras",
|
||||
"creditsAffiliation":"Není nijak spojeno s WikiVoyage"
|
||||
"creditsAffiliation":"Není nijak spojeno s WikiVoyage",
|
||||
"yes":"Ano",
|
||||
"no":"Ne",
|
||||
"ok":"Ok",
|
||||
"downloading":"Stahuji, vyčkejte prosím...",
|
||||
"downloadComplete":"Stahování dokončeno, najdete ho v sekci 'Stažené'.",
|
||||
"error":"Nastala chyba",
|
||||
"offlineError":"Vypadá to, že jste offline a tento článek nemáte stažený.",
|
||||
"imageError":"Chyba při stahování obrázku, zkontrolujte připojení."
|
||||
}
|
|
@ -51,5 +51,22 @@
|
|||
"about":"About",
|
||||
"sourceCode":"Source code",
|
||||
"creditsCreatedBy":"Created by Matyáš Caras",
|
||||
"creditsAffiliation":"Not affiliated with WikiVoyage"
|
||||
"creditsAffiliation":"Not affiliated with WikiVoyage",
|
||||
"yes":"Yes",
|
||||
"no":"No",
|
||||
"ok":"Ok",
|
||||
"downloading":"Downloading, please wait...",
|
||||
"@downloading":{
|
||||
"description":"Shown in a dialog when downloading something"
|
||||
},
|
||||
"downloadComplete":"Download complete, you will find it in the 'Downloads' section.",
|
||||
"error":"An error occured",
|
||||
"@error":{
|
||||
"description":"Generic error dialog title"
|
||||
},
|
||||
"offlineError":"You seem to be offline and the curren article is not in your downloads.",
|
||||
"@offlineError":{
|
||||
"description":"Shown when trying to render a page offline"
|
||||
},
|
||||
"imageError":"Error downloading image, check network connection."
|
||||
}
|
|
@ -68,7 +68,7 @@ Drawer genDrawer(int page, BuildContext context) => Drawer(
|
|||
ListTile(
|
||||
selected: page == 3,
|
||||
title: Text(AppLocalizations.of(context)!.downloadsTitle),
|
||||
leading: const Icon(Icons.search),
|
||||
leading: const Icon(Icons.download),
|
||||
onTap: () => page == 3
|
||||
? Navigator.of(context).pop()
|
||||
: Navigator.of(context).push(
|
||||
|
|
|
@ -7,6 +7,7 @@ import 'package:html/dom.dart' as dom;
|
|||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:voyagehandbook/api/classes.dart';
|
||||
import 'package:voyagehandbook/util/storage.dart';
|
||||
import 'package:voyagehandbook/util/styles.dart';
|
||||
import 'package:voyagehandbook/util/widgets/warning.dart';
|
||||
import 'package:html_unescape/html_unescape_small.dart';
|
||||
|
@ -49,7 +50,7 @@ class PageRenderer {
|
|||
{this.offline = false});
|
||||
|
||||
/// Used to create Widgets from raw HTML from WM API
|
||||
ListView renderFromPageHTML(RawPage page) {
|
||||
Future<ListView> renderFromPageHTML(RawPage page) async {
|
||||
var out = <Widget>[
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
|
@ -85,14 +86,17 @@ class PageRenderer {
|
|||
children: [
|
||||
SelectableText(loc.attrUnder),
|
||||
const SizedBox(
|
||||
width: 5,
|
||||
width: 2,
|
||||
),
|
||||
SelectableText(
|
||||
page.license.title,
|
||||
onTap: () => launchUrl(Uri.parse(page.license.url),
|
||||
mode: LaunchMode.externalApplication),
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
Flexible(
|
||||
child: SelectableText(
|
||||
page.license.title,
|
||||
textAlign: TextAlign.center,
|
||||
onTap: () => launchUrl(Uri.parse(page.license.url),
|
||||
mode: LaunchMode.externalApplication),
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
|
@ -103,7 +107,7 @@ class PageRenderer {
|
|||
var sections = document.body!.getElementsByTagName("section");
|
||||
for (var sec in sections) {
|
||||
if (sec.localName == "section") {
|
||||
if (!offline) out.addAll(_renderSection(sec));
|
||||
if (!offline) out.addAll(await _renderSection(sec));
|
||||
outHtml += sec.outerHtml;
|
||||
}
|
||||
}
|
||||
|
@ -115,7 +119,7 @@ class PageRenderer {
|
|||
return l;
|
||||
}
|
||||
|
||||
List<Widget> _renderSection(dom.Element sec) {
|
||||
Future<List<Widget>> _renderSection(dom.Element sec) async {
|
||||
var out = <Widget>[];
|
||||
// Get Section Title
|
||||
var headings = sec.children.where(
|
||||
|
@ -213,7 +217,7 @@ class PageRenderer {
|
|||
); // space paragraphs
|
||||
break;
|
||||
case "figure":
|
||||
out.addAll(_renderImageFigure(element));
|
||||
out.addAll(await _renderImageFigure(element));
|
||||
out.add(
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
|
@ -221,7 +225,7 @@ class PageRenderer {
|
|||
);
|
||||
break;
|
||||
case "section":
|
||||
out.addAll(_renderSection(element));
|
||||
out.addAll(await _renderSection(element));
|
||||
break;
|
||||
case "dl":
|
||||
var dd = element.getElementsByTagName("dd").first;
|
||||
|
@ -285,7 +289,7 @@ class PageRenderer {
|
|||
for (var e in inner.body!.children) {
|
||||
if (e.localName == "figure") {
|
||||
// render image
|
||||
out.addAll(_renderImageFigure(e));
|
||||
out.addAll(await _renderImageFigure(e));
|
||||
out.add(
|
||||
const SizedBox(
|
||||
height: 5,
|
||||
|
@ -352,13 +356,24 @@ class PageRenderer {
|
|||
out.add(const SizedBox(
|
||||
height: 10,
|
||||
));
|
||||
var offlineImage = await StorageAccess.getOfflineImage(img
|
||||
.attributes["src"]!
|
||||
.split('/')
|
||||
.last
|
||||
.replaceAll(RegExp(r"(?!\..+?)\?.+"), ""));
|
||||
out.add(
|
||||
SizedBox(
|
||||
width: width * 0.8,
|
||||
height: height * 0.3,
|
||||
child: WidgetZoom(
|
||||
zoomWidget:
|
||||
CachedNetworkImage(imageUrl: img.attributes["src"]!),
|
||||
zoomWidget: CachedNetworkImage(
|
||||
imageUrl: img.attributes["src"]!,
|
||||
errorWidget: (context, url, error) => (offlineImage != null)
|
||||
? Image.file(offlineImage)
|
||||
: Flexible(
|
||||
child: Text(loc.imageError),
|
||||
),
|
||||
),
|
||||
heroAnimationTag: 'tag',
|
||||
),
|
||||
),
|
||||
|
@ -396,7 +411,7 @@ class PageRenderer {
|
|||
return out;
|
||||
}
|
||||
|
||||
List<Widget> _renderImageFigure(dom.Element element) {
|
||||
Future<List<Widget>> _renderImageFigure(dom.Element element) async {
|
||||
var out = <Widget>[];
|
||||
|
||||
/// Image figure
|
||||
|
@ -409,6 +424,12 @@ class PageRenderer {
|
|||
if (figcap.isNotEmpty) {
|
||||
caption = figcap.first.text; // TODO: handle links
|
||||
}
|
||||
var offlineImage = await StorageAccess.getOfflineImage(img
|
||||
.attributes["src"]!
|
||||
.split('/')
|
||||
.last
|
||||
.replaceAll(RegExp(r"(?!\..+?)\?.+"), ""));
|
||||
|
||||
out.add(const SizedBox(
|
||||
height: 10,
|
||||
));
|
||||
|
@ -423,10 +444,11 @@ class PageRenderer {
|
|||
imageUrl: img.attributes["src"]!.replaceAll("//", "https://"),
|
||||
progressIndicatorBuilder: (context, url, downloadProgress) =>
|
||||
LinearProgressIndicator(value: downloadProgress.progress),
|
||||
errorWidget: (context, url, error) => Icon(
|
||||
Icons.error,
|
||||
color: scheme.error,
|
||||
),
|
||||
errorWidget: (context, url, error) => (offlineImage != null)
|
||||
? Image.file(offlineImage)
|
||||
: Flexible(
|
||||
child: Text(loc.imageError),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -501,7 +523,9 @@ class PageRenderer {
|
|||
content.add(
|
||||
WidgetSpan(
|
||||
child: SelectableText(
|
||||
match.group(2)!,
|
||||
match.group(2)!.replaceAll(
|
||||
RegExp(r'<(?:(?:\/b)|(?:\/i)|(?:\/a)|(?:b)|(?:i)|(?:a )).*?>'),
|
||||
""),
|
||||
onTap: () {
|
||||
if (match.group(0)!.contains('rel="mw:ExtLink')) {
|
||||
// handle as an external link
|
||||
|
@ -547,8 +571,9 @@ class PageRenderer {
|
|||
.toList();
|
||||
for (var s in needToFormat) {
|
||||
content.add(TextSpan(
|
||||
text:
|
||||
noFormatting[needToFormat.indexOf(s)])); // add text before styled
|
||||
text: noFormatting[needToFormat.indexOf(s)].replaceAll(
|
||||
RegExp(r'<(?:(?:\/b)|(?:\/i)|(?:\/a)|(?:b)|(?:i)|(?:a )).*?>'),
|
||||
""))); // add text before styled
|
||||
var raw = s.group(0)!;
|
||||
content.add(
|
||||
TextSpan(
|
||||
|
|
|
@ -2,8 +2,10 @@ import 'dart:convert';
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:voyagehandbook/api/classes.dart';
|
||||
import 'package:voyagehandbook/api/wikimedia.dart';
|
||||
|
||||
/*
|
||||
|
@ -89,26 +91,36 @@ class StorageAccess {
|
|||
|
||||
static Future<bool> isDownloaded(String pageKey) async {
|
||||
var files =
|
||||
Directory("${(await getApplicationDocumentsDirectory()).path}/recent");
|
||||
Directory("${(await getApplicationDocumentsDirectory()).path}/offline");
|
||||
if (!files.existsSync()) files.createSync();
|
||||
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);
|
||||
try {
|
||||
var files = Directory(
|
||||
"${(await getApplicationDocumentsDirectory()).path}/offline");
|
||||
if (!files.existsSync()) files.createSync();
|
||||
var offlinePage = File("${files.path}/$pageKey");
|
||||
var raw = await WikiApi.getRawPage(pageKey);
|
||||
var page = parse(raw.html);
|
||||
var out = "<html><head></head><body>";
|
||||
var sections = page.body!.children
|
||||
.where((element) => element.localName == "section");
|
||||
for (var el in sections) {
|
||||
out += el.outerHtml;
|
||||
}
|
||||
out += "</body></html>";
|
||||
var imgMatch = RegExp(r'<img.+?src="(.+?)".+?>')
|
||||
.allMatches(page.body!.innerHtml); // TODO: ask to overwrite
|
||||
if (imgMatch.isNotEmpty) {
|
||||
// download images offline
|
||||
for (var match in imgMatch) {
|
||||
var src = match.group(1)!;
|
||||
if (!src.startsWith("https://")) {
|
||||
src = src.replaceAll("//", "https://");
|
||||
}
|
||||
var r = await Dio().get(
|
||||
src,
|
||||
options: Options(
|
||||
|
@ -119,16 +131,48 @@ class StorageAccess {
|
|||
},
|
||||
),
|
||||
);
|
||||
var img = File("${files.path}/${src.split('/').last}");
|
||||
var assetDir = Directory("${files.path}/assets");
|
||||
if (!assetDir.existsSync()) assetDir.createSync();
|
||||
var img = File(
|
||||
"${assetDir.path}/${src.split('/').last.replaceAll(RegExp(r"(?!\..+?)\?.+"), "")}");
|
||||
print(img.path);
|
||||
var openImg = img.openSync(mode: FileMode.write);
|
||||
openImg.writeFromSync(r.data);
|
||||
}
|
||||
}
|
||||
raw.html = out;
|
||||
if (sections.isEmpty) {
|
||||
return Future.error("No sections to save");
|
||||
}
|
||||
offlinePage.writeAsStringSync(jsonEncode(raw.toJson()),
|
||||
mode: FileMode.writeOnly);
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return Future.error(e);
|
||||
}
|
||||
if (page.isEmpty) return Future.error("No sections to save");
|
||||
offlinePage.writeAsStringSync(
|
||||
jsonEncode({"title": pageTitle, "key": pageKey, "content": out}),
|
||||
mode: FileMode.writeOnly);
|
||||
}
|
||||
|
||||
static Future<RawPage?> getOfflinePage(String pageKey) async {
|
||||
var files =
|
||||
Directory("${(await getApplicationDocumentsDirectory()).path}/offline");
|
||||
if (!files.existsSync()) return null;
|
||||
var offlinePage = File("${files.path}/$pageKey");
|
||||
if (!offlinePage.existsSync()) return null;
|
||||
try {
|
||||
return RawPage.fromJson(jsonDecode(offlinePage.readAsStringSync()));
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<File?> getOfflineImage(String key) async {
|
||||
var files = Directory(
|
||||
"${(await getApplicationDocumentsDirectory()).path}/offline/assets");
|
||||
return File("${files.path}/$key");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:voyagehandbook/util/drawer.dart';
|
||||
import 'package:voyagehandbook/util/storage.dart';
|
||||
import 'package:voyagehandbook/util/styles.dart';
|
||||
import 'package:voyagehandbook/views/pageview.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
@ -29,16 +30,21 @@ class _DownloadsViewState extends State<DownloadsView> {
|
|||
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(
|
||||
body: Center(
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(context).size.height,
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: _isLoading
|
||||
? [const CircularProgressIndicator()]
|
||||
: _content.isEmpty
|
||||
? [Text(AppLocalizations.of(context)!.noDownloads)]
|
||||
? [
|
||||
Text(
|
||||
AppLocalizations.of(context)!.noDownloads,
|
||||
textAlign: TextAlign.center,
|
||||
)
|
||||
]
|
||||
: _content,
|
||||
),
|
||||
),
|
||||
|
@ -52,7 +58,7 @@ class _DownloadsViewState extends State<DownloadsView> {
|
|||
files.length,
|
||||
(index) => SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
height: MediaQuery.of(context).size.height * 0.15,
|
||||
height: MediaQuery.of(context).size.height * 0.1,
|
||||
child: InkWell(
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
|
@ -62,9 +68,13 @@ class _DownloadsViewState extends State<DownloadsView> {
|
|||
),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(files[index]["title"]),
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
files[index]["title"],
|
||||
textAlign: TextAlign.center,
|
||||
style: PageStyles.h1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:voyagehandbook/api/wikimedia.dart';
|
||||
|
@ -78,32 +79,52 @@ class _ArticleViewState extends State<ArticleView> {
|
|||
}
|
||||
|
||||
void loadPage() async {
|
||||
if (!mounted) return;
|
||||
var renderer = PageRenderer(
|
||||
Theme.of(context).colorScheme,
|
||||
MediaQuery.of(context).size.height,
|
||||
MediaQuery.of(context).size.width,
|
||||
context,
|
||||
AppLocalizations.of(context)!);
|
||||
if (kDebugMode) {
|
||||
|
||||
try {
|
||||
_content = [
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
height: MediaQuery.of(context).size.height,
|
||||
child: renderer
|
||||
child: await renderer
|
||||
.renderFromPageHTML(await WikiApi.getRawPage(widget.pageKey)),
|
||||
)
|
||||
];
|
||||
} else {
|
||||
try {
|
||||
_content = [
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
height: MediaQuery.of(context).size.height,
|
||||
child: renderer
|
||||
.renderFromPageHTML(await WikiApi.getRawPage(widget.pageKey)),
|
||||
)
|
||||
];
|
||||
} catch (e) {
|
||||
} catch (e) {
|
||||
if (e.toString().contains("Failed host lookup")) {
|
||||
// user is offline
|
||||
var offline = await StorageAccess.getOfflinePage(widget.pageKey);
|
||||
if (offline == null) {
|
||||
// Not downloaded, show error
|
||||
if (!mounted) return;
|
||||
_content = [
|
||||
Text(
|
||||
AppLocalizations.of(context)!.renderError,
|
||||
style: PageStyles.h1,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
Text(AppLocalizations.of(context)!.offlineError)
|
||||
];
|
||||
} else {
|
||||
// Render offline version
|
||||
if (!mounted) return;
|
||||
_content = [
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
height: MediaQuery.of(context).size.height,
|
||||
child: await renderer.renderFromPageHTML(offline),
|
||||
)
|
||||
];
|
||||
}
|
||||
} else {
|
||||
_content = [
|
||||
Text(
|
||||
AppLocalizations.of(context)!.renderError,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:voyagehandbook/api/classes.dart';
|
||||
import 'package:voyagehandbook/api/wikimedia.dart';
|
||||
import 'package:voyagehandbook/util/storage.dart';
|
||||
import 'package:voyagehandbook/views/pageview.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
@ -145,6 +146,109 @@ class _SearchViewState extends State<SearchView> {
|
|||
_searchResults[index]
|
||||
.title),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => Dialog(
|
||||
child: SizedBox(
|
||||
height: 100,
|
||||
child: Row(
|
||||
children: [
|
||||
const Padding(
|
||||
padding:
|
||||
EdgeInsets
|
||||
.all(
|
||||
10),
|
||||
child:
|
||||
CircularProgressIndicator(),
|
||||
),
|
||||
Text(AppLocalizations
|
||||
.of(context)!
|
||||
.downloading)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
try {
|
||||
await StorageAccess
|
||||
.downloadArticle(
|
||||
_searchResults[
|
||||
index]
|
||||
.key,
|
||||
_searchResults[
|
||||
index]
|
||||
.title);
|
||||
if (!mounted) return;
|
||||
Navigator.of(context)
|
||||
.pop();
|
||||
ScaffoldMessenger.of(
|
||||
context)
|
||||
.clearSnackBars();
|
||||
ScaffoldMessenger.of(
|
||||
context)
|
||||
.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
AppLocalizations.of(
|
||||
context)!
|
||||
.downloadComplete),
|
||||
duration:
|
||||
const Duration(
|
||||
seconds: 4),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
Navigator.of(context)
|
||||
.pop();
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) =>
|
||||
AlertDialog(
|
||||
title: Text(
|
||||
AppLocalizations.of(
|
||||
context)!
|
||||
.error),
|
||||
content:
|
||||
SingleChildScrollView(
|
||||
child: Text(
|
||||
e.toString()),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () =>
|
||||
Navigator.of(
|
||||
context)
|
||||
.pop(),
|
||||
child: Text(
|
||||
AppLocalizations.of(
|
||||
context)!
|
||||
.ok),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
AppLocalizations.of(
|
||||
context)!
|
||||
.yes),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () =>
|
||||
Navigator.of(context)
|
||||
.pop(),
|
||||
child: Text(
|
||||
AppLocalizations.of(
|
||||
context)!
|
||||
.no),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -164,13 +268,27 @@ class _SearchViewState extends State<SearchView> {
|
|||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
_searchResults[index].title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize:
|
||||
16), // TODO: responsive sizing
|
||||
),
|
||||
Row(children: [
|
||||
Text(
|
||||
_searchResults[index].title,
|
||||
style: const TextStyle(
|
||||
fontWeight:
|
||||
FontWeight.bold,
|
||||
fontSize:
|
||||
16), // TODO: responsive sizing
|
||||
),
|
||||
if (_searchResults[index]
|
||||
.downloaded)
|
||||
const SizedBox(
|
||||
width: 15,
|
||||
),
|
||||
if (_searchResults[index]
|
||||
.downloaded)
|
||||
const Icon(
|
||||
Icons.download,
|
||||
size: 15,
|
||||
),
|
||||
]),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
|
|
Loading…
Reference in a new issue