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'; /* Voyage Handbook - The open-source WikiVoyage reader Copyright (C) 2023 Matyáš Caras This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 3 as published by the Free Software Foundation. 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 . */ /// Used to ease up accessing local files class StorageAccess { /// Get files in `recent` folder, which contains recently opened pages static Future>> get recent async { var files = Directory("${(await getApplicationDocumentsDirectory()).path}/recent"); if (!files.existsSync()) files.createSync(); return files .listSync() .whereType() .toList() .map>((e) => jsonDecode(e.readAsStringSync())) .toList(); } /// Get files in `offline` folder, which contains recently opened pages static Future>> get offline async { var files = Directory("${(await getApplicationDocumentsDirectory()).path}/offline"); if (!files.existsSync()) files.createSync(); return files .listSync() .whereType() .toList() .map>((e) => jsonDecode(e.readAsStringSync())) .toList(); } 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)); } } static Future isDownloaded(String pageKey) async { var files = Directory("${(await getApplicationDocumentsDirectory()).path}/offline"); if (!files.existsSync()) files.createSync(); var offlinePage = File("${files.path}/$pageKey"); return offlinePage.existsSync(); } static Future downloadArticle(String pageKey, String pageTitle) async { 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 = ""; var sections = page.body!.children .where((element) => element.localName == "section"); for (var el in sections) { out += el.outerHtml; } out += ""; var imgMatch = RegExp(r'') .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( responseType: ResponseType.bytes, followRedirects: false, validateStatus: (status) { return (status ?? 200) < 500; }, ), ); 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); } } static Future 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 getOfflineImage(String key) async { var files = Directory( "${(await getApplicationDocumentsDirectory()).path}/offline/assets"); return File("${files.path}/$key"); } }