Skip to content
Draft
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
15 changes: 12 additions & 3 deletions flutter_readium/example/lib/widgets/reader.widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,18 @@ class ReaderWidget extends StatelessWidget {
container: true,
explicitChildNodes: true,
child: ExcludeSemantics(
child: ReadiumReaderWidget(
publication: state.publication!,
initialLocator: state.initialLocator,
child: BlocBuilder<TextSettingsBloc, TextSettingsState>(
builder: (context, settings) {
return Container(
// Ensure this color is the same as the pub background to avoid flashes of different colors
// predominantly happens on web when chapter changes
color: settings.theme.backgroundColor,
child: ReadiumReaderWidget(
publication: state.publication!,
initialLocator: state.initialLocator,
),
);
},
),
),
);
Expand Down
2 changes: 1 addition & 1 deletion flutter_readium/example/web/readiumReader.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion flutter_readium/lib/helpers/readiumReader.js

Large diffs are not rendered by default.

156 changes: 41 additions & 115 deletions flutter_readium/lib/src/flutter_readium_web.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,25 @@ class FlutterReadiumWebPlugin extends FlutterReadiumPlatform {
defaultPreferences = preferences;
}

@override
Future<Publication> openPublication(String pubUrl) async {
return await _getPublicationFromChannel(JsPublicationChannel().openPublication, pubUrl);
}

@override
Future<Publication> loadPublication(String pubUrl) async {
Publication publication;
return await _getPublicationFromChannel(JsPublicationChannel().loadPublication, pubUrl);
}

Future<Publication> _getPublicationFromChannel(
Future<String> Function(String) channelMethod,
String pubUrl,
) async {
try {
final publicationString = await JsPublicationChannel().getPublication(pubUrl);

final publicationString = await channelMethod(pubUrl);
var publicationJson = jsonDecode(publicationString) as Map<String, dynamic>;

publicationJson = _transformPublicationJson(publicationJson);

publication = Publication.fromJson(publicationJson);
return Publication.fromJson(publicationJson);
} on PlatformException catch (e) {
final type = e.intCode;
throw OpeningReadiumException(
Expand All @@ -71,131 +78,63 @@ class FlutterReadiumWebPlugin extends FlutterReadiumPlatform {
);
} on Error catch (e) {
final eString = e.toString();
throw ReadiumError('Error in PublicationChannel web: $eString');
throw ReadiumError('Error in PublicationChannel web $pubUrl: $eString');
} on Exception catch (e) {
final eString = e.toString();
throw ReadiumError('Exception in PublicationChannel web: $eString');
throw ReadiumError('Exception in PublicationChannel web $pubUrl: $eString');
}

return publication;
}

static Map<String, dynamic> _transformPublicationJson(
final Map<String, dynamic> publicationJson,
) {
// Transform 'links', 'readingOrder', 'resources', and 'tableOfContents' keys
_transformKeyItems(publicationJson, 'links');
_transformKeyItems(publicationJson, 'readingOrder');
_transformKeyItems(publicationJson, 'resources');

// rename key 'tableOfContents' to 'toc'
if (publicationJson.containsKey('tableOfContents')) {
publicationJson['toc'] = publicationJson.remove('tableOfContents');
}

// Transform 'children' key in 'toc'
if (publicationJson.containsKey('toc') && publicationJson['toc'] is Map<String, dynamic>) {
_transformKeyItems(publicationJson, 'toc');
publicationJson['toc'] = _transformChildren(publicationJson['toc']);
}

// Transform 'translations' key in 'metadata'
// TODO: create issue in ts-toolkit and remove this workaround when fixed
if (publicationJson.containsKey('metadata') && publicationJson['metadata'] is Map) {
final metadataMap = publicationJson['metadata'] as Map<String, dynamic>;

if (metadataMap.containsKey('authors') && metadataMap['authors'] is Map) {
// rename key 'authors' to 'author'
metadataMap['author'] = metadataMap.remove('authors');
// remove 'items' wrapper if exists
_transformKeyItems(metadataMap, 'author');

for (final author in metadataMap['author']) {
if (author is Map && author.containsKey('name') && author['name'] is Map) {
final nameMap = author['name'] as Map<String, dynamic>;
if (nameMap.containsKey('translations') && nameMap['translations'] is Map) {
final translationsMap = nameMap['translations'] as Map<String, dynamic>;
_validateTranslations(translationsMap);
author['name'] = translationsMap;
}
}
}
}

if (metadataMap.containsKey('title') && metadataMap['title'] is Map) {
final titleMap = metadataMap['title'] as Map<String, dynamic>;
if (titleMap.containsKey('translations') && titleMap['translations'] is Map) {
final translationsMap = titleMap['translations'] as Map<String, dynamic>;

_validateTranslations(translationsMap);

metadataMap['title'] = translationsMap;
}
}
_replaceUndefinedKey(metadataMap);

if (metadataMap.containsKey('sortAs')) {
final sortAs = metadataMap['sortAs'];
if (sortAs is Map && sortAs['translations'] is Map) {
final translations = sortAs['translations'] as Map;
if (translations.isNotEmpty) {
if (sortAs is Map) {
if (sortAs.isNotEmpty) {
// Use the first value in the translations map
metadataMap['sortAs'] = translations.values.first;
metadataMap['sortAs'] = sortAs.values.first;
R2Log.d('Pub: ${metadataMap['title']} sortAs map transformed to first value.');
} else {
metadataMap['sortAs'] = null;
R2Log.d('Pub: ${metadataMap['title']} sortAs map is empty, setting to null.');
}
} else if (sortAs is! String) {
metadataMap['sortAs'] = null;
R2Log.d(
'Pub: ${metadataMap['title']} sortAs is not a String or Map, setting to null. Actual type: ${sortAs.runtimeType}');
}
}
}

return publicationJson;
}

static void _transformKeyItems(final Map<String, dynamic> json, final String key) {
if (json.containsKey(key) && json[key] is Map) {
final map = json[key] as Map<String, dynamic>;
if (map.containsKey('items') && map['items'] is List) {
json[key] = map['items'];
static void _replaceUndefinedKey(Map<dynamic, dynamic> map) {
final keysToReplace = <dynamic>[];
map.forEach((key, value) {
if (key == 'undefined') {
keysToReplace.add(key);
}
}
}

static List<dynamic> _transformChildren(final List<dynamic> items) => items.map((final item) {
if (item is Map<String, dynamic> && item.containsKey('children')) {
final children = item['children'];
if (children is Map<String, dynamic> && children.containsKey('items')) {
item['children'] = children['items'];
}
if (item['children'] is List) {
item['children'] = _transformChildren(item['children']);
if (value is Map) {
_replaceUndefinedKey(value);
} else if (value is List) {
for (var item in value) {
if (item is Map) {
_replaceUndefinedKey(item);
}
}
return item;
}).toList();

static void _validateTranslations(Map<String, dynamic> translationsMap) {
if (translationsMap.containsKey('undefined')) {
translationsMap['und'] = translationsMap.remove('undefined');
}

// TODO: unknown if other languages also fails the validation, needs better handling
translationsMap.forEach((final key, final value) {
if (key.length > 3) {
R2Log.d('PUBLICATION WEB: Translations map key "$key" is longer than three letters.');
}
});
}

@override
Future<Publication> openPublication(String pubUrl) async {
// NOTE: For web, loadPublication and openPublication does the same thing,
//
// If calling the openPublication method outside of ReadiumWebView it will throw an error right away if there is no div with the id 'container'
// additionally the openPublication method does currently not return a publication object
R2Log.d(
'Cannot call openPublication outside of ReadiumWebView on web. Using getPublication instead to fetch the publication data.');
final publication = await loadPublication(pubUrl);
return publication;
for (var key in keysToReplace) {
map['und'] = map.remove(key);
}
}

@override
Expand All @@ -205,23 +144,10 @@ class FlutterReadiumWebPlugin extends FlutterReadiumPlatform {
}

@override
Future<String?> getLinkContent(Link link) {
return getString(link);
}

static Future<String> getString(final Link link) async {
// Get HTML string for full chapters, for example
final linkString = json.encode(link);
final resourceString = await JsPublicationChannel().getResource(linkString);
return resourceString;
}

static Future<Uint8List> getBytes(final Link link) async {
// TODO: Is this still needed for audio books with the new implementation
Future<String?> getLinkContent(Link link) async {
final linkString = json.encode(link);
final resourceBytesString = await JsPublicationChannel().getResource(linkString, asBytes: true);
final byteList = jsonDecode(resourceBytesString).cast<int>();
return Uint8List.fromList(byteList);
final linkContent = await JsPublicationChannel().getLinkContent(linkString);
return linkContent;
}

@override
Expand Down
47 changes: 34 additions & 13 deletions flutter_readium/lib/src/js_publication_channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import 'package:flutter_readium_platform_interface/flutter_readium_platform_inte
@JS('ReadiumReader')
extension type ReadiumReader._(JSObject _) implements JSObject {
external ReadiumReader();
external JSPromise openPublication(
JSString publicationURL, JSString pubId, JSString? initialPositionJson, JSString preferencesJson);
external JSPromise getPublication(JSString link);
external JSPromise initializeNavigator(
JSString publicationURL, JSString? initialPositionJson, JSString preferencesJson);
external JSPromise<JSString> openPublication(JSString link);
external JSPromise<JSString> loadPublication(JSString link);
external JSPromise goTo(JSString location);
external void goLeft();
external void goRight();
external void closePublication();
external JSPromise getResource(JSString linkString, JSBoolean? asBytes);
external JSPromise<JSString> getLinkContent(JSString linkString, JSBoolean? asBytes);
external void setEPUBPreferences(JSString newPreferencesString);
external JSBoolean get isNavigatorReady;
}
Expand All @@ -26,11 +27,11 @@ external set updateReaderStatus(JSFunction f);
class JsPublicationChannel {
static final ReadiumReader _readiumReader = ReadiumReader();

Future<void> openPublication(String publicationURL,
{required String pubId, required String initialPreferences, String? initialPositionJson}) async {
Future<void> initializeNavigator(String publicationURL,
{required String initialPreferences, String? initialPositionJson}) async {
try {
await _readiumReader
.openPublication(publicationURL.toJS, pubId.toJS, initialPositionJson?.toJS, initialPreferences.toJS)
.initializeNavigator(publicationURL.toJS, initialPositionJson?.toJS, initialPreferences.toJS)
.toDart;
} on Object catch (jsError, stackTrace) {
String errorString = jsError.toString();
Expand All @@ -45,10 +46,30 @@ class JsPublicationChannel {
}
}

Future<String> getPublication(String link) async {
Future<String> openPublication(String link) async {
try {
final publicationPromise = _readiumReader.getPublication(link.toJS);
final publicationString = await publicationPromise.toDart as String;
final publicationPromise = await _readiumReader.openPublication(link.toJS).toDart;
final publicationString = publicationPromise.toDart;

return publicationString;
} on Object catch (jsError, stackTrace) {
String errorString = jsError.toString();
int? statusCode = _extractStatusCode(errorString);
String nativeCode = _convertToNativeCode(statusCode);

throw PlatformException(
code: nativeCode,
message: errorString,
details: statusCode,
stacktrace: stackTrace.toString(),
);
}
}

Future<String> loadPublication(String link) async {
try {
final publicationPromise = await _readiumReader.loadPublication(link.toJS).toDart;
final publicationString = publicationPromise.toDart;

return publicationString;
} on Object catch (jsError, stackTrace) {
Expand Down Expand Up @@ -122,10 +143,10 @@ class JsPublicationChannel {
_readiumReader.closePublication();
}

Future<String> getResource(String link, {bool? asBytes}) async {
Future<String> getLinkContent(String link, {bool? asBytes}) async {
try {
final resourceJS = _readiumReader.getResource(link.toJS, asBytes?.toJS);
var resourceString = await resourceJS.toDart as String;
final resourceJS = await _readiumReader.getLinkContent(link.toJS, asBytes?.toJS).toDart;
var resourceString = resourceJS.toDart;
return resourceString;
} on Object catch (jsError, stackTrace) {
String errorString = jsError.toString();
Expand Down
6 changes: 2 additions & 4 deletions flutter_readium/lib/src/readium_webview.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ class ReadiumWebViewState extends State<ReadiumWebView> {

@js_interop.JSExport()
void onReaderStatusChanged(final String statusString) {
print('Reader status changed: $statusString');
final status = ReadiumReaderStatus.values.firstWhereOrNull(
(e) => e.name == statusString,
);
Expand All @@ -72,12 +71,11 @@ class ReadiumWebViewState extends State<ReadiumWebView> {
throw Exception('Publication URL not found in publication links');
}

final pubId = widget.publication.identifier;
final preferences = _defaultPreferences?.toJson() ?? <String, dynamic>{};
final currentLocatorString = widget.currentLocator != null ? json.encode(widget.currentLocator) : null;
registerJSExports();
await JsPublicationChannel().openPublication(publicationUrl,
pubId: pubId, initialPreferences: json.encode(preferences), initialPositionJson: currentLocatorString);
await JsPublicationChannel().initializeNavigator(publicationUrl,
initialPreferences: json.encode(preferences), initialPositionJson: currentLocatorString);
} catch (e) {
// This is a temporary solution to show an error message when opening a publication fails
// Do we need to have the app send what message it wants to show and make a dialog here? or continue to display it in the html view?
Expand Down
22 changes: 11 additions & 11 deletions flutter_readium/web/_scripts/Epub/epubNavigator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ export async function initializeEpubNavigatorAndPeripherals(
});

const listeners: EpubNavigatorListeners = {
scroll: function (_amount: number): void {},
frameLoaded: function (_wnd: Window): void {
scroll(_amount: number): void {},
frameLoaded(_wnd: Window): void {
nav._cframes.forEach(
(frameManager: FrameManager | FXLFrameManager | undefined) => {
if (frameManager) {
Expand All @@ -102,23 +102,23 @@ export async function initializeEpubNavigatorAndPeripherals(
);
p.observe(window);
},
positionChanged: (_locator: Locator): void => {
positionChanged(_locator: Locator): void {
window.focus();

(window as any).updateTextLocator?.(JSON.stringify(_locator));
window.updateTextLocator?.(JSON.stringify(_locator));
},
tap: function (_e: FrameClickEvent): boolean {
tap(_e: FrameClickEvent): boolean {
return false;
},
click: function (_e: FrameClickEvent): boolean {
click(_e: FrameClickEvent): boolean {
return false;
},
zoom: function (_scale: number): void {},
miscPointer: function (_amount: number): void {
zoom(_scale: number): void {},
miscPointer(_amount: number): void {
// fires when a tap or a click was made in the middle of the iframe e.g. show/hide UI
},
customEvent: function (_key: string, _data: unknown): void {},
handleLocator: function (locator: Locator): boolean {
customEvent(_key: string, _data: unknown): void {},
handleLocator(locator: Locator): boolean {
const href = locator.href;
if (
href.startsWith("http://") ||
Expand All @@ -132,7 +132,7 @@ export async function initializeEpubNavigatorAndPeripherals(
}
return false;
},
textSelected: function (_selection: BasicTextSelection): void {
textSelected(_selection: BasicTextSelection): void {
highlightSelection(nav, publication, _selection);
},
};
Expand Down
Loading