Skip to content

Commit

Permalink
Download PDF files; implement opening and sharing
Browse files Browse the repository at this point in the history
When downloading a language, download the PDF files (available in the
newly created PDF repositories for each language).
Implement opening and sharing of these PDF files in our share menu.
This should be fully functional but tests are still missing
  • Loading branch information
holybiber committed Jul 19, 2024
1 parent b6e6cd8 commit 0246e73
Show file tree
Hide file tree
Showing 13 changed files with 162 additions and 52 deletions.
15 changes: 14 additions & 1 deletion lib/data/globals.dart
Original file line number Diff line number Diff line change
Expand Up @@ -145,20 +145,33 @@ class Globals {
static const String githubUser = '4training';
static const String branch = 'main';
static const String htmlPath = 'html';
static const String pdfPath = 'pdf';
static const String remoteZipUrl = '/archive/refs/heads/$branch.zip';

/// Url of the zip file for the HTML resources of a language
static String getRemoteUrl(String languageCode) {
static String getRemoteUrlHtml(String languageCode) {
return 'https://github.com/$githubUser/$htmlPath-$languageCode$remoteZipUrl';
}

/// Url of the zip file for the HTML resources of a language
static String getRemoteUrlPdf(String languageCode) {
return 'https://github.com/$githubUser/$pdfPath-$languageCode$remoteZipUrl';
}

/// Folder name of the resources of a language. Example: html-en-main
///
/// Must be the main folder name that is inside the zip file we download.
static String getResourcesDir(String languageCode) {
return '$htmlPath-$languageCode-$branch';
}

/// Folder name of the PDF files of a language. Example: pdf-en-main
///
/// Must be the main folder name that is inside the zip file we download.
static String getPdfDir(String languageCode) {
return '$pdfPath-$languageCode-$branch';
}

/// Folder name of the assets dir of a language
static String getAssetsDir(String languageCode) {
return 'assets-$languageCode';
Expand Down
77 changes: 48 additions & 29 deletions lib/data/languages.dart
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ class LanguageController extends FamilyNotifier<Language, String> {
Future<bool> lazyInit() async {
await _initController();
String path =
'${_controller.assetsDir}/${Globals.getResourcesDir(languageCode)}';
join(_controller.assetsDir!, Globals.getResourcesDir(languageCode));
final stat = await ref
.watch(fileSystemProvider)
.stat(join(path, 'structure', 'contents.json'));
Expand All @@ -182,16 +182,16 @@ class LanguageController extends FamilyNotifier<Language, String> {
try {
// Now we store the full path to the language
String path =
'${_controller.assetsDir}/${Globals.getResourcesDir(languageCode)}';
join(_controller.assetsDir!, Globals.getResourcesDir(languageCode));
debugPrint("Path: $path");
Directory dir = fileSystem.directory(path);

bool downloaded = await _controller.assetsDirAlreadyExists();
debugPrint("Trying to load '$languageCode', downloaded: $downloaded");
if (!downloaded) return false;

// Store the size of the downloaded directory
int sizeInKB = await _calculateMemoryUsage(dir);
// Store the size of the downloaded files (HTML + PDF)
int sizeInKB = await _calculateMemoryUsage(
fileSystem.directory(_controller.assetsDir!));

// Get the timestamp: When were our contents stored on the device?
FileStat stat =
Expand All @@ -207,22 +207,49 @@ class LanguageController extends FamilyNotifier<Language, String> {
final Map<String, Page> pages = {};
final List<String> pageIndex = [];
final Map<String, Image> images = {};
final Set<String> pdfFiles = {};

// Go through existing PDF files
var pdfPath =
join(_controller.assetsDir!, Globals.getPdfDir(languageCode));
var pdfDir = fileSystem.directory(pdfPath);
if (await pdfDir.exists()) {
await for (var file
in pdfDir.list(recursive: false, followLinks: false)) {
if (file is File) {
pdfFiles.add(file.basename);
} else {
debugPrint("Found unexpected element $file in the PDF directory");
}
}
}

// Store everything in our data structures
for (var element in structure["worksheets"]) {
// TODO add error handling
pageIndex.add(element['page']);
String? pdfName; // Stores PDF file name if it is available
if (element.containsKey('pdf') && pdfFiles.contains(element['pdf'])) {
pdfName = join(pdfPath, element['pdf']);
pdfFiles.remove(element['pdf']);
}
pages[element['page']] = Page(element['page'], element['title'],
element['filename'], element['version']);
element['filename'], element['version'], pdfName);
}

// Consistency checking...
if (pdfFiles.isNotEmpty) {
debugPrint("Found unexpected PDF file(s): $pdfFiles");
}
await _checkConsistency(dir, pages);
await _checkConsistency(fileSystem.directory(path), pages);

// Register available images
var filesDir = fileSystem.directory(join(path, 'files'));
if (await filesDir.exists()) {
await for (var file
in filesDir.list(recursive: false, followLinks: false)) {
if (file is File) {
images[basename(file.path)] = Image(basename(file.path));
images[file.basename] = Image(file.basename);
} else {
debugPrint("Found unexpected element $file in files/ directory");
}
Expand Down Expand Up @@ -251,36 +278,25 @@ class LanguageController extends FamilyNotifier<Language, String> {
}

/// Download all files for one language via DownloadAssetsController
/// Returns whether we were successful and shouldn't throw
/// Returns whether we were successful. Shouldn't throw
Future<bool> _download() async {
await _initController();
debugPrint("Starting to download language '$languageCode' ...");
// URL of the zip file to be downloaded
String remoteUrl = Globals.getRemoteUrl(languageCode);

try {
await _controller.startDownload(
assetsUrls: [remoteUrl],
onProgress: (progressValue) {
if (progressValue < 20) {
// The value goes for some reason only up to 18.7 or so ...
String progress = "Downloading $languageCode: ";

for (int i = 0; i < 20; i++) {
progress += (i <= progressValue) ? "|" : ".";
}
debugPrint("$progress ${progressValue.round()}");
} else {
debugPrint("Download completed");
}
},
);
// assetUrls takes an array, but we can't specify both URLs in one call:
// DownloadAssets throws when both files have the same name (main.zip) :-/
await _controller
.startDownload(assetsUrls: [Globals.getRemoteUrlHtml(languageCode)]);
await _controller
.startDownload(assetsUrls: [Globals.getRemoteUrlPdf(languageCode)]);
} catch (e) {
debugPrint("Error while downloading language '$languageCode': $e");
// delete the empty folder left behind by startDownload()
await _controller.clearAssets();
return false;
}
debugPrint("Downloading language '$languageCode' finished.");
return true;
}

Expand All @@ -303,7 +319,7 @@ class LanguageController extends FamilyNotifier<Language, String> {
Set<String> files = {};
await for (var file in dir.list(recursive: false, followLinks: false)) {
if (file is File) {
files.add(basename(file.path));
files.add(file.basename);
}
}
pages.forEach((key, page) {
Expand Down Expand Up @@ -331,7 +347,10 @@ class Page {

final String version;

const Page(this.name, this.title, this.fileName, this.version);
/// Full path of the associated PDF file if it exists on the device
final String? pdfName;

const Page(this.name, this.title, this.fileName, this.version, this.pdfName);
}

/// Holds properties of an image.
Expand Down
3 changes: 3 additions & 0 deletions lib/design/theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,6 @@ ThemeData lightTheme = _defaultLightTheme.copyWith(
ThemeData darkTheme =
FlexThemeData.dark(scheme: FlexScheme.red, useMaterial3: true)
.copyWith(appBarTheme: darkAppBarTheme);

/// Size of smileys (used on "sorry, not yet available" dialogs)
const double smileySize = 50;
3 changes: 2 additions & 1 deletion lib/l10n/locales/app_de.arb
Original file line number Diff line number Diff line change
Expand Up @@ -338,5 +338,6 @@
"sharePdf": "PDF teilen",
"openPdf": "PDF öffnen",
"openInBrowser": "Im Browser öffnen",
"shareLink": "Link teilen"
"shareLink": "Link teilen",
"pdfNotAvailable": "Für dieses Arbeitsblatt ist leider noch kein PDF verfügbar. Wenn du mithelfen möchtest, damit sich das bald ändert, dann melde dich bitte!"
}
3 changes: 2 additions & 1 deletion lib/l10n/locales/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -338,5 +338,6 @@
"sharePdf": "Share PDF",
"openPdf": "Open PDF",
"openInBrowser": "Open in browser",
"shareLink": "Share link"
"shareLink": "Share link",
"pdfNotAvailable": "Unfortunately there is no PDF available yet for this worksheet. If you want to help make this change soon, please contact us!"
}
3 changes: 1 addition & 2 deletions lib/widgets/main_drawer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@ import 'package:app4training/data/app_language.dart';
import 'package:app4training/data/categories.dart';
import 'package:app4training/data/globals.dart';
import 'package:app4training/data/languages.dart';
import 'package:app4training/design/theme.dart';
import 'package:app4training/l10n/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

const double smileySize = 50;

/// Our main menu with the list of pages, organized into categories.
/// The currently shown page is highlighted and the category it belongs to
/// is expanded.
Expand Down
87 changes: 74 additions & 13 deletions lib/widgets/share_button.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import 'dart:async';

import 'package:app4training/data/languages.dart';
import 'package:app4training/design/theme.dart';
import 'package:app4training/l10n/l10n.dart';
import 'package:app4training/routes/view_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:open_filex/open_filex.dart';
import 'package:share_plus/share_plus.dart';
import 'package:url_launcher/url_launcher.dart';

Expand All @@ -20,6 +23,12 @@ class ShareButton extends ConsumerWidget {
final viewPage = context.findAncestorWidgetOfExactType<ViewPage>()!;
String currentPage = viewPage.page;
String currentLang = viewPage.langCode;
String? pdfFile =
ref.watch(languageProvider(currentLang)).pages[currentPage]?.pdfName;
// Color for the PDF-related entries (greyed out if PDF is not available)
Color pdfColor = (pdfFile != null)
? Theme.of(context).colorScheme.onSurface
: Theme.of(context).disabledColor;

return MenuAnchor(
controller: menuController,
Expand All @@ -44,21 +53,48 @@ class ShareButton extends ConsumerWidget {
children: [
Column(children: [
ListTile(
dense: true,
title: Text(context.l10n.openPdf),
leading: const ImageIcon(
AssetImage("assets/file-document-outline.png")),
onTap: () {
menuController.close();
},
),
dense: true,
title: Text(context.l10n.openPdf,
style: TextStyle(color: pdfColor)),
leading: ImageIcon(
const AssetImage(
"assets/file-document-outline.png"),
color: pdfColor),
onTap: () async {
if (pdfFile != null) {
menuController.close();
var result = await OpenFilex.open(pdfFile);
debugPrint(
'OpenResult: ${result.message}; ${result.type}');
} else {
await showDialog(
context: context,
builder: (context) {
return const PdfNotAvailableDialog();
});
menuController.close();
}
}),
ListTile(
dense: true,
title: Text(context.l10n.sharePdf),
leading: const ImageIcon(AssetImage(
"assets/file-document-arrow-right-outline.png")),
onTap: () {
menuController.close();
title: Text(context.l10n.sharePdf,
style: TextStyle(color: pdfColor)),
leading: ImageIcon(
const AssetImage(
"assets/file-document-arrow-right-outline.png"),
color: pdfColor),
onTap: () async {
if (pdfFile != null) {
menuController.close();
await Share.shareXFiles([XFile(pdfFile)]);
} else {
await showDialog(
context: context,
builder: (context) {
return const PdfNotAvailableDialog();
});
menuController.close();
}
},
),
ListTile(
Expand Down Expand Up @@ -86,3 +122,28 @@ class ShareButton extends ConsumerWidget {
]);
}
}

class PdfNotAvailableDialog extends StatelessWidget {
const PdfNotAvailableDialog({super.key});

@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(context.l10n.sorry),
content: Column(mainAxisSize: MainAxisSize.min, children: [
Icon(Icons.sentiment_dissatisfied,
size: smileySize,
// For unknown reasons smiley is invisible otherwise
color: Theme.of(context).colorScheme.onSurface),
const SizedBox(height: 10),
Text(context.l10n.pdfNotAvailable),
]),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(context.l10n.okay))
]);
}
}
8 changes: 8 additions & 0 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.2"
open_filex:
dependency: "direct main"
description:
name: open_filex
sha256: "74e2280754cf8161e860746c3181db2c996d6c1909c7057b738ede4a469816b8"
url: "https://pub.dev"
source: hosted
version: "4.4.0"
package_config:
dependency: transitive
description:
Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ dependencies:
package_info_plus: ^5.0.1
workmanager: ^0.5.2
share_plus: ^7.2.2
open_filex: ^4.4.0


dev_dependencies:
Expand Down
6 changes: 5 additions & 1 deletion test/globals_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ import 'package:flutter_test/flutter_test.dart';
void main() {
test('Test remote URLs and local path', () {
expect(
Globals.getRemoteUrl('de'),
Globals.getRemoteUrlHtml('de'),
equals(
'https://github.com/4training/html-de/archive/refs/heads/main.zip'));
expect(
Globals.getRemoteUrlPdf('de'),
equals(
'https://github.com/4training/pdf-de/archive/refs/heads/main.zip'));
expect(Globals.getAssetsDir('de'), equals('assets-de'));
expect(Globals.getResourcesDir('de'), equals('html-de-main'));
expect(
Expand Down
4 changes: 2 additions & 2 deletions test/language_selection_button_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ void main() {
appLanguageProvider.overrideWith(() => TestAppLanguage('en')),
languageProvider.overrideWith(() => TestLanguageController(
downloadedLanguages: ['de'],
pages: {'Healing': const Page('test', 'test', 'test', '1.0')}))
pages: {'Healing': const Page('test', 'test', 'test', '1.0', null)}))
], child: const TestLanguagesButton()));

expect(find.byIcon(Icons.translate), findsOneWidget);
Expand All @@ -57,7 +57,7 @@ void main() {
appLanguageProvider.overrideWith(() => TestAppLanguage('de')),
languageProvider.overrideWith(() => TestLanguageController(
downloadedLanguages: ['de', 'en', 'fr', 'es', 'ar'],
pages: {'Healing': const Page('test', 'test', 'test', '1.0')}))
pages: {'Healing': const Page('test', 'test', 'test', '1.0', null)}))
], child: const TestLanguagesButton()));

expect(find.byIcon(Icons.translate), findsOneWidget);
Expand Down
Loading

0 comments on commit 0246e73

Please sign in to comment.