2023-03-27 19:45:57 +02:00
|
|
|
import 'dart:convert';
|
2023-03-18 16:55:08 +01:00
|
|
|
import 'dart:io';
|
|
|
|
|
2023-04-06 22:46:40 +02:00
|
|
|
import 'package:dio/dio.dart';
|
2023-04-07 13:02:40 +02:00
|
|
|
import 'package:flutter/foundation.dart';
|
2023-04-06 22:46:40 +02:00
|
|
|
import 'package:html/parser.dart';
|
2023-03-18 16:55:08 +01:00
|
|
|
import 'package:path_provider/path_provider.dart';
|
2023-04-07 13:02:40 +02:00
|
|
|
import 'package:voyagehandbook/api/classes.dart';
|
2023-04-06 22:46:40 +02:00
|
|
|
import 'package:voyagehandbook/api/wikimedia.dart';
|
2023-03-18 16:55:08 +01:00
|
|
|
|
2023-03-28 18:08:26 +02:00
|
|
|
/*
|
|
|
|
Voyage Handbook - The open-source WikiVoyage reader
|
|
|
|
Copyright (C) 2023 Matyáš Caras
|
|
|
|
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
2023-03-28 20:10:46 +02:00
|
|
|
it under the terms of the GNU General Public License version 3 as published by
|
|
|
|
the Free Software Foundation.
|
2023-03-28 18:08:26 +02:00
|
|
|
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
2023-03-18 16:55:08 +01:00
|
|
|
/// Used to ease up accessing local files
|
|
|
|
class StorageAccess {
|
|
|
|
/// Get files in `recent` folder, which contains recently opened pages
|
2023-03-27 19:45:57 +02:00
|
|
|
static Future<List<Map<String, dynamic>>> get recent async {
|
2023-03-18 16:55:08 +01:00
|
|
|
var files =
|
|
|
|
Directory("${(await getApplicationDocumentsDirectory()).path}/recent");
|
|
|
|
if (!files.existsSync()) files.createSync();
|
2023-03-27 19:45:57 +02:00
|
|
|
return files
|
|
|
|
.listSync()
|
|
|
|
.whereType<File>()
|
|
|
|
.toList()
|
|
|
|
.map<Map<String, dynamic>>((e) => jsonDecode(e.readAsStringSync()))
|
|
|
|
.toList();
|
|
|
|
}
|
|
|
|
|
2023-04-06 22:46:40 +02:00
|
|
|
/// 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();
|
|
|
|
}
|
|
|
|
|
2023-03-27 19:45:57 +02:00
|
|
|
static void addToRecents(String pageName, String pageKey) async {
|
|
|
|
var files =
|
|
|
|
Directory("${(await getApplicationDocumentsDirectory()).path}/recent");
|
|
|
|
if (!files.existsSync()) files.createSync();
|
|
|
|
var content = files.listSync();
|
|
|
|
if (content.length > 4) {
|
|
|
|
// delete last recent
|
|
|
|
// TODO: configurable
|
|
|
|
File? f;
|
|
|
|
for (var file in content) {
|
|
|
|
if (file is Directory) continue;
|
|
|
|
var modi = (await file.stat()).modified;
|
|
|
|
if (f == null || (await f.stat()).modified.isAfter(modi)) {
|
|
|
|
f = file as File;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
f!.deleteSync();
|
|
|
|
}
|
|
|
|
|
|
|
|
var recent = File("${files.path}/${pageName.replaceAll(' ', '_')}");
|
|
|
|
if (recent.existsSync()) {
|
|
|
|
// if recent already exists, simply change date
|
|
|
|
var recentContent = jsonDecode(recent.readAsStringSync());
|
|
|
|
recentContent["date"] = DateTime.now().millisecondsSinceEpoch;
|
|
|
|
recent.writeAsStringSync(jsonEncode(recentContent));
|
|
|
|
} else {
|
|
|
|
// else create files
|
|
|
|
var recentContent = {
|
|
|
|
"date": DateTime.now().millisecondsSinceEpoch,
|
|
|
|
"name": pageName,
|
|
|
|
"key": pageKey
|
|
|
|
};
|
|
|
|
recent.writeAsStringSync(jsonEncode(recentContent));
|
|
|
|
}
|
2023-03-18 16:55:08 +01:00
|
|
|
}
|
2023-04-06 22:46:40 +02:00
|
|
|
|
|
|
|
static Future<bool> isDownloaded(String pageKey) async {
|
|
|
|
var files =
|
2023-04-07 13:02:40 +02:00
|
|
|
Directory("${(await getApplicationDocumentsDirectory()).path}/offline");
|
|
|
|
if (!files.existsSync()) files.createSync();
|
2023-04-06 22:46:40 +02:00
|
|
|
var offlinePage = File("${files.path}/$pageKey");
|
|
|
|
return offlinePage.existsSync();
|
|
|
|
}
|
|
|
|
|
|
|
|
static Future<void> downloadArticle(String pageKey, String pageTitle) async {
|
2023-04-07 13:02:40 +02:00
|
|
|
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
|
2023-04-06 22:46:40 +02:00
|
|
|
if (imgMatch.isNotEmpty) {
|
|
|
|
// download images offline
|
|
|
|
for (var match in imgMatch) {
|
|
|
|
var src = match.group(1)!;
|
2023-04-07 13:02:40 +02:00
|
|
|
if (!src.startsWith("https://")) {
|
|
|
|
src = src.replaceAll("//", "https://");
|
|
|
|
}
|
2023-04-06 22:46:40 +02:00
|
|
|
var r = await Dio().get(
|
|
|
|
src,
|
|
|
|
options: Options(
|
|
|
|
responseType: ResponseType.bytes,
|
|
|
|
followRedirects: false,
|
|
|
|
validateStatus: (status) {
|
|
|
|
return (status ?? 200) < 500;
|
|
|
|
},
|
|
|
|
),
|
|
|
|
);
|
2023-04-07 13:02:40 +02:00
|
|
|
var assetDir = Directory("${files.path}/assets");
|
|
|
|
if (!assetDir.existsSync()) assetDir.createSync();
|
|
|
|
var img = File(
|
|
|
|
"${assetDir.path}/${src.split('/').last.replaceAll(RegExp(r"(?!\..+?)\?.+"), "")}");
|
2023-04-06 22:46:40 +02:00
|
|
|
print(img.path);
|
|
|
|
var openImg = img.openSync(mode: FileMode.write);
|
|
|
|
openImg.writeFromSync(r.data);
|
|
|
|
}
|
|
|
|
}
|
2023-04-07 13:02:40 +02:00
|
|
|
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);
|
2023-04-06 22:46:40 +02:00
|
|
|
}
|
2023-04-07 13:02:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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");
|
2023-04-06 22:46:40 +02:00
|
|
|
}
|
2023-03-18 16:55:08 +01:00
|
|
|
}
|