Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 16 additions & 18 deletions app/lib/pages/reader_page.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import 'dart:typed_data';

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:papyrus/data/data_store.dart';
Expand All @@ -26,8 +24,7 @@ class ReaderPage extends StatefulWidget {

class _ReaderPageState extends State<ReaderPage> {
Book? _book;
Uint8List? _bytes;
ReaderFormat? _format;
ReaderDocument? _document;
ReaderLocator? _initialLocator;
ReaderPreferences? _initialPreferences;
ReaderSession? _session;
Expand Down Expand Up @@ -86,9 +83,14 @@ class _ReaderPageState extends State<ReaderPage> {

setState(() {
_book = book;
_bytes = bytes;
_format = format;
_initialLocator = ReaderBookAdapter.restoreLocator(book!);
_document = ReaderDocument(
id: book!.id,
format: format,
title: book.title,
author: book.author,
loadBytes: () async => bytes,
);
_initialLocator = ReaderBookAdapter.restoreLocator(book);
_initialPreferences = preferences;
_session = session;
});
Expand Down Expand Up @@ -120,21 +122,17 @@ class _ReaderPageState extends State<ReaderPage> {
}

final book = _book;
final bytes = _bytes;
final format = _format;
final document = _document;
final preferences = _initialPreferences;
if (book == null || bytes == null || format == null || preferences == null) {
return const Scaffold(body: Center(child: AppCircularProgressIndicator()));
if (book == null || document == null || preferences == null) {
return Scaffold(
appBar: AppBar(leading: BackButton(onPressed: _close)),
body: const Center(child: AppCircularProgressIndicator()),
);
}

return PapyrusReader(
document: ReaderDocument(
id: book.id,
format: format,
title: book.title,
author: book.author,
loadBytes: () async => bytes,
),
document: document,
initialLocator: _initialLocator,
initialPreferences: preferences,
onLocatorChanged: _session!.updateLocator,
Expand Down
4 changes: 2 additions & 2 deletions app/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -555,8 +555,8 @@ packages:
dependency: "direct main"
description:
path: "."
ref: "08a5161b9d00eb73581f74ce087b9ad6c1568ca7"
resolved-ref: "08a5161b9d00eb73581f74ce087b9ad6c1568ca7"
ref: e9f91aa1a6fd6a93494e7aeba057f6f65c6e1c18
resolved-ref: e9f91aa1a6fd6a93494e7aeba057f6f65c6e1c18
url: "https://github.com/PapyrusReader/reader.git"
source: git
version: "0.0.1"
Expand Down
2 changes: 1 addition & 1 deletion app/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ dependencies:
papyrus_reader:
git:
url: https://github.com/PapyrusReader/reader.git
ref: 08a5161b9d00eb73581f74ce087b9ad6c1568ca7
ref: e9f91aa1a6fd6a93494e7aeba057f6f65c6e1c18
epub_pro: ^5.6.0
syncfusion_flutter_pdf: ^32.2.3
dart_mobi: ^1.0.2
Expand Down
70 changes: 70 additions & 0 deletions app/test/pages/reader_page_test.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:papyrus/data/data_store.dart';
import 'package:papyrus/models/book.dart';
import 'package:papyrus/media/media_cache_service.dart';
import 'package:papyrus/providers/auth_provider.dart';
import 'package:papyrus/services/book_import_service_stub.dart';
import 'package:papyrus_reader/papyrus_reader.dart';
import 'package:papyrus/pages/reader_page.dart';
import 'package:papyrus/providers/preferences_provider.dart';
import 'package:provider/provider.dart';
Expand All @@ -10,6 +15,50 @@ import 'package:shared_preferences/shared_preferences.dart';
import '../helpers/test_helpers.dart';

void main() {
testWidgets('host theme changes keep the same open document and avoid rereading media', (tester) async {
SharedPreferences.setMockInitialValues({});
final preferences = PreferencesProvider(await SharedPreferences.getInstance());
final dataStore = DataStore()
..loadData(
books: [buildTestBook(id: 'epub-book', fileFormat: BookFormat.epub)],
);
final cache = _ReaderMediaCache();
addTearDown(dataStore.dispose);
late StateSetter updateTheme;
var dark = false;
await tester.pumpWidget(
MultiProvider(
providers: [
ChangeNotifierProvider.value(value: dataStore),
ChangeNotifierProvider.value(value: preferences),
Provider<MediaCacheService>.value(value: cache),
Provider<BookImportService>(create: (_) => BookImportService()),
ChangeNotifierProvider<AuthProvider>(create: (_) => _ReaderAuth()),
],
child: StatefulBuilder(
builder: (context, setState) {
updateTheme = setState;
return MaterialApp(
theme: ThemeData(brightness: dark ? Brightness.dark : Brightness.light),
home: const ReaderPage(bookId: 'epub-book'),
);
},
),
),
);
for (var frame = 0; frame < 8 && find.byType(PapyrusReader).evaluate().isEmpty; frame++) {
await tester.pump();
}
expect(cache.loads, 1, reason: tester.widgetList<Text>(find.byType(Text)).map((text) => text.data).join(' / '));
expect(find.byType(PapyrusReader), findsOneWidget);
final before = tester.widget<PapyrusReader>(find.byType(PapyrusReader)).document;
updateTheme(() => dark = true);
await tester.pump();
await tester.pump();
expect(tester.widget<PapyrusReader>(find.byType(PapyrusReader)).document, same(before));
expect(cache.loads, 1);
await tester.pumpWidget(const SizedBox.shrink());
});
testWidgets('explains when a book format is not supported', (tester) async {
SharedPreferences.setMockInitialValues({});
final preferences = PreferencesProvider(await SharedPreferences.getInstance());
Expand All @@ -33,3 +82,24 @@ void main() {
expect(find.text('This book format is not supported yet.'), findsOneWidget);
});
}

class _ReaderMediaCache extends MediaCacheService {
int loads = 0;
@override
Future<Uint8List> ensureBookFileCached(
Book book, {
required LocalBookFileReader readLocalBookFile,
required LocalBookFileWriter writeLocalBookFile,
required MediaDownloader downloadMedia,
}) async {
loads++;
return Uint8List.fromList([1, 2, 3]);
}
}

class _ReaderAuth extends ChangeNotifier implements AuthProvider {
@override
Future<Uint8List> downloadMedia(String assetId) async => Uint8List(0);
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
112 changes: 112 additions & 0 deletions app/test/pages/reader_theme_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import 'dart:math' as math;
import 'dart:typed_data';

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:papyrus/themes/app_theme.dart';
import 'package:papyrus/themes/app_motion.dart';
import 'package:papyrus_reader/papyrus_reader.dart';

void main() {
for (final host in ['light', 'dark', 'eink']) {
for (final reading in [Brightness.light, Brightness.dark]) {
for (final width in [600.0, 1200.0]) {
testWidgets('settings menu contrast: app $host, reader $reading, width $width', (tester) async {
tester.view.physicalSize = Size(width, 1000);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
final controller = ReaderController(
initialPreferences: ReaderPreferences(brightness: reading),
registry: ReaderEngineRegistry([
ReaderEngineRegistration(
formats: const {ReaderFormat.pdf},
factory: () => PdfReaderEngine(facadeFactory: (_) async => _ThemePdfFacade()),
),
]),
);
addTearDown(controller.dispose);
await tester.pumpWidget(
MaterialApp(
theme: switch (host) {
'dark' => AppTheme.dark,
'eink' => AppTheme.eink,
_ => AppTheme.light,
},
home: PapyrusReader(
document: ReaderDocument(id: 'theme', format: ReaderFormat.pdf, loadBytes: () async => Uint8List(0)),
controller: controller,
),
),
);
await tester.pumpAndSettle();
await tester.tap(find.byTooltip('Reading settings'));
await tester.pumpAndSettle();
final label = find.text('Reading mode');
final readerTheme = Theme.of(tester.element(label));
final readerColors = readerTheme.colorScheme;
if (host == 'eink') {
expect(readerTheme.extension<AppMotion>()!.reduceAnimations, isTrue);
expect(readerTheme.inputDecorationTheme.hintFadeDuration, Duration.zero);
expect(readerTheme.filledButtonTheme.style!.animationDuration, Duration.zero);
expect(readerTheme.splashFactory, same(NoSplash.splashFactory));
}
final labelText = tester.widget<RichText>(find.descendant(of: label, matching: find.byType(RichText)).first);
expect(labelText.text.style!.color, readerColors.onSurfaceVariant);
await tester.tap(find.text('Paginated').first);
await tester.pumpAndSettle();
final option = find.text('Continuous scroll').last;
final menuTheme = Theme.of(tester.element(option));
expect(menuTheme.canvasColor, readerColors.surface);
final optionText = tester.widget<RichText>(
find.descendant(of: option, matching: find.byType(RichText)).first,
);
final foreground = optionText.text.style!.color!.computeLuminance();
final background = menuTheme.canvasColor.computeLuminance();
expect(
(math.max(foreground, background) + .05) / (math.min(foreground, background) + .05),
greaterThanOrEqualTo(4.5),
);
await tester.tap(option);
await tester.pumpAndSettle();
expect(controller.preferences.layoutMode, ReaderLayoutMode.scroll);
// A mobile route captures the initial host theme. Reopen a popup
// after changing appearance while that settings route is still open.
await tester.tap(find.text(reading == Brightness.light ? 'Night' : 'Light'));
await tester.pumpAndSettle();
await tester.tap(find.text('Automatic').first);
await tester.pumpAndSettle();
final columns = find.text('Double').last;
final changedTheme = Theme.of(tester.element(columns));
expect(changedTheme.brightness, reading == Brightness.light ? Brightness.dark : Brightness.light);
expect(changedTheme.canvasColor, changedTheme.colorScheme.surface);
final columnsText = tester.widget<RichText>(
find.descendant(of: columns, matching: find.byType(RichText)).first,
);
final changedForeground = columnsText.text.style!.color!.computeLuminance();
final changedBackground = changedTheme.canvasColor.computeLuminance();
expect(
(math.max(changedForeground, changedBackground) + .05) /
(math.min(changedForeground, changedBackground) + .05),
greaterThanOrEqualTo(4.5),
);
await tester.tap(columns);
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
await tester.pumpWidget(const SizedBox.shrink());
});
}
}
}
}

final class _ThemePdfFacade implements PdfFacade {
@override
int get pageCount => 3;
@override
List<PdfFacadeOutlineEntry> get outline => const [];
@override
Future<void> showPage(int pageIndex, double pageOffset) async {}
@override
Widget buildViewport(PdfViewportConfiguration configuration) => const SizedBox.expand();
}
Loading