From 40445fd83fb4ec890314e5c433e6f7bb5182c4eb Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 14 Jun 2026 10:40:07 +0200 Subject: [PATCH] fix: internal e-reader epubx issues fix: gradle issues due to flutter migration fix: minor colortheme adjustments --- analysis_options.yaml | 4 + .../reports/problems/problems-report.html | 663 ------------------ android/gradle.properties | 2 + .../book_details/bloc/book_details_bloc.dart | 50 +- .../book_details/bloc/book_details_event.dart | 11 +- .../book_details/bloc/book_details_state.dart | 6 + .../book_details_remote_datasource.dart | 95 ++- .../repositories/book_details_repository.dart | 21 +- .../presentation/pages/book_details_page.dart | 87 ++- .../presentation/pages/book_view_page.dart | 2 +- .../presentation/pages/settings_page.dart | 58 +- .../widgets/download_options_widget.dart | 13 +- .../presentation/widgets/feedback_widget.dart | 2 +- .../widgets/sync_filter_bottom_sheet.dart | 6 +- .../widgets/sync_settings_widget.dart | 6 +- .../widgets/theme_selector_widget.dart | 10 +- packages/epubx/CHANGELOG.md | 148 ++++ packages/epubx/CODE_OF_CONDUCT.md | 46 ++ packages/epubx/LICENSE | 21 + packages/epubx/README.md | 134 ++++ packages/epubx/analysis_options.yaml | 15 + packages/epubx/lib/epubx.dart | 36 + .../epubx/lib/src/entities/epub_book.dart | 47 ++ .../src/entities/epub_byte_content_file.dart | 30 + .../epubx/lib/src/entities/epub_chapter.dart | 39 ++ .../epubx/lib/src/entities/epub_content.dart | 52 ++ .../lib/src/entities/epub_content_file.dart | 23 + .../lib/src/entities/epub_content_type.dart | 17 + .../epubx/lib/src/entities/epub_schema.dart | 25 + .../src/entities/epub_text_content_file.dart | 22 + packages/epubx/lib/src/epub_reader.dart | 208 ++++++ packages/epubx/lib/src/epub_writer.dart | 59 ++ .../lib/src/readers/book_cover_reader.dart | 46 ++ .../epubx/lib/src/readers/chapter_reader.dart | 53 ++ .../epubx/lib/src/readers/content_reader.dart | 137 ++++ .../lib/src/readers/navigation_reader.dart | 559 +++++++++++++++ .../epubx/lib/src/readers/package_reader.dart | 403 +++++++++++ .../src/readers/root_file_path_reader.dart | 36 + .../epubx/lib/src/readers/schema_reader.dart | 31 + .../lib/src/ref_entities/epub_book_ref.dart | 62 ++ .../epub_byte_content_file_ref.dart | 12 + .../src/ref_entities/epub_chapter_ref.dart | 52 ++ .../ref_entities/epub_content_file_ref.dart | 95 +++ .../src/ref_entities/epub_content_ref.dart | 53 ++ .../epub_text_content_file_ref.dart | 12 + .../src/schema/navigation/epub_metadata.dart | 22 + .../schema/navigation/epub_navigation.dart | 51 ++ .../epub_navigation_doc_author.dart | 24 + .../navigation/epub_navigation_doc_title.dart | 24 + .../navigation/epub_navigation_head.dart | 28 + .../navigation/epub_navigation_head_meta.dart | 22 + .../navigation/epub_navigation_label.dart | 18 + .../navigation/epub_navigation_list.dart | 41 ++ .../navigation/epub_navigation_map.dart | 21 + .../navigation/epub_navigation_page_list.dart | 21 + .../epub_navigation_page_target.dart | 49 ++ .../epub_navigation_page_target_type.dart | 1 + .../navigation/epub_navigation_point.dart | 52 ++ .../navigation/epub_navigation_target.dart | 43 ++ .../epubx/lib/src/schema/opf/epub_guide.dart | 29 + .../src/schema/opf/epub_guide_reference.dart | 27 + .../lib/src/schema/opf/epub_manifest.dart | 26 + .../src/schema/opf/epub_manifest_item.dart | 49 ++ .../lib/src/schema/opf/epub_metadata.dart | 78 +++ .../schema/opf/epub_metadata_contributor.dart | 21 + .../src/schema/opf/epub_metadata_creator.dart | 19 + .../src/schema/opf/epub_metadata_date.dart | 16 + .../schema/opf/epub_metadata_identifier.dart | 19 + .../src/schema/opf/epub_metadata_meta.dart | 33 + .../lib/src/schema/opf/epub_package.dart | 38 + .../epubx/lib/src/schema/opf/epub_spine.dart | 32 + .../src/schema/opf/epub_spine_item_ref.dart | 24 + .../lib/src/schema/opf/epub_version.dart | 1 + .../epubx/lib/src/utils/enum_from_string.dart | 16 + .../epubx/lib/src/utils/zip_path_utils.dart | 18 + .../lib/src/writers/epub_guide_writer.dart | 15 + .../lib/src/writers/epub_manifest_writer.dart | 17 + .../lib/src/writers/epub_metadata_writer.dart | 105 +++ .../src/writers/epub_navigation_writer.dart | 67 ++ .../lib/src/writers/epub_package_writer.dart | 31 + .../lib/src/writers/epub_spine_writer.dart | 15 + packages/epubx/pubspec.lock | 429 ++++++++++++ packages/epubx/pubspec.yaml | 21 + packages/epubx/tool/publish.sh | 14 + packages/epubx/tool/travis.sh | 10 + pubspec.lock | 41 +- pubspec.yaml | 13 +- 87 files changed, 4276 insertions(+), 874 deletions(-) delete mode 100644 android/build/reports/problems/problems-report.html create mode 100644 packages/epubx/CHANGELOG.md create mode 100644 packages/epubx/CODE_OF_CONDUCT.md create mode 100644 packages/epubx/LICENSE create mode 100644 packages/epubx/README.md create mode 100644 packages/epubx/analysis_options.yaml create mode 100644 packages/epubx/lib/epubx.dart create mode 100644 packages/epubx/lib/src/entities/epub_book.dart create mode 100644 packages/epubx/lib/src/entities/epub_byte_content_file.dart create mode 100644 packages/epubx/lib/src/entities/epub_chapter.dart create mode 100644 packages/epubx/lib/src/entities/epub_content.dart create mode 100644 packages/epubx/lib/src/entities/epub_content_file.dart create mode 100644 packages/epubx/lib/src/entities/epub_content_type.dart create mode 100644 packages/epubx/lib/src/entities/epub_schema.dart create mode 100644 packages/epubx/lib/src/entities/epub_text_content_file.dart create mode 100644 packages/epubx/lib/src/epub_reader.dart create mode 100644 packages/epubx/lib/src/epub_writer.dart create mode 100644 packages/epubx/lib/src/readers/book_cover_reader.dart create mode 100644 packages/epubx/lib/src/readers/chapter_reader.dart create mode 100644 packages/epubx/lib/src/readers/content_reader.dart create mode 100644 packages/epubx/lib/src/readers/navigation_reader.dart create mode 100644 packages/epubx/lib/src/readers/package_reader.dart create mode 100644 packages/epubx/lib/src/readers/root_file_path_reader.dart create mode 100644 packages/epubx/lib/src/readers/schema_reader.dart create mode 100644 packages/epubx/lib/src/ref_entities/epub_book_ref.dart create mode 100644 packages/epubx/lib/src/ref_entities/epub_byte_content_file_ref.dart create mode 100644 packages/epubx/lib/src/ref_entities/epub_chapter_ref.dart create mode 100644 packages/epubx/lib/src/ref_entities/epub_content_file_ref.dart create mode 100644 packages/epubx/lib/src/ref_entities/epub_content_ref.dart create mode 100644 packages/epubx/lib/src/ref_entities/epub_text_content_file_ref.dart create mode 100644 packages/epubx/lib/src/schema/navigation/epub_metadata.dart create mode 100644 packages/epubx/lib/src/schema/navigation/epub_navigation.dart create mode 100644 packages/epubx/lib/src/schema/navigation/epub_navigation_doc_author.dart create mode 100644 packages/epubx/lib/src/schema/navigation/epub_navigation_doc_title.dart create mode 100644 packages/epubx/lib/src/schema/navigation/epub_navigation_head.dart create mode 100644 packages/epubx/lib/src/schema/navigation/epub_navigation_head_meta.dart create mode 100644 packages/epubx/lib/src/schema/navigation/epub_navigation_label.dart create mode 100644 packages/epubx/lib/src/schema/navigation/epub_navigation_list.dart create mode 100644 packages/epubx/lib/src/schema/navigation/epub_navigation_map.dart create mode 100644 packages/epubx/lib/src/schema/navigation/epub_navigation_page_list.dart create mode 100644 packages/epubx/lib/src/schema/navigation/epub_navigation_page_target.dart create mode 100644 packages/epubx/lib/src/schema/navigation/epub_navigation_page_target_type.dart create mode 100644 packages/epubx/lib/src/schema/navigation/epub_navigation_point.dart create mode 100644 packages/epubx/lib/src/schema/navigation/epub_navigation_target.dart create mode 100644 packages/epubx/lib/src/schema/opf/epub_guide.dart create mode 100644 packages/epubx/lib/src/schema/opf/epub_guide_reference.dart create mode 100644 packages/epubx/lib/src/schema/opf/epub_manifest.dart create mode 100644 packages/epubx/lib/src/schema/opf/epub_manifest_item.dart create mode 100644 packages/epubx/lib/src/schema/opf/epub_metadata.dart create mode 100644 packages/epubx/lib/src/schema/opf/epub_metadata_contributor.dart create mode 100644 packages/epubx/lib/src/schema/opf/epub_metadata_creator.dart create mode 100644 packages/epubx/lib/src/schema/opf/epub_metadata_date.dart create mode 100644 packages/epubx/lib/src/schema/opf/epub_metadata_identifier.dart create mode 100644 packages/epubx/lib/src/schema/opf/epub_metadata_meta.dart create mode 100644 packages/epubx/lib/src/schema/opf/epub_package.dart create mode 100644 packages/epubx/lib/src/schema/opf/epub_spine.dart create mode 100644 packages/epubx/lib/src/schema/opf/epub_spine_item_ref.dart create mode 100644 packages/epubx/lib/src/schema/opf/epub_version.dart create mode 100644 packages/epubx/lib/src/utils/enum_from_string.dart create mode 100644 packages/epubx/lib/src/utils/zip_path_utils.dart create mode 100644 packages/epubx/lib/src/writers/epub_guide_writer.dart create mode 100644 packages/epubx/lib/src/writers/epub_manifest_writer.dart create mode 100644 packages/epubx/lib/src/writers/epub_metadata_writer.dart create mode 100644 packages/epubx/lib/src/writers/epub_navigation_writer.dart create mode 100644 packages/epubx/lib/src/writers/epub_package_writer.dart create mode 100644 packages/epubx/lib/src/writers/epub_spine_writer.dart create mode 100644 packages/epubx/pubspec.lock create mode 100644 packages/epubx/pubspec.yaml create mode 100644 packages/epubx/tool/publish.sh create mode 100755 packages/epubx/tool/travis.sh diff --git a/analysis_options.yaml b/analysis_options.yaml index 0d29021..b0653c0 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -9,6 +9,10 @@ # packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml +analyzer: + exclude: + - packages/** + linter: # The lint rules applied to this project can be customized in the # section below to disable rules from the `package:flutter_lints/flutter.yaml` diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html deleted file mode 100644 index d4cd3a9..0000000 --- a/android/build/reports/problems/problems-report.html +++ /dev/null @@ -1,663 +0,0 @@ - - - - - - - - - - - - - Gradle Configuration Cache - - - -
- -
- Loading... -
- - - - - - diff --git a/android/gradle.properties b/android/gradle.properties index f018a61..5a1481d 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,3 +1,5 @@ org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true android.enableJetifier=true +android.builtInKotlin=false +android.newDsl=false diff --git a/lib/features/book_details/bloc/book_details_bloc.dart b/lib/features/book_details/bloc/book_details_bloc.dart index e1c62de..51c8582 100644 --- a/lib/features/book_details/bloc/book_details_bloc.dart +++ b/lib/features/book_details/bloc/book_details_bloc.dart @@ -1,3 +1,5 @@ +import 'dart:typed_data'; + import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:docman/docman.dart'; import 'package:logger/logger.dart'; @@ -422,32 +424,34 @@ class BookDetailsBloc extends Bloc { ), ); - final downloadedPath = await repository.openInInternalReader( - event.selectedDirectory, - event.schema, - event.book, - format: event.format, - progressCallback: (progress) { - logger.d('Reader download progress: $progress%'); - emit(state.copyWith(downloadProgress: progress)); - }, - ); + final uuid = state.bookViewModel?.uuid ?? state.bookDetails!.uuid; - if (downloadedPath.isNotEmpty) { - emit( - state.copyWith( - openInInternalReaderState: OpenInInternalReaderState.success, - downloadFilePath: downloadedPath, - ), - ); - } else { - emit( - state.copyWith( - openInInternalReaderState: OpenInInternalReaderState.error, - errorMessage: 'Failed to open book in internal reader', - ), + Uint8List? bytes; + + if (await downloadManager.checkFileExistence(uuid)) { + final path = downloadManager.getBookPath(uuid)!; + logger.i('Trying downloaded copy for reader from: $path'); + bytes = await repository.readLocalEpubBytes(path); + } + + if (bytes == null) { + logger.i('Streaming EPUB bytes into reader (no local EPUB copy).'); + bytes = await repository.streamBookBytes( + event.book, + format: event.format, + progressCallback: (progress) { + logger.d('Reader stream progress: $progress%'); + emit(state.copyWith(downloadProgress: progress)); + }, ); } + + emit( + state.copyWith( + openInInternalReaderState: OpenInInternalReaderState.success, + readerBytes: bytes, + ), + ); } catch (e) { logger.e('Error opening book in internal reader: $e'); emit( diff --git a/lib/features/book_details/bloc/book_details_event.dart b/lib/features/book_details/bloc/book_details_event.dart index 8731a0d..3051b1a 100644 --- a/lib/features/book_details/bloc/book_details_event.dart +++ b/lib/features/book_details/bloc/book_details_event.dart @@ -125,20 +125,13 @@ class OpenBookInReader extends BookDetailsEvent { } class OpenBookInInternalReader extends BookDetailsEvent { - final DocumentFile selectedDirectory; - final DownloadSchema schema; final BookDetailsModel book; final String format; - const OpenBookInInternalReader({ - required this.selectedDirectory, - required this.schema, - required this.book, - required this.format, - }); + const OpenBookInInternalReader({required this.book, required this.format}); @override - List get props => [selectedDirectory, schema, book, format]; + List get props => [book, format]; } class OpenBookInBrowser extends BookDetailsEvent { diff --git a/lib/features/book_details/bloc/book_details_state.dart b/lib/features/book_details/bloc/book_details_state.dart index 08a7190..cba779d 100644 --- a/lib/features/book_details/bloc/book_details_state.dart +++ b/lib/features/book_details/bloc/book_details_state.dart @@ -1,3 +1,5 @@ +import 'dart:typed_data'; + import 'package:equatable/equatable.dart'; import 'package:calibre_web_companion/features/book_details/data/models/book_details_model.dart'; @@ -57,6 +59,7 @@ class BookDetailsState extends Equatable { final OpenInInternalReaderState openInInternalReaderState; final String? downloadErrorMessage; final String? downloadFilePath; + final Uint8List? readerBytes; final MetadataUpdateState metadataUpdateState; final SendToEReaderState sendToEReaderState; final int sendToEReaderProgress; @@ -83,6 +86,7 @@ class BookDetailsState extends Equatable { this.openInInternalReaderState = OpenInInternalReaderState.initial, this.downloadErrorMessage, this.downloadFilePath, + this.readerBytes, this.metadataUpdateState = MetadataUpdateState.initial, this.sendToEReaderState = SendToEReaderState.initial, this.sendToEReaderProgress = 0, @@ -110,6 +114,7 @@ class BookDetailsState extends Equatable { OpenInInternalReaderState? openInInternalReaderState, String? downloadErrorMessage, String? downloadFilePath, + Uint8List? readerBytes, MetadataUpdateState? metadataUpdateState, SendToEReaderState? sendToEReaderState, int? sendToEReaderProgress, @@ -137,6 +142,7 @@ class BookDetailsState extends Equatable { openInInternalReaderState ?? this.openInInternalReaderState, downloadErrorMessage: downloadErrorMessage ?? this.downloadErrorMessage, downloadFilePath: downloadFilePath ?? this.downloadFilePath, + readerBytes: readerBytes ?? this.readerBytes, metadataUpdateState: metadataUpdateState ?? this.metadataUpdateState, sendToEReaderState: sendToEReaderState ?? this.sendToEReaderState, sendToEReaderProgress: diff --git a/lib/features/book_details/data/datasources/book_details_remote_datasource.dart b/lib/features/book_details/data/datasources/book_details_remote_datasource.dart index 7be399f..f477c7c 100644 --- a/lib/features/book_details/data/datasources/book_details_remote_datasource.dart +++ b/lib/features/book_details/data/datasources/book_details_remote_datasource.dart @@ -8,7 +8,6 @@ import 'package:http/http.dart' as http; import 'package:logger/logger.dart'; import 'package:open_file/open_file.dart'; import 'package:path/path.dart' as path; -import 'package:path_provider/path_provider.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:get_it/get_it.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -710,63 +709,63 @@ class BookDetailsRemoteDatasource { } } - Future downloadBookForReader( - BookDetailsModel book, - DocumentFile selectedDirectory, - DownloadSchema schema, { + Future streamBookBytes( + BookDetailsModel book, { String format = 'epub', Function(int)? progressCallback, }) async { + final response = await getDownloadStream(book.id.toString(), format); + final contentLength = response.contentLength ?? -1; + + final List bytes = []; + int received = 0; + await for (final chunk in response.stream) { + bytes.addAll(chunk); + received += chunk.length; + if (contentLength > 0 && progressCallback != null) { + progressCallback((received / contentLength * 100).round()); + } + } + return Uint8List.fromList(bytes); + } + + Future readLocalEpubBytes(String path) async { try { - logger.i('Preparing book for internal reader: ${book.title}'); + String name; + Uint8List bytes; - final selectedFormat = format.toLowerCase(); - - final safFileUri = await downloadBookToPath( - book: book, - selectedDirectory: selectedDirectory, - schema: schema, - format: selectedFormat, - // Reuse an already-downloaded file instead of fetching it again every - // time the reader is opened. - reuseExistingFile: true, - progressCallback: progressCallback, - ); - - DocumentFile? safFile = - safFileUri.isNotEmpty ? await DocumentFile.fromUri(safFileUri) : null; - - if (safFile == null || !safFile.isFile) { - logger.e('Downloaded file is not a valid file: $safFileUri'); - throw Exception('Downloaded file is not a valid file: $safFileUri'); + if (Platform.isAndroid && + (path.startsWith('content://') || path.startsWith('file://'))) { + final doc = await DocumentFile.fromUri(path); + if (doc == null || !doc.isFile) return null; + name = doc.name; + final read = await doc.read(); + if (read == null || read.isEmpty) return null; + bytes = read; + } else { + final file = File(path); + if (!file.existsSync()) return null; + name = file.path.split('/').last; + bytes = await file.readAsBytes(); + if (bytes.isEmpty) return null; } - final bytes = await safFile.read(); - if (bytes == null) { - logger.e('Could not read bytes from SAF file.'); - throw Exception('Could not read bytes from SAF file.'); + final lower = name.toLowerCase(); + final isEpubName = lower.endsWith('.epub') || lower.endsWith('.kepub'); + final looksLikeZip = + bytes.length >= 2 && bytes[0] == 0x50 && bytes[1] == 0x4B; + + if (!isEpubName || !looksLikeZip) { + logger.i( + 'Local copy "$name" is not a readable EPUB — will stream instead.', + ); + return null; } - final tempDir = await getTemporaryDirectory(); - - final safeFileName = safFile.name.replaceAll( - RegExp(r'[^a-zA-Z0-9.\-_]'), - '_', - ); - - final localFile = File('${tempDir.path}/$safeFileName'); - await localFile.writeAsBytes(bytes, flush: true); - - if (!await localFile.exists()) { - logger.e('Failed to create local cache file at ${localFile.path}'); - throw Exception('Failed to create local cache file.'); - } - - logger.i('File prepared for reader at: ${localFile.path}'); - return localFile.path; + return Uint8List.fromList(bytes); } catch (e) { - logger.e('Error preparing book for reader: $e'); - throw Exception('Error preparing book for reader: $e'); + logger.w('Could not read local copy ($path), will stream instead: $e'); + return null; } } diff --git a/lib/features/book_details/data/repositories/book_details_repository.dart b/lib/features/book_details/data/repositories/book_details_repository.dart index c5299dc..44517e0 100644 --- a/lib/features/book_details/data/repositories/book_details_repository.dart +++ b/lib/features/book_details/data/repositories/book_details_repository.dart @@ -180,21 +180,18 @@ class BookDetailsRepository { } } - Future openInInternalReader( - DocumentFile selectedDirectory, - DownloadSchema schema, + Future streamBookBytes( BookDetailsModel book, { String format = 'epub', Function(int)? progressCallback, - }) async { - return await datasource.downloadBookForReader( - book, - selectedDirectory, - schema, - format: format, - progressCallback: progressCallback, - ); - } + }) => datasource.streamBookBytes( + book, + format: format, + progressCallback: progressCallback, + ); + + Future readLocalEpubBytes(String path) => + datasource.readLocalEpubBytes(path); Future getSeriesPath(String seriesName) async { try { diff --git a/lib/features/book_details/presentation/pages/book_details_page.dart b/lib/features/book_details/presentation/pages/book_details_page.dart index 0ecf0fd..b1ae108 100644 --- a/lib/features/book_details/presentation/pages/book_details_page.dart +++ b/lib/features/book_details/presentation/pages/book_details_page.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:typed_data'; import 'dart:convert'; import 'package:docman/docman.dart'; @@ -57,6 +58,13 @@ class BookDetailsPage extends StatefulWidget { class _BookDetailsPageState extends State { bool _didUpdateMetadata = false; late final WebDavSyncService _webDavService; + Timer? _readerProgressTimer; + + @override + void dispose() { + _readerProgressTimer?.cancel(); + super.dispose(); + } bool _isInternalReaderSupportedFormat(String format) { return format.toLowerCase() == 'epub'; @@ -183,30 +191,40 @@ class _BookDetailsPageState extends State { Future _openInternalReader( BuildContext context, - String filePath, + Uint8List bytes, BookDetailsModel bookDetailsModel, ) async { final localization = AppLocalizations.of(context)!; - if (!filePath.toLowerCase().endsWith('.epub')) { - context.showSnackBar( - localization.errorOpeningBookInInternalReaderPdf, - isError: true, - duration: const Duration(seconds: 10), - ); - return; - } - final bookUuid = bookDetailsModel.uuid; await _restoreReaderProgressFromCloud(bookUuid); if (!context.mounted) return; + final looksLikeZip = + bytes.length >= 4 && + bytes[0] == 0x50 && + bytes[1] == 0x4B && + bytes[2] == 0x03 && + bytes[3] == 0x04; + if (!looksLikeZip) { + if (context.mounted) { + context.showSnackBar( + localization.errorOpeningBookInInternalReader, + isError: true, + ); + } + return; + } + try { - await CosmosEpub.openLocalBook( + await CosmosEpub.openFileBook( context: context, - localPath: filePath, + bytes: bytes, bookId: bookUuid, accentColor: Theme.of(context).colorScheme.primary, + onPageFlip: (currentPage, totalPages) { + _scheduleReaderProgressSync(bookUuid); + }, ); } catch (e) { if (context.mounted) { @@ -218,9 +236,19 @@ class _BookDetailsPageState extends State { return; } + // Flush any pending debounced upload immediately when the reader closes. + _readerProgressTimer?.cancel(); await _saveReaderProgressToCloud(bookUuid); } + void _scheduleReaderProgressSync(String bookUuid) { + _readerProgressTimer?.cancel(); + _readerProgressTimer = Timer( + const Duration(seconds: 5), + () => _saveReaderProgressToCloud(bookUuid), + ); + } + Future _restoreReaderProgressFromCloud(String bookUuid) async { final prefs = await SharedPreferences.getInstance(); if (!(prefs.getBool('webdav_enabled') ?? false)) return; @@ -376,12 +404,12 @@ class _BookDetailsPageState extends State { if (state.openInInternalReaderState == OpenInInternalReaderState.success && - state.downloadFilePath != null) { + state.readerBytes != null) { context.read().add(const ClearSnackBarStates()); _openInternalReader( context, - state.downloadFilePath!, + state.readerBytes!, state.bookDetails!, ); } @@ -1051,40 +1079,9 @@ class _BookDetailsPageState extends State { return; } - DocumentFile? selectedDirectory; - - if (settingsState.defaultDownloadPath.isEmpty) { - selectedDirectory = await DocMan.pick.directory(); - if (selectedDirectory == null) { - // ignore: use_build_context_synchronously - context.showSnackBar( - localizations.noFolderWasSelected, - isError: true, - ); - return; - } - } else { - final uri = settingsState.defaultDownloadPath; - selectedDirectory = - uri.isNotEmpty - ? await DocumentFile.fromUri(uri) - : null; - if (selectedDirectory == null || - !selectedDirectory.isDirectory) { - // ignore: use_build_context_synchronously - context.showSnackBar( - localizations.noFolderWasSelected, - isError: true, - ); - return; - } - } - // ignore: use_build_context_synchronously context.read().add( OpenBookInInternalReader( - selectedDirectory: selectedDirectory, - schema: settingsState.downloadSchema, book: book, format: selectedFormat, ), diff --git a/lib/features/book_view/presentation/pages/book_view_page.dart b/lib/features/book_view/presentation/pages/book_view_page.dart index c30fd47..20460cf 100644 --- a/lib/features/book_view/presentation/pages/book_view_page.dart +++ b/lib/features/book_view/presentation/pages/book_view_page.dart @@ -440,7 +440,7 @@ class _BookViewPageState extends State { BuildContext context, AppLocalizations localizations, ) async { - final result = await FilePicker.platform.pickFiles( + final result = await FilePicker.pickFiles( type: FileType.custom, allowedExtensions: ['pdf', 'epub', 'mobi', 'fb2', 'cbr', 'djvu', 'cbz'], allowMultiple: false, diff --git a/lib/features/settings/presentation/pages/settings_page.dart b/lib/features/settings/presentation/pages/settings_page.dart index f1e6470..22086d1 100644 --- a/lib/features/settings/presentation/pages/settings_page.dart +++ b/lib/features/settings/presentation/pages/settings_page.dart @@ -391,7 +391,7 @@ class _SettingsPageState extends State { Icon( icon, size: 28, - color: Theme.of(context).colorScheme.secondary, + color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 16), Expanded( @@ -443,7 +443,7 @@ class _SettingsPageState extends State { Icon( Icons.vpn_key_rounded, size: 28, - color: Theme.of(context).colorScheme.secondary, + color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 16), Expanded( @@ -483,7 +483,7 @@ class _SettingsPageState extends State { title, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.secondary, + color: Theme.of(context).colorScheme.primary, ), ), ); @@ -511,7 +511,7 @@ class _SettingsPageState extends State { Icon( Icons.format_size_rounded, size: 28, - color: Theme.of(context).colorScheme.secondary, + color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 16), Expanded( @@ -581,7 +581,7 @@ class _SettingsPageState extends State { Icon( Icons.e_mobiledata, size: 28, - color: Theme.of(context).colorScheme.secondary, + color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 16), Expanded( @@ -632,7 +632,7 @@ class _SettingsPageState extends State { children: [ Icon( Icons.download_rounded, - color: Theme.of(context).colorScheme.secondary, + color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 16), Expanded( @@ -702,7 +702,7 @@ class _SettingsPageState extends State { children: [ Icon( Icons.lock_person, - color: Theme.of(context).colorScheme.secondary, + color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 12), Expanded( @@ -803,7 +803,7 @@ class _SettingsPageState extends State { Icon( Icons.send_rounded, size: 28, - color: Theme.of(context).colorScheme.secondary, + color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 16), Expanded( @@ -854,7 +854,7 @@ class _SettingsPageState extends State { Icon( Icons.download_for_offline_rounded, size: 24, - color: Theme.of(context).colorScheme.secondary, + color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 12), Expanded( @@ -891,8 +891,7 @@ class _SettingsPageState extends State { await DocMan.pick.directory(); selectedPath = selectedDirectory?.uri; } else { - selectedPath = - await FilePicker.platform.getDirectoryPath(); + selectedPath = await FilePicker.getDirectoryPath(); } if (selectedPath == null) { @@ -945,7 +944,7 @@ class _SettingsPageState extends State { Icon( Icons.info_outline, size: 28, - color: Theme.of(context).colorScheme.secondary, + color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 16), Expanded( @@ -1007,7 +1006,7 @@ class _SettingsPageState extends State { Icon( Icons.language, size: 28, - color: Theme.of(context).colorScheme.secondary, + color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 16), Expanded( @@ -1085,7 +1084,7 @@ class _SettingsPageState extends State { Icon( Icons.coffee, size: 28, - color: Theme.of(context).colorScheme.secondary, + color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 16), @@ -1115,7 +1114,7 @@ class _SettingsPageState extends State { Icon( Icons.send, size: 28, - color: Theme.of(context).colorScheme.secondary, + color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 16), Expanded( @@ -1153,7 +1152,7 @@ class _SettingsPageState extends State { Icon( Icons.visibility_rounded, size: 28, - color: Theme.of(context).colorScheme.secondary, + color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 16), Expanded( @@ -1222,7 +1221,7 @@ class _SettingsPageState extends State { Icon( Icons.tune_rounded, size: 24, - color: Theme.of(context).colorScheme.secondary, + color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 12), Text( @@ -1277,7 +1276,7 @@ class _SettingsPageState extends State { contentPadding: const EdgeInsets.symmetric(horizontal: 12), leading: Icon( _bookActionIcon(action), - color: Theme.of(context).colorScheme.secondary, + color: Theme.of(context).colorScheme.primary, ), title: Text(_bookActionTitle(action, localizations)), trailing: Row( @@ -1383,7 +1382,7 @@ class _SettingsPageState extends State { Icon( Icons.view_list_rounded, size: 24, - color: Theme.of(context).colorScheme.secondary, + color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 12), Text( @@ -1444,7 +1443,7 @@ class _SettingsPageState extends State { contentPadding: const EdgeInsets.symmetric(horizontal: 12), leading: Icon( _bookSectionIcon(section), - color: Theme.of(context).colorScheme.secondary, + color: Theme.of(context).colorScheme.primary, ), title: Text(_bookSectionTitle(section, localizations)), trailing: Row( @@ -1597,7 +1596,7 @@ class _SettingsPageState extends State { keyValue: sectionKey, leading: Icon( _discoverMainSectionIcon(section), - color: Theme.of(context).colorScheme.secondary, + color: Theme.of(context).colorScheme.primary, ), title: _discoverMainSectionTitle(section, localizations), value: enabledSections.contains(sectionKey), @@ -1652,7 +1651,7 @@ class _SettingsPageState extends State { keyValue: itemKey, leading: Icon( _discoverItemIcon(item), - color: Theme.of(context).colorScheme.secondary, + color: Theme.of(context).colorScheme.primary, ), title: _discoverItemTitle(item, localizations), value: enabledItems.contains(itemKey), @@ -1704,7 +1703,7 @@ class _SettingsPageState extends State { keyValue: itemKey, leading: Icon( _categoryItemIcon(item), - color: Theme.of(context).colorScheme.secondary, + color: Theme.of(context).colorScheme.primary, ), title: _categoryItemTitle(item, localizations), value: enabledItems.contains(itemKey), @@ -1733,11 +1732,7 @@ class _SettingsPageState extends State { children: [ Row( children: [ - Icon( - icon, - size: 24, - color: Theme.of(context).colorScheme.secondary, - ), + Icon(icon, size: 24, color: Theme.of(context).colorScheme.primary), const SizedBox(width: 12), Text(title, style: Theme.of(context).textTheme.titleMedium), if (onReset != null) ...[ @@ -1759,6 +1754,7 @@ class _SettingsPageState extends State { (child, index, animation) => Material(type: MaterialType.transparency, child: child), itemCount: itemCount, + // ignore: deprecated_member_use onReorder: onReorder, itemBuilder: (context, index) => tileBuilder(index), ), @@ -1920,7 +1916,7 @@ class _SettingsPageState extends State { Icon( Icons.description_outlined, size: 28, - color: Theme.of(context).colorScheme.secondary, + color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 16), Expanded( @@ -1963,7 +1959,7 @@ class _SettingsPageState extends State { Icon( Icons.bug_report_rounded, size: 28, - color: Theme.of(context).colorScheme.secondary, + color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 16), Expanded( @@ -2013,7 +2009,7 @@ class _SettingsPageState extends State { children: [ Icon( Icons.cloud_sync_rounded, - color: Theme.of(context).colorScheme.secondary, + color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 16), Expanded( diff --git a/lib/features/settings/presentation/widgets/download_options_widget.dart b/lib/features/settings/presentation/widgets/download_options_widget.dart index 6665901..e5bc5de 100644 --- a/lib/features/settings/presentation/widgets/download_options_widget.dart +++ b/lib/features/settings/presentation/widgets/download_options_widget.dart @@ -69,7 +69,8 @@ class DownloadOptionsWidget extends StatelessWidget { const SizedBox(width: 16), ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.primary, + backgroundColor: + Theme.of(context).colorScheme.primaryContainer, ), onPressed: () async { String? selectedPath; @@ -79,8 +80,7 @@ class DownloadOptionsWidget extends StatelessWidget { await DocMan.pick.directory(); selectedPath = selectedDirectory?.uri; } else { - selectedPath = - await FilePicker.platform.getDirectoryPath(); + selectedPath = await FilePicker.getDirectoryPath(); } if (selectedPath == null) { @@ -109,7 +109,7 @@ class DownloadOptionsWidget extends StatelessWidget { child: Text( localizations.select, style: TextStyle( - color: Theme.of(context).colorScheme.onPrimary, + color: Theme.of(context).colorScheme.onPrimaryContainer, ), ), ), @@ -174,7 +174,8 @@ class DownloadOptionsWidget extends StatelessWidget { const SizedBox(width: 16), ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.primary, + backgroundColor: + Theme.of(context).colorScheme.primaryContainer, ), onPressed: () async { final result = await _showSchemaSelectionDialog( @@ -198,7 +199,7 @@ class DownloadOptionsWidget extends StatelessWidget { child: Text( localizations.select, style: TextStyle( - color: Theme.of(context).colorScheme.onPrimary, + color: Theme.of(context).colorScheme.onPrimaryContainer, ), ), ), diff --git a/lib/features/settings/presentation/widgets/feedback_widget.dart b/lib/features/settings/presentation/widgets/feedback_widget.dart index 9bae8aa..342b604 100644 --- a/lib/features/settings/presentation/widgets/feedback_widget.dart +++ b/lib/features/settings/presentation/widgets/feedback_widget.dart @@ -38,7 +38,7 @@ class FeedbackWidget extends StatelessWidget { Icon( Icons.bug_report_outlined, size: 28, - color: Theme.of(context).colorScheme.secondary, + color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 16), Expanded( diff --git a/lib/features/settings/presentation/widgets/sync_filter_bottom_sheet.dart b/lib/features/settings/presentation/widgets/sync_filter_bottom_sheet.dart index 6954a93..5c551a6 100644 --- a/lib/features/settings/presentation/widgets/sync_filter_bottom_sheet.dart +++ b/lib/features/settings/presentation/widgets/sync_filter_bottom_sheet.dart @@ -354,12 +354,10 @@ class _SyncFilterBottomSheetState extends State { elevation: 0, color: Theme.of(context).colorScheme.surfaceContainer, child: ListTile( - leading: Icon(icon, color: Theme.of(context).colorScheme.secondary), + leading: Icon(icon, color: Theme.of(context).colorScheme.primary), title: Text(title), subtitle: Text( - count > 0 - ? "$count ${localization.selected}" - : localization.all, + count > 0 ? "$count ${localization.selected}" : localization.all, ), trailing: const Icon(Icons.arrow_forward_ios, size: 16), onTap: onTap, diff --git a/lib/features/settings/presentation/widgets/sync_settings_widget.dart b/lib/features/settings/presentation/widgets/sync_settings_widget.dart index 6f40633..ec5cb98 100644 --- a/lib/features/settings/presentation/widgets/sync_settings_widget.dart +++ b/lib/features/settings/presentation/widgets/sync_settings_widget.dart @@ -135,7 +135,7 @@ class _SyncSettingsWidgetState extends State { width: double.infinity, child: ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.primary, + backgroundColor: Theme.of(context).colorScheme.primaryContainer, ), onPressed: () => _openConfigurationSheet(context, state), child: Row( @@ -143,13 +143,13 @@ class _SyncSettingsWidgetState extends State { children: [ Icon( Icons.sync, - color: Theme.of(context).colorScheme.onPrimary, + color: Theme.of(context).colorScheme.onPrimaryContainer, ), SizedBox(width: 8), Text( localization.configureAndSync, style: TextStyle( - color: Theme.of(context).colorScheme.onPrimary, + color: Theme.of(context).colorScheme.onPrimaryContainer, ), ), ], diff --git a/lib/features/settings/presentation/widgets/theme_selector_widget.dart b/lib/features/settings/presentation/widgets/theme_selector_widget.dart index d1d661b..b6f1824 100644 --- a/lib/features/settings/presentation/widgets/theme_selector_widget.dart +++ b/lib/features/settings/presentation/widgets/theme_selector_widget.dart @@ -43,7 +43,7 @@ class ThemeSelectorWidget extends StatelessWidget { Icon( Icons.dark_mode, size: 28, - color: Theme.of(context).colorScheme.secondary, + color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 16), Expanded( @@ -107,7 +107,7 @@ class ThemeSelectorWidget extends StatelessWidget { decoration: BoxDecoration( color: isSelected - ? Theme.of(context).colorScheme.secondaryContainer + ? Theme.of(context).colorScheme.primaryContainer : Colors.transparent, borderRadius: BorderRadius.circular(8), ), @@ -117,7 +117,7 @@ class ThemeSelectorWidget extends StatelessWidget { icon, color: isSelected - ? Theme.of(context).colorScheme.secondary + ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.onSurface, ), const SizedBox(height: 8), @@ -126,7 +126,7 @@ class ThemeSelectorWidget extends StatelessWidget { style: TextStyle( color: isSelected - ? Theme.of(context).colorScheme.secondary + ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.onSurface, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, ), @@ -164,7 +164,7 @@ class ThemeSelectorWidget extends StatelessWidget { Icon( Icons.palette, size: 28, - color: Theme.of(context).colorScheme.secondary, + color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 16), Expanded( diff --git a/packages/epubx/CHANGELOG.md b/packages/epubx/CHANGELOG.md new file mode 100644 index 0000000..63c3d9a --- /dev/null +++ b/packages/epubx/CHANGELOG.md @@ -0,0 +1,148 @@ +## 4.0.0 + +- Merge all pull requests + +## 3.0.0 +### Changed +- `metadata` file now saves as `mimetype` [pull#1](https://github.com/rbcprolabs/epubx.dart/pull/1) +### Added +- Epub v3 support [dart-epub | pull#76](https://github.com/orthros/dart-epub/pull/76) +- Doc comment [dart-epub | pull#80](https://github.com/orthros/dart-epub/pull/80) + +## 3.0.0-dev.3 +### Changed +- At `EpubReader.{openBook, readBook}` first argument can be future (not before) + +## 3.0.0-dev.2 +### Fixed +- Fixed null-safety bug + +## 3.0.0-dev.1 +### Added +- Null-safety migration +### Changed +- Upgrade all dependencies + +## 2.1.0 +### Fixed +- Version 3 EPUB's can have a null Table of Contents +- Updated `pedantic` analysis options + +## 2.0.7 +### Added +- Added example of using `epub` in a web page: `examples/web_ex` +### Fixed +- Fixed errors from pedantic analysis +### Changed +- Added pedantic analysis options + +## 2.0.6 +### Fixed +- Fixed Issue #35: File cannot be opened if its path is url-encoded in the manifest +- Updated `examples/dart_ex` to have a README as well as use a locally stored file. + +## 2.0.5 +### Changed +- Exposed `EpubChapterRef` to consumers. + +## 2.0.4 +### Fixed +- Merged pull request #45 + - Fixes pana hits to make code more readable + +## 2.0.3 +### Changed +- Raised `sdk` version constraint to 2.0.0 +- Raised constraint on `async` to 3.0.0 +### Fixed +- Merged pull request #40 by vblago. + - Fixes Undefined class 'XmlBuilder' + +## 2.0.2 +### Changed +- Lowered sdk version constraint to 2.0.0-dev.61.0 + +## 2.0.1 +### Changed +- Formatted documents + +## 2.0.0 +### Added +- Added support for writing Epubs back to Byte Arrays +- Tests for writing Epubs + +### Changed +- Epub Readers and Writers now have their == operator and hashCode get-er overridden + +### Fixed +- Fixed an issue when reading EpubContentFileRef + +## 1.3.2 +### Changed +- Updates to Travis configuration and publishing + +## 1.3.1 +### Changed +- Updates to Travis configuration and publishing +### Removed +- Removed unused variable `FilePath` from `EpubBook` and `EpubBookRef` + +## 1.3.0 +### Added +- Package now supports Dart 2! +### Removed +- Removed support for Dart 1.2.21 + +## 1.2.10 +### Fixed +- Merged pull request #15 from ShadowJonathan/dev. + - Fixes issue with parsing schema by removing `opf:` namespace + +## 1.2.9 +### Changed +- Ran code through `dartfmt` as per analysis by `pana` + +## 1.2.8 +### Added +- Added unit tests for Images +### Changed +- Updated dependencies + +## 1.2.7 +### Added +- Added upper limit of Dart version to 2.0.1 + +## 1.2.6 +### Added +- Added Support for Dart 2.0 + +## 1.2.5 +### Added +- A publish step in the travis deploy + +## 1.2.4 +### Changed +- EnumFromString no longer uses the `mirrors` package to make this Flutter compatible by @MostafaAyesh + +## 1.2.3 +### Added +- This Changelog! + +### Changed +- Author email + +## 1.2.2 +### Changed +- Dependencies were updated to more permissive versions by @jarontai + +### Added +- Example by @jarontai +- More Entities and types are exported by @jarontai + +### Fixed +- Issue with case sensitivity in switch statements from @jarontai +- Issue with Async Loops from @jarontai + +## 1.2.1 +### Fixed +- Made code in line with Dart styleguide diff --git a/packages/epubx/CODE_OF_CONDUCT.md b/packages/epubx/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..71f7a43 --- /dev/null +++ b/packages/epubx/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at colin@ifdevthentalk.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/packages/epubx/LICENSE b/packages/epubx/LICENSE new file mode 100644 index 0000000..d9ec56f --- /dev/null +++ b/packages/epubx/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Colin Nelson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/epubx/README.md b/packages/epubx/README.md new file mode 100644 index 0000000..42fc905 --- /dev/null +++ b/packages/epubx/README.md @@ -0,0 +1,134 @@ +# epubx + +It package is [dart-epub](https://github.com/orthros/dart-epub) fork + +[Flutter UI implementation](https://pub.dev/packages/epub_view) + +Epub Reader and Writer for Dart inspired by [this fantastic C# Epub Reader](https://github.com/versfx/EpubReader) + +This does not rely on the ```dart:io``` package in any way, so it is avilable for both desktop and web-based implementations + +[![pub package](https://img.shields.io/pub/v/epubx.svg)](https://pub.dartlang.org/packages/epubx) +## Installing +Add the package to the ```dependencies``` section of your pubspec.yaml +``` +dependencies: + epubx: any +``` + +## Example +```dart + +//Get the epub into memory somehow +String fileName = 'sample.epub'; +String fullPath = path.join(io.Directory.current.path, fileName); +var targetFile = new io.File(fullPath); +List bytes = await targetFile.readAsBytes(); + + +// Opens a book and reads all of its content into memory +EpubBook epubBook = await EpubReader.readBook(bytes); + +// COMMON PROPERTIES + +// Book's title +String title = epubBook.Title; + +// Book's authors (comma separated list) +String author = epubBook.Author; + +// Book's authors (list of authors names) +List authors = epubBook.AuthorList; + +// Book's cover image (null if there is no cover) +Image coverImage = epubBook.CoverImage; + + +// CHAPTERS + +// Enumerating chapters +epubBook.Chapters.forEach((EpubChapter chapter) { + // Title of chapter + String chapterTitle = chapter.Title; + + // HTML content of current chapter + String chapterHtmlContent = chapter.HtmlContent; + + // Nested chapters + List subChapters = chapter.SubChapters; +}); + + +// CONTENT + +// Book's content (HTML files, stylesheets, images, fonts, etc.) +EpubContent bookContent = epubBook.Content; + + +// IMAGES + +// All images in the book (file name is the key) +Map images = bookContent.Images; + +EpubByteContentFile firstImage = images.values.first; + +// Content type (e.g. EpubContentType.IMAGE_JPEG, EpubContentType.IMAGE_PNG) +EpubContentType contentType = firstImage.ContentType; + +// MIME type (e.g. "image/jpeg", "image/png") +String mimeContentType = firstImage.ContentMimeType; + +// HTML & CSS + +// All XHTML files in the book (file name is the key) +Map htmlFiles = bookContent.Html; + +// All CSS files in the book (file name is the key) +Map cssFiles = bookContent.Css; + +// Entire HTML content of the book +htmlFiles.values.forEach((EpubTextContentFile htmlFile) { + String htmlContent = htmlFile.Content; +}); + +// All CSS content in the book +cssFiles.values.forEach((EpubTextContentFile cssFile){ + String cssContent = cssFile.Content; +}); + + +// OTHER CONTENT + +// All fonts in the book (file name is the key) +Map fonts = bookContent.Fonts; + +// All files in the book (including HTML, CSS, images, fonts, and other types of files) +Map allFiles = bookContent.AllFiles; + + +// ACCESSING RAW SCHEMA INFORMATION + +// EPUB OPF data +EpubPackage package = epubBook.Schema.Package; + +// Enumerating book's contributors +package.Metadata.Contributors.forEach((EpubMetadataContributor contributor){ + String contributorName = contributor.Contributor; + String contributorRole = contributor.Role; +}); + +// EPUB NCX data +EpubNavigation navigation = epubBook.Schema.Navigation; + +// Enumerating NCX metadata +navigation.Head.Metadata.forEach((EpubNavigationHeadMeta meta){ + String metadataItemName = meta.Name; + String metadataItemContent = meta.Content; +}); + +// Writing Data +var written = await EpubWriter.writeBook(epubBook); + +// You can even re-read the book into a new object! +var bookRoundTrip = await EpubReader.readBook(written); +``` \ No newline at end of file diff --git a/packages/epubx/analysis_options.yaml b/packages/epubx/analysis_options.yaml new file mode 100644 index 0000000..1728f8d --- /dev/null +++ b/packages/epubx/analysis_options.yaml @@ -0,0 +1,15 @@ +# Defines a default set of lint rules enforced for +# projects at Google. For details and rationale, +# see https://github.com/dart-lang/pedantic#enabled-lints. +include: package:pedantic/analysis_options.yaml + +# For lint rules and documentation, see http://dart-lang.github.io/linter/lints. +# Uncomment to specify additional rules. +# linter: +# rules: +# - camel_case_types + +analyzer: + exclude: + - example/** + - test/** diff --git a/packages/epubx/lib/epubx.dart b/packages/epubx/lib/epubx.dart new file mode 100644 index 0000000..c5127f2 --- /dev/null +++ b/packages/epubx/lib/epubx.dart @@ -0,0 +1,36 @@ +library epubx; + +export 'src/utils/enum_from_string.dart'; + +export 'src/epub_reader.dart'; +export 'src/epub_writer.dart'; +export 'src/ref_entities/epub_book_ref.dart'; +export 'src/ref_entities/epub_chapter_ref.dart'; +export 'src/entities/epub_book.dart'; +export 'src/entities/epub_chapter.dart'; +export 'src/entities/epub_content.dart'; +export 'src/entities/epub_content_type.dart'; +export 'src/entities/epub_byte_content_file.dart'; +export 'src/entities/epub_content_file.dart'; +export 'src/entities/epub_text_content_file.dart'; +export 'src/entities/epub_schema.dart'; +export 'src/schema/opf/epub_guide.dart'; +export 'src/schema/opf/epub_guide_reference.dart'; +export 'src/schema/opf/epub_spine.dart'; +export 'src/schema/opf/epub_spine_item_ref.dart'; +export 'src/schema/opf/epub_manifest.dart'; +export 'src/schema/opf/epub_manifest_item.dart'; +export 'src/schema/opf/epub_metadata.dart'; +export 'src/schema/opf/epub_metadata_creator.dart'; +export 'src/schema/opf/epub_package.dart'; +export 'src/schema/opf/epub_version.dart'; +export 'src/schema/navigation/epub_metadata.dart'; +export 'src/schema/navigation/epub_navigation.dart'; +export 'src/schema/navigation/epub_navigation_head.dart'; +export 'src/schema/navigation/epub_navigation_doc_author.dart'; +export 'src/schema/navigation/epub_navigation_doc_title.dart'; +export 'src/schema/navigation/epub_navigation_head_meta.dart'; +export 'src/schema/navigation/epub_navigation_label.dart'; +export 'src/schema/navigation/epub_navigation_map.dart'; +export 'src/schema/navigation/epub_navigation_point.dart'; +export 'package:image/image.dart' show Image; diff --git a/packages/epubx/lib/src/entities/epub_book.dart b/packages/epubx/lib/src/entities/epub_book.dart new file mode 100644 index 0000000..22872b6 --- /dev/null +++ b/packages/epubx/lib/src/entities/epub_book.dart @@ -0,0 +1,47 @@ +import 'package:image/image.dart'; +import 'package:quiver/collection.dart' as collections; +import 'package:quiver/core.dart'; + +import 'epub_chapter.dart'; +import 'epub_content.dart'; +import 'epub_schema.dart'; + +class EpubBook { + String? Title; + String? Author; + List? AuthorList; + EpubSchema? Schema; + EpubContent? Content; + Image? CoverImage; + List? Chapters; + + @override + int get hashCode { + var objects = [ + Title.hashCode, + Author.hashCode, + Schema.hashCode, + Content.hashCode, + ...CoverImage?.getBytes().map((byte) => byte.hashCode) ?? [0], + ...AuthorList?.map((author) => author.hashCode) ?? [0], + ...Chapters?.map((chapter) => chapter.hashCode) ?? [0], + ]; + return hashObjects(objects); + } + + @override + bool operator ==(other) { + if (!(other is EpubBook)) { + return false; + } + + return Title == other.Title && + Author == other.Author && + collections.listsEqual(AuthorList, other.AuthorList) && + Schema == other.Schema && + Content == other.Content && + collections.listsEqual( + CoverImage!.getBytes(), other.CoverImage!.getBytes()) && + collections.listsEqual(Chapters, other.Chapters); + } +} diff --git a/packages/epubx/lib/src/entities/epub_byte_content_file.dart b/packages/epubx/lib/src/entities/epub_byte_content_file.dart new file mode 100644 index 0000000..8bf868f --- /dev/null +++ b/packages/epubx/lib/src/entities/epub_byte_content_file.dart @@ -0,0 +1,30 @@ +import 'package:quiver/collection.dart' as collections; +import 'package:quiver/core.dart'; + +import 'epub_content_file.dart'; + +class EpubByteContentFile extends EpubContentFile { + List? Content; + + @override + int get hashCode { + var objects = [ + ContentMimeType.hashCode, + ContentType.hashCode, + FileName.hashCode, + ...Content?.map((content) => content.hashCode) ?? [0], + ]; + return hashObjects(objects); + } + + @override + bool operator ==(other) { + if (!(other is EpubByteContentFile)) { + return false; + } + return collections.listsEqual(Content, other.Content) && + ContentMimeType == other.ContentMimeType && + ContentType == other.ContentType && + FileName == other.FileName; + } +} diff --git a/packages/epubx/lib/src/entities/epub_chapter.dart b/packages/epubx/lib/src/entities/epub_chapter.dart new file mode 100644 index 0000000..1acfc37 --- /dev/null +++ b/packages/epubx/lib/src/entities/epub_chapter.dart @@ -0,0 +1,39 @@ +import 'package:quiver/collection.dart' as collections; +import 'package:quiver/core.dart'; + +class EpubChapter { + String? Title; + String? ContentFileName; + String? Anchor; + String? HtmlContent; + List? SubChapters; + + @override + int get hashCode { + var objects = [ + Title.hashCode, + ContentFileName.hashCode, + Anchor.hashCode, + HtmlContent.hashCode, + ...SubChapters?.map((subChapter) => subChapter.hashCode) ?? [0], + ]; + return hashObjects(objects); + } + + @override + bool operator ==(other) { + if (!(other is EpubChapter)) { + return false; + } + return Title == other.Title && + ContentFileName == other.ContentFileName && + Anchor == other.Anchor && + HtmlContent == other.HtmlContent && + collections.listsEqual(SubChapters, other.SubChapters); + } + + @override + String toString() { + return 'Title: $Title, Subchapter count: ${SubChapters!.length}'; + } +} diff --git a/packages/epubx/lib/src/entities/epub_content.dart b/packages/epubx/lib/src/entities/epub_content.dart new file mode 100644 index 0000000..e30b22d --- /dev/null +++ b/packages/epubx/lib/src/entities/epub_content.dart @@ -0,0 +1,52 @@ +import 'package:quiver/collection.dart' as collections; +import 'package:quiver/core.dart'; + +import 'epub_byte_content_file.dart'; +import 'epub_content_file.dart'; +import 'epub_text_content_file.dart'; + +class EpubContent { + Map? Html; + Map? Css; + Map? Images; + Map? Fonts; + Map? AllFiles; + + EpubContent() { + Html = {}; + Css = {}; + Images = {}; + Fonts = {}; + AllFiles = {}; + } + + @override + int get hashCode { + var objects = [ + ...Html!.keys.map((key) => key.hashCode), + ...Html!.values.map((value) => value.hashCode), + ...Css!.keys.map((key) => key.hashCode), + ...Css!.values.map((value) => value.hashCode), + ...Images!.keys.map((key) => key.hashCode), + ...Images!.values.map((value) => value.hashCode), + ...Fonts!.keys.map((key) => key.hashCode), + ...Fonts!.values.map((value) => value.hashCode), + ...AllFiles!.keys.map((key) => key.hashCode), + ...AllFiles!.values.map((value) => value.hashCode), + ]; + + return hashObjects(objects); + } + + @override + bool operator ==(other) { + if (!(other is EpubContent)) { + return false; + } + return collections.mapsEqual(Html, other.Html) && + collections.mapsEqual(Css, other.Css) && + collections.mapsEqual(Images, other.Images) && + collections.mapsEqual(Fonts, other.Fonts) && + collections.mapsEqual(AllFiles, other.AllFiles); + } +} diff --git a/packages/epubx/lib/src/entities/epub_content_file.dart b/packages/epubx/lib/src/entities/epub_content_file.dart new file mode 100644 index 0000000..d43801c --- /dev/null +++ b/packages/epubx/lib/src/entities/epub_content_file.dart @@ -0,0 +1,23 @@ +import 'package:quiver/core.dart'; + +import 'epub_content_type.dart'; + +abstract class EpubContentFile { + String? FileName; + EpubContentType? ContentType; + String? ContentMimeType; + + @override + int get hashCode => + hash3(FileName.hashCode, ContentType.hashCode, ContentMimeType.hashCode); + + @override + bool operator ==(other) { + if (!(other is EpubContentFile)) { + return false; + } + return FileName == other.FileName && + ContentType == other.ContentType && + ContentMimeType == other.ContentMimeType; + } +} diff --git a/packages/epubx/lib/src/entities/epub_content_type.dart b/packages/epubx/lib/src/entities/epub_content_type.dart new file mode 100644 index 0000000..b1c0e07 --- /dev/null +++ b/packages/epubx/lib/src/entities/epub_content_type.dart @@ -0,0 +1,17 @@ +enum EpubContentType { + XHTML_1_1, + DTBOOK, + DTBOOK_NCX, + OEB1_DOCUMENT, + XML, + CSS, + OEB1_CSS, + IMAGE_GIF, + IMAGE_JPEG, + IMAGE_PNG, + IMAGE_SVG, + IMAGE_BMP, + FONT_TRUETYPE, + FONT_OPENTYPE, + OTHER +} diff --git a/packages/epubx/lib/src/entities/epub_schema.dart b/packages/epubx/lib/src/entities/epub_schema.dart new file mode 100644 index 0000000..c34d190 --- /dev/null +++ b/packages/epubx/lib/src/entities/epub_schema.dart @@ -0,0 +1,25 @@ +import 'package:quiver/core.dart'; + +import '../schema/navigation/epub_navigation.dart'; +import '../schema/opf/epub_package.dart'; + +class EpubSchema { + EpubPackage? Package; + EpubNavigation? Navigation; + String? ContentDirectoryPath; + + @override + int get hashCode => hash3( + Package.hashCode, Navigation.hashCode, ContentDirectoryPath.hashCode); + + @override + bool operator ==(other) { + if (!(other is EpubSchema)) { + return false; + } + + return Package == other.Package && + Navigation == other.Navigation && + ContentDirectoryPath == other.ContentDirectoryPath; + } +} diff --git a/packages/epubx/lib/src/entities/epub_text_content_file.dart b/packages/epubx/lib/src/entities/epub_text_content_file.dart new file mode 100644 index 0000000..a378074 --- /dev/null +++ b/packages/epubx/lib/src/entities/epub_text_content_file.dart @@ -0,0 +1,22 @@ +import 'package:quiver/core.dart'; + +import 'epub_content_file.dart'; + +class EpubTextContentFile extends EpubContentFile { + String? Content; + + @override + int get hashCode => hash4(Content, ContentMimeType, ContentType, FileName); + + @override + bool operator ==(other) { + if (!(other is EpubTextContentFile)) { + return false; + } + + return Content == other.Content && + ContentMimeType == other.ContentMimeType && + ContentType == other.ContentType && + FileName == other.FileName; + } +} diff --git a/packages/epubx/lib/src/epub_reader.dart b/packages/epubx/lib/src/epub_reader.dart new file mode 100644 index 0000000..4c86203 --- /dev/null +++ b/packages/epubx/lib/src/epub_reader.dart @@ -0,0 +1,208 @@ +import 'dart:async'; + +import 'package:archive/archive.dart'; + +import 'entities/epub_book.dart'; +import 'entities/epub_byte_content_file.dart'; +import 'entities/epub_chapter.dart'; +import 'entities/epub_content.dart'; +import 'entities/epub_content_file.dart'; +import 'entities/epub_text_content_file.dart'; +import 'readers/content_reader.dart'; +import 'readers/schema_reader.dart'; +import 'ref_entities/epub_book_ref.dart'; +import 'ref_entities/epub_byte_content_file_ref.dart'; +import 'ref_entities/epub_chapter_ref.dart'; +import 'ref_entities/epub_content_file_ref.dart'; +import 'ref_entities/epub_content_ref.dart'; +import 'ref_entities/epub_text_content_file_ref.dart'; +import 'schema/opf/epub_metadata_creator.dart'; + +/// A class that provides the primary interface to read Epub files. +/// +/// To open an Epub and load all data at once use the [readBook()] method. +/// +/// To open an Epub and load only basic metadata use the [openBook()] method. +/// This is a good option to quickly load text-based metadata, while leaving the +/// heavier lifting of loading images and main content for subsequent operations. +/// +/// ## Example +/// ```dart +/// // Read the basic metadata. +/// EpubBookRef epub = await EpubReader.openBook(epubFileBytes); +/// // Extract values of interest. +/// String title = epub.Title; +/// String author = epub.Author; +/// var metadata = epub.Schema.Package.Metadata; +/// String genres = metadata.Subjects.join(', '); +/// ``` +class EpubReader { + /// Loads basics metadata. + /// + /// Opens the book asynchronously without reading its main content. + /// Holds the handle to the EPUB file. + /// + /// Argument [bytes] should be the bytes of + /// the epub file you have loaded with something like the [dart:io] package's + /// [readAsBytes()]. + /// + /// This is a fast and convenient way to get the most important information + /// about the book, notably the [Title], [Author] and [AuthorList]. + /// Additional information is loaded in the [Schema] property such as the + /// Epub version, Publishers, Languages and more. + static Future openBook(FutureOr> bytes) async { + List loadedBytes; + if (bytes is Future) { + loadedBytes = await bytes; + } else { + loadedBytes = bytes; + } + + var epubArchive = ZipDecoder().decodeBytes(loadedBytes); + + var bookRef = EpubBookRef(epubArchive); + bookRef.Schema = await SchemaReader.readSchema(epubArchive); + bookRef.Title = bookRef.Schema!.Package!.Metadata!.Titles! + .firstWhere((String name) => true, orElse: () => ''); + bookRef.AuthorList = bookRef.Schema!.Package!.Metadata!.Creators! + .map((EpubMetadataCreator creator) => creator.Creator) + .toList(); + bookRef.Author = bookRef.AuthorList!.join(', '); + bookRef.Content = ContentReader.parseContentMap(bookRef); + return bookRef; + } + + /// Opens the book asynchronously and reads all of its content into the memory. Does not hold the handle to the EPUB file. + static Future readBook(FutureOr> bytes) async { + var result = EpubBook(); + List loadedBytes; + if (bytes is Future) { + loadedBytes = await bytes; + } else { + loadedBytes = bytes; + } + + var epubBookRef = await openBook(loadedBytes); + result.Schema = epubBookRef.Schema; + result.Title = epubBookRef.Title; + result.AuthorList = epubBookRef.AuthorList; + result.Author = epubBookRef.Author; + result.Content = await readContent(epubBookRef.Content!); + // Non-fatal: a missing/declared-but-absent cover shouldn't fail the book. + try { + result.CoverImage = await epubBookRef.readCover(); + } catch (_) { + result.CoverImage = null; + } + // Chapter extraction walks the navigation map and throws if a nav entry + // points at a missing content file. Keep it non-fatal — an empty chapter + // list lets consumers fall back to spine order instead of failing the whole + // book. (calibre-web-companion patch.) + var chapterRefs = []; + try { + chapterRefs = await epubBookRef.getChapters(); + } catch (_) { + chapterRefs = []; + } + result.Chapters = await readChapters(chapterRefs); + + return result; + } + + static Future readContent(EpubContentRef contentRef) async { + var result = EpubContent(); + result.Html = await readTextContentFiles(contentRef.Html!); + result.Css = await readTextContentFiles(contentRef.Css!); + result.Images = await readByteContentFiles(contentRef.Images!); + result.Fonts = await readByteContentFiles(contentRef.Fonts!); + result.AllFiles = {}; + + result.Html!.forEach((String? key, EpubTextContentFile value) { + result.AllFiles![key!] = value; + }); + result.Css!.forEach((String? key, EpubTextContentFile value) { + result.AllFiles![key!] = value; + }); + + result.Images!.forEach((String? key, EpubByteContentFile value) { + result.AllFiles![key!] = value; + }); + result.Fonts!.forEach((String? key, EpubByteContentFile value) { + result.AllFiles![key!] = value; + }); + + await Future.forEach(contentRef.AllFiles!.keys, (dynamic key) async { + if (!result.AllFiles!.containsKey(key)) { + // Non-fatal: skip unreadable/missing files (see readTextContentFiles). + try { + result.AllFiles![key] = + await readByteContentFile(contentRef.AllFiles![key]!); + } catch (_) {} + } + }); + + return result; + } + + static Future> readTextContentFiles( + Map textContentFileRefs) async { + var result = {}; + + await Future.forEach(textContentFileRefs.keys, (dynamic key) async { + EpubContentFileRef value = textContentFileRefs[key]!; + var textContentFile = EpubTextContentFile(); + textContentFile.FileName = value.FileName; + textContentFile.ContentType = value.ContentType; + textContentFile.ContentMimeType = value.ContentMimeType; + // Non-fatal: a manifest may list a file that isn't actually in the + // archive. Skip it instead of failing the whole book — the reader falls + // back to whatever content is present. (calibre-web-companion patch.) + try { + textContentFile.Content = await value.readContentAsText(); + result[key] = textContentFile; + } catch (_) {} + }); + return result; + } + + static Future> readByteContentFiles( + Map byteContentFileRefs) async { + var result = {}; + await Future.forEach(byteContentFileRefs.keys, (dynamic key) async { + // Non-fatal: skip files that can't be read (see readTextContentFiles). + try { + result[key] = await readByteContentFile(byteContentFileRefs[key]!); + } catch (_) {} + }); + return result; + } + + static Future readByteContentFile( + EpubContentFileRef contentFileRef) async { + var result = EpubByteContentFile(); + + result.FileName = contentFileRef.FileName; + result.ContentType = contentFileRef.ContentType; + result.ContentMimeType = contentFileRef.ContentMimeType; + result.Content = await contentFileRef.readContentAsBytes(); + + return result; + } + + static Future> readChapters( + List chapterRefs) async { + var result = []; + await Future.forEach(chapterRefs, (EpubChapterRef chapterRef) async { + var chapter = EpubChapter(); + + chapter.Title = chapterRef.Title; + chapter.ContentFileName = chapterRef.ContentFileName; + chapter.Anchor = chapterRef.Anchor; + chapter.HtmlContent = await chapterRef.readHtmlContent(); + chapter.SubChapters = await readChapters(chapterRef.SubChapters!); + + result.add(chapter); + }); + return result; + } +} diff --git a/packages/epubx/lib/src/epub_writer.dart b/packages/epubx/lib/src/epub_writer.dart new file mode 100644 index 0000000..7c9caa5 --- /dev/null +++ b/packages/epubx/lib/src/epub_writer.dart @@ -0,0 +1,59 @@ +import 'package:archive/archive.dart'; +import 'dart:convert' as convert; +import 'package:epubx/src/utils/zip_path_utils.dart'; +import 'package:epubx/src/writers/epub_package_writer.dart'; + +import 'entities/epub_book.dart'; +import 'entities/epub_byte_content_file.dart'; +import 'entities/epub_text_content_file.dart'; + +class EpubWriter { + static const _container_file = + ''; + + // Creates a Zip Archive of an EpubBook + static Archive _createArchive(EpubBook book) { + var arch = Archive(); + + // Add simple metadata + arch.addFile(ArchiveFile.noCompress( + 'mimetype', 20, convert.utf8.encode('application/epub+zip'))); + + // Add Container file + arch.addFile(ArchiveFile('META-INF/container.xml', _container_file.length, + convert.utf8.encode(_container_file))); + + // Add all content to the archive + book.Content!.AllFiles!.forEach((name, file) { + List? content; + + if (file is EpubByteContentFile) { + content = file.Content; + } else if (file is EpubTextContentFile) { + content = convert.utf8.encode(file.Content!); + } + + arch.addFile(ArchiveFile( + ZipPathUtils.combine(book.Schema!.ContentDirectoryPath, name)!, + content!.length, + content)); + }); + + // Generate the content.opf file and add it to the Archive + var contentopf = EpubPackageWriter.writeContent(book.Schema!.Package!); + + arch.addFile(ArchiveFile( + ZipPathUtils.combine(book.Schema!.ContentDirectoryPath, 'content.opf')!, + contentopf.length, + convert.utf8.encode(contentopf))); + + return arch; + } + + // Serializes the EpubBook into a byte array + static List? writeBook(EpubBook book) { + var arch = _createArchive(book); + + return ZipEncoder().encode(arch); + } +} diff --git a/packages/epubx/lib/src/readers/book_cover_reader.dart b/packages/epubx/lib/src/readers/book_cover_reader.dart new file mode 100644 index 0000000..1f14e0e --- /dev/null +++ b/packages/epubx/lib/src/readers/book_cover_reader.dart @@ -0,0 +1,46 @@ +import 'dart:async'; + +import 'package:collection/collection.dart' show IterableExtension; +import 'package:image/image.dart' as images; + +import '../ref_entities/epub_book_ref.dart'; +import '../ref_entities/epub_byte_content_file_ref.dart'; +import '../schema/opf/epub_manifest_item.dart'; +import '../schema/opf/epub_metadata_meta.dart'; + +class BookCoverReader { + static Future readBookCover(EpubBookRef bookRef) async { + var metaItems = bookRef.Schema!.Package!.Metadata!.MetaItems; + if (metaItems == null || metaItems.isEmpty) return null; + + var coverMetaItem = metaItems.firstWhereOrNull( + (EpubMetadataMeta metaItem) => + metaItem.Name != null && metaItem.Name!.toLowerCase() == 'cover'); + if (coverMetaItem == null) return null; + if (coverMetaItem.Content == null || coverMetaItem.Content!.isEmpty) { + throw Exception( + 'Incorrect EPUB metadata: cover item content is missing.'); + } + + var coverManifestItem = bookRef.Schema!.Package!.Manifest!.Items! + .firstWhereOrNull((EpubManifestItem manifestItem) => + manifestItem.Id!.toLowerCase() == + coverMetaItem.Content!.toLowerCase()); + if (coverManifestItem == null) { + throw Exception( + 'Incorrect EPUB manifest: item with ID = \"${coverMetaItem.Content}\" is missing.'); + } + + EpubByteContentFileRef? coverImageContentFileRef; + if (!bookRef.Content!.Images!.containsKey(coverManifestItem.Href)) { + throw Exception( + 'Incorrect EPUB manifest: item with href = \"${coverManifestItem.Href}\" is missing.'); + } + + coverImageContentFileRef = bookRef.Content!.Images![coverManifestItem.Href]; + var coverImageContent = + await coverImageContentFileRef!.readContentAsBytes(); + var retval = images.decodeImage(coverImageContent); + return retval; + } +} diff --git a/packages/epubx/lib/src/readers/chapter_reader.dart b/packages/epubx/lib/src/readers/chapter_reader.dart new file mode 100644 index 0000000..bff4932 --- /dev/null +++ b/packages/epubx/lib/src/readers/chapter_reader.dart @@ -0,0 +1,53 @@ +import '../ref_entities/epub_book_ref.dart'; +import '../ref_entities/epub_chapter_ref.dart'; +import '../ref_entities/epub_text_content_file_ref.dart'; +import '../schema/navigation/epub_navigation_point.dart'; + +class ChapterReader { + static List getChapters(EpubBookRef bookRef) { + if (bookRef.Schema!.Navigation == null) { + return []; + } + return getChaptersImpl( + bookRef, bookRef.Schema!.Navigation!.NavMap!.Points!); + } + + static List getChaptersImpl( + EpubBookRef bookRef, List navigationPoints) { + var result = []; + for (var navigationPoint in navigationPoints) { + String? contentFileName; + String? anchor; + if (navigationPoint.Content?.Source == null) continue; + var contentSourceAnchorCharIndex = + navigationPoint.Content!.Source!.indexOf('#'); + if (contentSourceAnchorCharIndex == -1) { + contentFileName = navigationPoint.Content!.Source; + anchor = null; + } else { + contentFileName = navigationPoint.Content!.Source! + .substring(0, contentSourceAnchorCharIndex); + anchor = navigationPoint.Content!.Source! + .substring(contentSourceAnchorCharIndex + 1); + } + contentFileName = Uri.decodeFull(contentFileName!); + EpubTextContentFileRef? htmlContentFileRef; + if (!bookRef.Content!.Html!.containsKey(contentFileName)) { + throw Exception( + 'Incorrect EPUB manifest: item with href = \"$contentFileName\" is missing.'); + } + + htmlContentFileRef = bookRef.Content!.Html![contentFileName]; + var chapterRef = EpubChapterRef(htmlContentFileRef); + chapterRef.ContentFileName = contentFileName; + chapterRef.Anchor = anchor; + chapterRef.Title = navigationPoint.NavigationLabels!.first.Text; + chapterRef.SubChapters = + getChaptersImpl(bookRef, navigationPoint.ChildNavigationPoints!); + + result.add(chapterRef); + } + ; + return result; + } +} diff --git a/packages/epubx/lib/src/readers/content_reader.dart b/packages/epubx/lib/src/readers/content_reader.dart new file mode 100644 index 0000000..78d0e7c --- /dev/null +++ b/packages/epubx/lib/src/readers/content_reader.dart @@ -0,0 +1,137 @@ +import '../entities/epub_content_type.dart'; +import '../ref_entities/epub_book_ref.dart'; +import '../ref_entities/epub_byte_content_file_ref.dart'; +import '../ref_entities/epub_content_file_ref.dart'; +import '../ref_entities/epub_content_ref.dart'; +import '../ref_entities/epub_text_content_file_ref.dart'; +import '../schema/opf/epub_manifest_item.dart'; + +class ContentReader { + static EpubContentRef parseContentMap(EpubBookRef bookRef) { + var result = EpubContentRef(); + result.Html = {}; + result.Css = {}; + result.Images = {}; + result.Fonts = {}; + result.AllFiles = {}; + + bookRef.Schema!.Package!.Manifest!.Items! + .forEach((EpubManifestItem manifestItem) { + var fileName = manifestItem.Href; + var contentMimeType = manifestItem.MediaType!; + var contentType = getContentTypeByContentMimeType(contentMimeType); + switch (contentType) { + case EpubContentType.XHTML_1_1: + case EpubContentType.CSS: + case EpubContentType.OEB1_DOCUMENT: + case EpubContentType.OEB1_CSS: + case EpubContentType.XML: + case EpubContentType.DTBOOK: + case EpubContentType.DTBOOK_NCX: + var epubTextContentFile = EpubTextContentFileRef(bookRef); + { + epubTextContentFile.FileName = Uri.decodeFull(fileName!); + epubTextContentFile.ContentMimeType = contentMimeType; + epubTextContentFile.ContentType = contentType; + } + ; + switch (contentType) { + case EpubContentType.XHTML_1_1: + result.Html![fileName] = epubTextContentFile; + break; + case EpubContentType.CSS: + result.Css![fileName] = epubTextContentFile; + break; + case EpubContentType.DTBOOK: + case EpubContentType.DTBOOK_NCX: + case EpubContentType.OEB1_DOCUMENT: + case EpubContentType.XML: + case EpubContentType.OEB1_CSS: + case EpubContentType.IMAGE_GIF: + case EpubContentType.IMAGE_JPEG: + case EpubContentType.IMAGE_PNG: + case EpubContentType.IMAGE_SVG: + case EpubContentType.IMAGE_BMP: + case EpubContentType.FONT_TRUETYPE: + case EpubContentType.FONT_OPENTYPE: + case EpubContentType.OTHER: + break; + } + result.AllFiles![fileName] = epubTextContentFile; + break; + default: + var epubByteContentFile = EpubByteContentFileRef(bookRef); + { + epubByteContentFile.FileName = Uri.decodeFull(fileName!); + epubByteContentFile.ContentMimeType = contentMimeType; + epubByteContentFile.ContentType = contentType; + } + ; + switch (contentType) { + case EpubContentType.IMAGE_GIF: + case EpubContentType.IMAGE_JPEG: + case EpubContentType.IMAGE_PNG: + case EpubContentType.IMAGE_SVG: + case EpubContentType.IMAGE_BMP: + result.Images![fileName] = epubByteContentFile; + break; + case EpubContentType.FONT_TRUETYPE: + case EpubContentType.FONT_OPENTYPE: + result.Fonts![fileName] = epubByteContentFile; + break; + case EpubContentType.CSS: + case EpubContentType.XHTML_1_1: + case EpubContentType.DTBOOK: + case EpubContentType.DTBOOK_NCX: + case EpubContentType.OEB1_DOCUMENT: + case EpubContentType.XML: + case EpubContentType.OEB1_CSS: + case EpubContentType.OTHER: + break; + } + result.AllFiles![fileName] = epubByteContentFile; + break; + } + }); + return result; + } + + static EpubContentType getContentTypeByContentMimeType( + String contentMimeType) { + switch (contentMimeType.toLowerCase()) { + case 'application/xhtml+xml': + case 'text/html': + return EpubContentType.XHTML_1_1; + case 'application/x-dtbook+xml': + return EpubContentType.DTBOOK; + case 'application/x-dtbncx+xml': + return EpubContentType.DTBOOK_NCX; + case 'text/x-oeb1-document': + return EpubContentType.OEB1_DOCUMENT; + case 'application/xml': + return EpubContentType.XML; + case 'text/css': + return EpubContentType.CSS; + case 'text/x-oeb1-css': + return EpubContentType.OEB1_CSS; + case 'image/gif': + return EpubContentType.IMAGE_GIF; + case 'image/jpeg': + return EpubContentType.IMAGE_JPEG; + case 'image/png': + return EpubContentType.IMAGE_PNG; + case 'image/svg+xml': + return EpubContentType.IMAGE_SVG; + case 'image/bmp': + return EpubContentType.IMAGE_BMP; + case 'font/truetype': + return EpubContentType.FONT_TRUETYPE; + case 'font/opentype': + return EpubContentType.FONT_OPENTYPE; + case 'application/vnd.ms-opentype': + return EpubContentType.FONT_OPENTYPE; + default: + return EpubContentType.OTHER; + } + } +} diff --git a/packages/epubx/lib/src/readers/navigation_reader.dart b/packages/epubx/lib/src/readers/navigation_reader.dart new file mode 100644 index 0000000..67f073b --- /dev/null +++ b/packages/epubx/lib/src/readers/navigation_reader.dart @@ -0,0 +1,559 @@ +import 'dart:async'; + +import 'package:archive/archive.dart'; +import 'dart:convert' as convert; +import 'package:collection/collection.dart' show IterableExtension; +import 'package:epubx/src/schema/opf/epub_version.dart'; +import 'package:xml/xml.dart' as xml; +import 'package:path/path.dart' as path; + +import '../schema/navigation/epub_metadata.dart'; +import '../schema/navigation/epub_navigation.dart'; +import '../schema/navigation/epub_navigation_doc_author.dart'; +import '../schema/navigation/epub_navigation_doc_title.dart'; +import '../schema/navigation/epub_navigation_head.dart'; +import '../schema/navigation/epub_navigation_head_meta.dart'; +import '../schema/navigation/epub_navigation_label.dart'; +import '../schema/navigation/epub_navigation_list.dart'; +import '../schema/navigation/epub_navigation_map.dart'; +import '../schema/navigation/epub_navigation_page_list.dart'; +import '../schema/navigation/epub_navigation_page_target.dart'; +import '../schema/navigation/epub_navigation_page_target_type.dart'; +import '../schema/navigation/epub_navigation_point.dart'; +import '../schema/navigation/epub_navigation_target.dart'; +import '../schema/opf/epub_manifest_item.dart'; +import '../schema/opf/epub_package.dart'; +import '../utils/enum_from_string.dart'; +import '../utils/zip_path_utils.dart'; + +// ignore: omit_local_variable_types + +class NavigationReader { + static String? _tocFileEntryPath; + + static Future readNavigation( + Archive epubArchive, String contentDirectoryPath, EpubPackage package) async { + var result = EpubNavigation(); + if (package.Version == EpubVersion.Epub2) { + var tocId = package.Spine!.TableOfContents; + if (tocId == null || tocId.isEmpty) { + throw Exception('EPUB parsing error: TOC ID is empty.'); + } + + var tocManifestItem = package.Manifest!.Items!.cast().firstWhere( + (EpubManifestItem? item) => item!.Id!.toLowerCase() == tocId.toLowerCase(), + orElse: () => null, + ); + if (tocManifestItem == null) { + throw Exception('EPUB parsing error: TOC item $tocId not found in EPUB manifest.'); + } + + _tocFileEntryPath = ZipPathUtils.combine(contentDirectoryPath, tocManifestItem.Href); + var tocFileEntry = epubArchive.files.cast().firstWhere( + (ArchiveFile? file) => file!.name.toLowerCase() == _tocFileEntryPath!.toLowerCase(), + orElse: () => null); + if (tocFileEntry == null) { + throw Exception('EPUB parsing error: TOC file $_tocFileEntryPath not found in archive.'); + } + + var containerDocument = xml.XmlDocument.parse(convert.utf8.decode(tocFileEntry.content)); + + var ncxNamespace = 'http://www.daisy.org/z3986/2005/ncx/'; + var ncxNode = containerDocument + .findAllElements('ncx', namespace: ncxNamespace) + .cast() + .firstWhere((xml.XmlElement? elem) => elem != null, orElse: () => null); + if (ncxNode == null) { + throw Exception('EPUB parsing error: TOC file does not contain ncx element.'); + } + + var headNode = ncxNode + .findAllElements('head', namespace: ncxNamespace) + .cast() + .firstWhere((xml.XmlElement? elem) => elem != null, orElse: () => null); + if (headNode == null) { + throw Exception('EPUB parsing error: TOC file does not contain head element.'); + } + + var navigationHead = readNavigationHead(headNode); + result.Head = navigationHead; + var docTitleNode = ncxNode + .findElements('docTitle', namespace: ncxNamespace) + .cast() + .firstWhere((xml.XmlElement? elem) => elem != null, orElse: () => null); + if (docTitleNode == null) { + throw Exception('EPUB parsing error: TOC file does not contain docTitle element.'); + } + + var navigationDocTitle = readNavigationDocTitle(docTitleNode); + result.DocTitle = navigationDocTitle; + result.DocAuthors = []; + ncxNode.findElements('docAuthor', namespace: ncxNamespace).forEach((xml.XmlElement docAuthorNode) { + var navigationDocAuthor = readNavigationDocAuthor(docAuthorNode); + result.DocAuthors!.add(navigationDocAuthor); + }); + + var navMapNode = ncxNode + .findElements('navMap', namespace: ncxNamespace) + .cast() + .firstWhere((xml.XmlElement? elem) => elem != null, orElse: () => null); + if (navMapNode == null) { + throw Exception('EPUB parsing error: TOC file does not contain navMap element.'); + } + + var navMap = readNavigationMap(navMapNode); + result.NavMap = navMap; + var pageListNode = ncxNode + .findElements('pageList', namespace: ncxNamespace) + .cast() + .firstWhere((xml.XmlElement? elem) => elem != null, orElse: () => null); + if (pageListNode != null) { + var pageList = readNavigationPageList(pageListNode); + result.PageList = pageList; + } + + result.NavLists = []; + ncxNode.findElements('navList', namespace: ncxNamespace).forEach((xml.XmlElement navigationListNode) { + var navigationList = readNavigationList(navigationListNode); + result.NavLists!.add(navigationList); + }); + } else { + //Version 3 + + var tocManifestItem = package.Manifest!.Items! + .cast() + .firstWhere((element) => element!.Properties == 'nav', orElse: () => null); + if (tocManifestItem == null) { + throw Exception('EPUB parsing error: TOC item, not found in EPUB manifest.'); + } + + _tocFileEntryPath = ZipPathUtils.combine(contentDirectoryPath, tocManifestItem.Href); + var tocFileEntry = epubArchive.files.cast().firstWhere( + (ArchiveFile? file) => file!.name.toLowerCase() == _tocFileEntryPath!.toLowerCase(), + orElse: () => null); + if (tocFileEntry == null) { + throw Exception('EPUB parsing error: TOC file $_tocFileEntryPath not found in archive.'); + } + //Get relative toc file path + _tocFileEntryPath = ((_tocFileEntryPath!.split('/')..removeLast())..removeAt(0)).join('/') + '/'; + + var containerDocument = xml.XmlDocument.parse(convert.utf8.decode(tocFileEntry.content)); + + var headNode = containerDocument + .findAllElements('head') + .cast() + .firstWhere((xml.XmlElement? elem) => elem != null, orElse: () => null); + if (headNode == null) { + throw Exception('EPUB parsing error: TOC file does not contain head element.'); + } + + result.DocTitle = EpubNavigationDocTitle(); + result.DocTitle!.Titles = package.Metadata!.Titles; +// result.DocTitle.Titles.add(headNode.findAllElements("title").firstWhere((element) => element != null, orElse: () => null).text.trim()); + + result.DocAuthors = []; + + var navNode = containerDocument + .findAllElements('nav') + .cast() + .firstWhere((xml.XmlElement? elem) => elem != null, orElse: () => null); + if (navNode == null) { + throw Exception('EPUB parsing error: TOC file does not contain head element.'); + } + var navMapNode = navNode.findElements('ol').single; + + var navMap = readNavigationMapV3(navMapNode); + result.NavMap = navMap; + + //TODO : Implement pagesLists +// xml.XmlElement pageListNode = ncxNode +// .findElements("pageList", namespace: ncxNamespace) +// .firstWhere((xml.XmlElement elem) => elem != null, +// orElse: () => null); +// if (pageListNode != null) { +// EpubNavigationPageList pageList = readNavigationPageList(pageListNode); +// result.PageList = pageList; +// } + } + + return result; + } + + static EpubNavigationContent readNavigationContent(xml.XmlElement navigationContentNode) { + var result = EpubNavigationContent(); + navigationContentNode.attributes.forEach((xml.XmlAttribute navigationContentNodeAttribute) { + var attributeValue = navigationContentNodeAttribute.value; + switch (navigationContentNodeAttribute.name.local.toLowerCase()) { + case 'id': + result.Id = attributeValue; + break; + case 'src': + result.Source = attributeValue; + break; + } + }); + if (result.Source == null || result.Source!.isEmpty) { + throw Exception('Incorrect EPUB navigation content: content source is missing.'); + } + + return result; + } + + static EpubNavigationContent readNavigationContentV3(xml.XmlElement navigationContentNode) { + var result = EpubNavigationContent(); + navigationContentNode.attributes.forEach((xml.XmlAttribute navigationContentNodeAttribute) { + var attributeValue = navigationContentNodeAttribute.value; + switch (navigationContentNodeAttribute.name.local.toLowerCase()) { + case 'id': + result.Id = attributeValue; + break; + case 'href': + if (_tocFileEntryPath!.length < 2 || + attributeValue.startsWith(_tocFileEntryPath!)) { + result.Source = attributeValue; + } else { + result.Source = path.normalize(_tocFileEntryPath! + attributeValue); + } + + break; + } + }); + // element with span, the content will be null; + // if (result.Source == null || result.Source!.isEmpty) { + // throw Exception( + // 'Incorrect EPUB navigation content: content source is missing.'); + // } + return result; + } + + static String extractContentPath(String _tocFileEntryPath, String ref) { + if (!_tocFileEntryPath.endsWith('/')) _tocFileEntryPath = _tocFileEntryPath + '/'; + var r = _tocFileEntryPath + ref; + r = r.replaceAll('/\./', '/'); + r = r.replaceAll(RegExp(r'/[^/]+/\.\./'), '/'); + r = r.replaceAll(RegExp(r'^[^/]+/\.\./'), ''); + return r; + } + + static EpubNavigationDocAuthor readNavigationDocAuthor(xml.XmlElement docAuthorNode) { + var result = EpubNavigationDocAuthor(); + result.Authors = []; + docAuthorNode.children.whereType().forEach((xml.XmlElement textNode) { + if (textNode.name.local.toLowerCase() == 'text') { + result.Authors!.add(textNode.text); + } + }); + return result; + } + + static EpubNavigationDocTitle readNavigationDocTitle(xml.XmlElement docTitleNode) { + var result = EpubNavigationDocTitle(); + result.Titles = []; + docTitleNode.children.whereType().forEach((xml.XmlElement textNode) { + if (textNode.name.local.toLowerCase() == 'text') { + result.Titles!.add(textNode.text); + } + }); + return result; + } + + static EpubNavigationHead readNavigationHead(xml.XmlElement headNode) { + var result = EpubNavigationHead(); + result.Metadata = []; + + headNode.children.whereType().forEach((xml.XmlElement metaNode) { + if (metaNode.name.local.toLowerCase() == 'meta') { + var meta = EpubNavigationHeadMeta(); + metaNode.attributes.forEach((xml.XmlAttribute metaNodeAttribute) { + var attributeValue = metaNodeAttribute.value; + switch (metaNodeAttribute.name.local.toLowerCase()) { + case 'name': + meta.Name = attributeValue; + break; + case 'content': + meta.Content = attributeValue; + break; + case 'scheme': + meta.Scheme = attributeValue; + break; + } + }); + + if (meta.Name == null || meta.Name!.isEmpty) { + throw Exception('Incorrect EPUB navigation meta: meta name is missing.'); + } + if (meta.Content == null) { + throw Exception('Incorrect EPUB navigation meta: meta content is missing.'); + } + + result.Metadata!.add(meta); + } + }); + return result; + } + + static EpubNavigationLabel readNavigationLabel(xml.XmlElement navigationLabelNode) { + var result = EpubNavigationLabel(); + + var navigationLabelTextNode = navigationLabelNode + .findElements('text', namespace: navigationLabelNode.name.namespaceUri) + .firstWhereOrNull((xml.XmlElement? elem) => elem != null); + if (navigationLabelTextNode == null) { + throw Exception('Incorrect EPUB navigation label: label text element is missing.'); + } + + result.Text = navigationLabelTextNode.text; + + return result; + } + + static EpubNavigationLabel readNavigationLabelV3(xml.XmlElement navigationLabelNode) { + var result = EpubNavigationLabel(); + result.Text = navigationLabelNode.text.trim(); + return result; + } + + static EpubNavigationList readNavigationList(xml.XmlElement navigationListNode) { + var result = EpubNavigationList(); + navigationListNode.attributes.forEach((xml.XmlAttribute navigationListNodeAttribute) { + var attributeValue = navigationListNodeAttribute.value; + switch (navigationListNodeAttribute.name.local.toLowerCase()) { + case 'id': + result.Id = attributeValue; + break; + case 'class': + result.Class = attributeValue; + break; + } + }); + navigationListNode.children.whereType().forEach((xml.XmlElement navigationListChildNode) { + switch (navigationListChildNode.name.local.toLowerCase()) { + case 'navlabel': + var navigationLabel = readNavigationLabel(navigationListChildNode); + result.NavigationLabels!.add(navigationLabel); + break; + case 'navtarget': + var navigationTarget = readNavigationTarget(navigationListChildNode); + result.NavigationTargets!.add(navigationTarget); + break; + } + }); + // if (result.NavigationLabels!.isEmpty) { + // throw Exception( + // 'Incorrect EPUB navigation page target: at least one navLabel element is required.'); + // } + return result; + } + + static EpubNavigationMap readNavigationMap(xml.XmlElement navigationMapNode) { + var result = EpubNavigationMap(); + result.Points = []; + navigationMapNode.children.whereType().forEach((xml.XmlElement navigationPointNode) { + if (navigationPointNode.name.local.toLowerCase() == 'navpoint') { + var navigationPoint = readNavigationPoint(navigationPointNode); + result.Points!.add(navigationPoint); + } + }); + return result; + } + + static EpubNavigationMap readNavigationMapV3(xml.XmlElement navigationMapNode) { + var result = EpubNavigationMap(); + result.Points = []; + navigationMapNode.children.whereType().forEach((xml.XmlElement navigationPointNode) { + if (navigationPointNode.name.local.toLowerCase() == 'li') { + var navigationPoint = readNavigationPointV3(navigationPointNode); + result.Points!.add(navigationPoint); + } + }); + return result; + } + + static EpubNavigationPageList readNavigationPageList(xml.XmlElement navigationPageListNode) { + var result = EpubNavigationPageList(); + result.Targets = []; + navigationPageListNode.children.whereType().forEach((xml.XmlElement pageTargetNode) { + if (pageTargetNode.name.local == 'pageTarget') { + var pageTarget = readNavigationPageTarget(pageTargetNode); + result.Targets!.add(pageTarget); + } + }); + + return result; + } + + static EpubNavigationPageTarget readNavigationPageTarget(xml.XmlElement navigationPageTargetNode) { + var result = EpubNavigationPageTarget(); + result.NavigationLabels = []; + navigationPageTargetNode.attributes.forEach((xml.XmlAttribute navigationPageTargetNodeAttribute) { + var attributeValue = navigationPageTargetNodeAttribute.value; + switch (navigationPageTargetNodeAttribute.name.local.toLowerCase()) { + case 'id': + result.Id = attributeValue; + break; + case 'value': + result.Value = attributeValue; + break; + case 'type': + var converter = EnumFromString(EpubNavigationPageTargetType.values); + var type = converter.get(attributeValue); + result.Type = type; + break; + case 'class': + result.Class = attributeValue; + break; + case 'playorder': + result.PlayOrder = attributeValue; + break; + } + }); + if (result.Type == EpubNavigationPageTargetType.UNDEFINED) { + throw Exception('Incorrect EPUB navigation page target: page target type is missing.'); + } + + navigationPageTargetNode.children + .whereType() + .forEach((xml.XmlElement navigationPageTargetChildNode) { + switch (navigationPageTargetChildNode.name.local.toLowerCase()) { + case 'navlabel': + var navigationLabel = readNavigationLabel(navigationPageTargetChildNode); + result.NavigationLabels!.add(navigationLabel); + break; + case 'content': + var content = readNavigationContent(navigationPageTargetChildNode); + result.Content = content; + break; + } + }); + if (result.NavigationLabels!.isEmpty) { + throw Exception('Incorrect EPUB navigation page target: at least one navLabel element is required.'); + } + + return result; + } + + static EpubNavigationPoint readNavigationPoint(xml.XmlElement navigationPointNode) { + var result = EpubNavigationPoint(); + navigationPointNode.attributes.forEach((xml.XmlAttribute navigationPointNodeAttribute) { + var attributeValue = navigationPointNodeAttribute.value; + switch (navigationPointNodeAttribute.name.local.toLowerCase()) { + case 'id': + result.Id = attributeValue; + break; + case 'class': + result.Class = attributeValue; + break; + case 'playorder': + result.PlayOrder = attributeValue; + break; + } + }); + if (result.Id == null || result.Id!.isEmpty) { + throw Exception('Incorrect EPUB navigation point: point ID is missing.'); + } + + result.NavigationLabels = []; + result.ChildNavigationPoints = []; + navigationPointNode.children.whereType().forEach((xml.XmlElement navigationPointChildNode) { + switch (navigationPointChildNode.name.local.toLowerCase()) { + case 'navlabel': + var navigationLabel = readNavigationLabel(navigationPointChildNode); + result.NavigationLabels!.add(navigationLabel); + break; + case 'content': + var content = readNavigationContent(navigationPointChildNode); + result.Content = content; + break; + case 'navpoint': + var childNavigationPoint = readNavigationPoint(navigationPointChildNode); + result.ChildNavigationPoints!.add(childNavigationPoint); + break; + } + }); + + if (result.NavigationLabels!.isEmpty) { + throw Exception( + 'EPUB parsing error: navigation point ${result.Id} should contain at least one navigation label.'); + } + if (result.Content == null) { + throw Exception('EPUB parsing error: navigation point ${result.Id} should contain content.'); + } + + return result; + } + + static EpubNavigationPoint readNavigationPointV3(xml.XmlElement navigationPointNode) { + var result = EpubNavigationPoint(); + + result.NavigationLabels = []; + result.ChildNavigationPoints = []; + navigationPointNode.children.whereType().forEach((xml.XmlElement navigationPointChildNode) { + switch (navigationPointChildNode.name.local.toLowerCase()) { + case 'a': + case 'span': + var navigationLabel = readNavigationLabelV3(navigationPointChildNode); + result.NavigationLabels!.add(navigationLabel); + var content = readNavigationContentV3(navigationPointChildNode); + result.Content = content; + break; + case 'ol': + readNavigationMapV3(navigationPointChildNode).Points!.forEach((point) { + result.ChildNavigationPoints!.add(point); + }); + break; + } + }); + + if (result.NavigationLabels!.isEmpty) { + throw Exception( + 'EPUB parsing error: navigation point ${result.Id} should contain at least one navigation label.'); + } + if (result.Content == null) { + throw Exception('EPUB parsing error: navigation point ${result.Id} should contain content.'); + } + + return result; + } + + static EpubNavigationTarget readNavigationTarget(xml.XmlElement navigationTargetNode) { + var result = EpubNavigationTarget(); + navigationTargetNode.attributes.forEach((xml.XmlAttribute navigationPageTargetNodeAttribute) { + var attributeValue = navigationPageTargetNodeAttribute.value; + switch (navigationPageTargetNodeAttribute.name.local.toLowerCase()) { + case 'id': + result.Id = attributeValue; + break; + case 'value': + result.Value = attributeValue; + break; + case 'class': + result.Class = attributeValue; + break; + case 'playorder': + result.PlayOrder = attributeValue; + break; + } + }); + if (result.Id == null || result.Id!.isEmpty) { + throw Exception('Incorrect EPUB navigation target: navigation target ID is missing.'); + } + + navigationTargetNode.children.whereType().forEach((xml.XmlElement navigationTargetChildNode) { + switch (navigationTargetChildNode.name.local.toLowerCase()) { + case 'navlabel': + var navigationLabel = readNavigationLabel(navigationTargetChildNode); + result.NavigationLabels!.add(navigationLabel); + break; + case 'content': + var content = readNavigationContent(navigationTargetChildNode); + result.Content = content; + break; + } + }); + if (result.NavigationLabels!.isEmpty) { + throw Exception('Incorrect EPUB navigation target: at least one navLabel element is required.'); + } + + return result; + } +} diff --git a/packages/epubx/lib/src/readers/package_reader.dart b/packages/epubx/lib/src/readers/package_reader.dart new file mode 100644 index 0000000..59153a5 --- /dev/null +++ b/packages/epubx/lib/src/readers/package_reader.dart @@ -0,0 +1,403 @@ +import 'dart:async'; + +import 'package:archive/archive.dart'; +import 'dart:convert' as convert; +import 'package:collection/collection.dart' show IterableExtension; +import 'package:xml/xml.dart'; + +import '../schema/opf/epub_guide.dart'; +import '../schema/opf/epub_guide_reference.dart'; +import '../schema/opf/epub_manifest.dart'; +import '../schema/opf/epub_manifest_item.dart'; +import '../schema/opf/epub_metadata.dart'; +import '../schema/opf/epub_metadata_contributor.dart'; +import '../schema/opf/epub_metadata_creator.dart'; +import '../schema/opf/epub_metadata_date.dart'; +import '../schema/opf/epub_metadata_identifier.dart'; +import '../schema/opf/epub_metadata_meta.dart'; +import '../schema/opf/epub_package.dart'; +import '../schema/opf/epub_spine.dart'; +import '../schema/opf/epub_spine_item_ref.dart'; +import '../schema/opf/epub_version.dart'; + +class PackageReader { + static EpubGuide readGuide(XmlElement guideNode) { + var result = EpubGuide(); + result.Items = []; + guideNode.children + .whereType() + .forEach((XmlElement guideReferenceNode) { + if (guideReferenceNode.name.local.toLowerCase() == 'reference') { + var guideReference = EpubGuideReference(); + guideReferenceNode.attributes + .forEach((XmlAttribute guideReferenceNodeAttribute) { + var attributeValue = guideReferenceNodeAttribute.value; + switch (guideReferenceNodeAttribute.name.local.toLowerCase()) { + case 'type': + guideReference.Type = attributeValue; + break; + case 'title': + guideReference.Title = attributeValue; + break; + case 'href': + guideReference.Href = attributeValue; + break; + } + }); + if (guideReference.Type == null || guideReference.Type!.isEmpty) { + throw Exception('Incorrect EPUB guide: item type is missing'); + } + if (guideReference.Href == null || guideReference.Href!.isEmpty) { + throw Exception('Incorrect EPUB guide: item href is missing'); + } + result.Items!.add(guideReference); + } + }); + return result; + } + + static EpubManifest readManifest(XmlElement manifestNode) { + var result = EpubManifest(); + result.Items = []; + manifestNode.children + .whereType() + .forEach((XmlElement manifestItemNode) { + if (manifestItemNode.name.local.toLowerCase() == 'item') { + var manifestItem = EpubManifestItem(); + manifestItemNode.attributes + .forEach((XmlAttribute manifestItemNodeAttribute) { + var attributeValue = manifestItemNodeAttribute.value; + switch (manifestItemNodeAttribute.name.local.toLowerCase()) { + case 'id': + manifestItem.Id = attributeValue; + break; + case 'href': + manifestItem.Href = attributeValue; + break; + case 'media-type': + manifestItem.MediaType = attributeValue; + break; + case 'media-overlay': + manifestItem.MediaOverlay = attributeValue; + break; + case 'required-namespace': + manifestItem.RequiredNamespace = attributeValue; + break; + case 'required-modules': + manifestItem.RequiredModules = attributeValue; + break; + case 'fallback': + manifestItem.Fallback = attributeValue; + break; + case 'fallback-style': + manifestItem.FallbackStyle = attributeValue; + break; + case 'properties': + manifestItem.Properties = attributeValue; + break; + } + }); + + if (manifestItem.Id == null || manifestItem.Id!.isEmpty) { + throw Exception('Incorrect EPUB manifest: item ID is missing'); + } + if (manifestItem.Href == null || manifestItem.Href!.isEmpty) { + throw Exception('Incorrect EPUB manifest: item href is missing'); + } + if (manifestItem.MediaType == null || manifestItem.MediaType!.isEmpty) { + throw Exception( + 'Incorrect EPUB manifest: item media type is missing'); + } + result.Items!.add(manifestItem); + } + }); + return result; + } + + static EpubMetadata readMetadata( + XmlElement metadataNode, EpubVersion? epubVersion) { + var result = EpubMetadata(); + result.Titles = []; + result.Creators = []; + result.Subjects = []; + result.Publishers = []; + result.Contributors = []; + result.Dates = []; + result.Types = []; + result.Formats = []; + result.Identifiers = []; + result.Sources = []; + result.Languages = []; + result.Relations = []; + result.Coverages = []; + result.Rights = []; + result.MetaItems = []; + metadataNode.children + .whereType() + .forEach((XmlElement metadataItemNode) { + var innerText = metadataItemNode.text; + switch (metadataItemNode.name.local.toLowerCase()) { + case 'title': + result.Titles!.add(innerText); + break; + case 'creator': + var creator = readMetadataCreator(metadataItemNode); + result.Creators!.add(creator); + break; + case 'subject': + result.Subjects!.add(innerText); + break; + case 'description': + result.Description = innerText; + break; + case 'publisher': + result.Publishers!.add(innerText); + break; + case 'contributor': + var contributor = readMetadataContributor(metadataItemNode); + result.Contributors!.add(contributor); + break; + case 'date': + var date = readMetadataDate(metadataItemNode); + result.Dates!.add(date); + break; + case 'type': + result.Types!.add(innerText); + break; + case 'format': + result.Formats!.add(innerText); + break; + case 'identifier': + var identifier = readMetadataIdentifier(metadataItemNode); + result.Identifiers!.add(identifier); + break; + case 'source': + result.Sources!.add(innerText); + break; + case 'language': + result.Languages!.add(innerText); + break; + case 'relation': + result.Relations!.add(innerText); + break; + case 'coverage': + result.Coverages!.add(innerText); + break; + case 'rights': + result.Rights!.add(innerText); + break; + case 'meta': + if (epubVersion == EpubVersion.Epub2) { + var meta = readMetadataMetaVersion2(metadataItemNode); + result.MetaItems!.add(meta); + } else if (epubVersion == EpubVersion.Epub3) { + var meta = readMetadataMetaVersion3(metadataItemNode); + result.MetaItems!.add(meta); + } + break; + } + }); + return result; + } + + static EpubMetadataContributor readMetadataContributor( + XmlElement metadataContributorNode) { + var result = EpubMetadataContributor(); + metadataContributorNode.attributes + .forEach((XmlAttribute metadataContributorNodeAttribute) { + var attributeValue = metadataContributorNodeAttribute.value; + switch (metadataContributorNodeAttribute.name.local.toLowerCase()) { + case 'role': + result.Role = attributeValue; + break; + case 'file-as': + result.FileAs = attributeValue; + break; + } + }); + result.Contributor = metadataContributorNode.text; + return result; + } + + static EpubMetadataCreator readMetadataCreator( + XmlElement metadataCreatorNode) { + var result = EpubMetadataCreator(); + metadataCreatorNode.attributes + .forEach((XmlAttribute metadataCreatorNodeAttribute) { + var attributeValue = metadataCreatorNodeAttribute.value; + switch (metadataCreatorNodeAttribute.name.local.toLowerCase()) { + case 'role': + result.Role = attributeValue; + break; + case 'file-as': + result.FileAs = attributeValue; + break; + } + }); + result.Creator = metadataCreatorNode.text; + return result; + } + + static EpubMetadataDate readMetadataDate(XmlElement metadataDateNode) { + var result = EpubMetadataDate(); + var eventAttribute = metadataDateNode.getAttribute('event', + namespace: metadataDateNode.name.namespaceUri); + if (eventAttribute != null && eventAttribute.isNotEmpty) { + result.Event = eventAttribute; + } + result.Date = metadataDateNode.text; + return result; + } + + static EpubMetadataIdentifier readMetadataIdentifier( + XmlElement metadataIdentifierNode) { + var result = EpubMetadataIdentifier(); + metadataIdentifierNode.attributes + .forEach((XmlAttribute metadataIdentifierNodeAttribute) { + var attributeValue = metadataIdentifierNodeAttribute.value; + switch (metadataIdentifierNodeAttribute.name.local.toLowerCase()) { + case 'id': + result.Id = attributeValue; + break; + case 'scheme': + result.Scheme = attributeValue; + break; + } + }); + result.Identifier = metadataIdentifierNode.text; + return result; + } + + static EpubMetadataMeta readMetadataMetaVersion2( + XmlElement metadataMetaNode) { + var result = EpubMetadataMeta(); + metadataMetaNode.attributes + .forEach((XmlAttribute metadataMetaNodeAttribute) { + var attributeValue = metadataMetaNodeAttribute.value; + switch (metadataMetaNodeAttribute.name.local.toLowerCase()) { + case 'name': + result.Name = attributeValue; + break; + case 'content': + result.Content = attributeValue; + break; + } + }); + return result; + } + + static EpubMetadataMeta readMetadataMetaVersion3( + XmlElement metadataMetaNode) { + var result = EpubMetadataMeta(); + result.Attributes = {}; + metadataMetaNode.attributes + .forEach((XmlAttribute metadataMetaNodeAttribute) { + var attributeValue = metadataMetaNodeAttribute.value; + result.Attributes![metadataMetaNodeAttribute.name.local.toLowerCase()] = + attributeValue; + switch (metadataMetaNodeAttribute.name.local.toLowerCase()) { + case 'id': + result.Id = attributeValue; + break; + case 'refines': + result.Refines = attributeValue; + break; + case 'property': + result.Property = attributeValue; + break; + case 'scheme': + result.Scheme = attributeValue; + break; + } + }); + result.Content = metadataMetaNode.text; + return result; + } + + static Future readPackage( + Archive epubArchive, String rootFilePath) async { + var rootFileEntry = epubArchive.files.firstWhereOrNull( + (ArchiveFile testFile) => testFile.name == rootFilePath); + if (rootFileEntry == null) { + throw Exception('EPUB parsing error: root file not found in archive.'); + } + var containerDocument = + XmlDocument.parse(convert.utf8.decode(rootFileEntry.content)); + var opfNamespace = 'http://www.idpf.org/2007/opf'; + var packageNode = containerDocument + .findElements('package', namespace: opfNamespace) + .firstWhere((XmlElement? elem) => elem != null); + var result = EpubPackage(); + var epubVersionValue = packageNode.getAttribute('version'); + if (epubVersionValue == '2.0') { + result.Version = EpubVersion.Epub2; + } else if (epubVersionValue == '3.0') { + result.Version = EpubVersion.Epub3; + } else { + throw Exception('Unsupported EPUB version: $epubVersionValue.'); + } + var metadataNode = packageNode + .findElements('metadata', namespace: opfNamespace) + .cast() + .firstWhere((XmlElement? elem) => elem != null); + if (metadataNode == null) { + throw Exception('EPUB parsing error: metadata not found in the package.'); + } + var metadata = readMetadata(metadataNode, result.Version); + result.Metadata = metadata; + var manifestNode = packageNode + .findElements('manifest', namespace: opfNamespace) + .cast() + .firstWhere((XmlElement? elem) => elem != null); + if (manifestNode == null) { + throw Exception('EPUB parsing error: manifest not found in the package.'); + } + var manifest = readManifest(manifestNode); + result.Manifest = manifest; + + var spineNode = packageNode + .findElements('spine', namespace: opfNamespace) + .cast() + .firstWhere((XmlElement? elem) => elem != null); + if (spineNode == null) { + throw Exception('EPUB parsing error: spine not found in the package.'); + } + var spine = readSpine(spineNode); + result.Spine = spine; + var guideNode = packageNode + .findElements('guide', namespace: opfNamespace) + .firstWhereOrNull((XmlElement? elem) => elem != null); + if (guideNode != null) { + var guide = readGuide(guideNode); + result.Guide = guide; + } + return result; + } + + static EpubSpine readSpine(XmlElement spineNode) { + var result = EpubSpine(); + result.Items = []; + var tocAttribute = spineNode.getAttribute('toc'); + result.TableOfContents = tocAttribute; + var pageProgression = spineNode.getAttribute('page-progression-direction'); + result.ltr = + ((pageProgression == null) || pageProgression.toLowerCase() == 'ltr'); + spineNode.children + .whereType() + .forEach((XmlElement spineItemNode) { + if (spineItemNode.name.local.toLowerCase() == 'itemref') { + var spineItemRef = EpubSpineItemRef(); + var idRefAttribute = spineItemNode.getAttribute('idref'); + if (idRefAttribute == null || idRefAttribute.isEmpty) { + throw Exception('Incorrect EPUB spine: item ID ref is missing'); + } + spineItemRef.IdRef = idRefAttribute; + var linearAttribute = spineItemNode.getAttribute('linear'); + spineItemRef.IsLinear = + linearAttribute == null || (linearAttribute.toLowerCase() == 'no'); + result.Items!.add(spineItemRef); + } + }); + return result; + } +} diff --git a/packages/epubx/lib/src/readers/root_file_path_reader.dart b/packages/epubx/lib/src/readers/root_file_path_reader.dart new file mode 100644 index 0000000..0110414 --- /dev/null +++ b/packages/epubx/lib/src/readers/root_file_path_reader.dart @@ -0,0 +1,36 @@ +import 'dart:async'; + +import 'package:archive/archive.dart'; +import 'dart:convert' as convert; +import 'package:collection/collection.dart' show IterableExtension; +import 'package:xml/xml.dart' as xml; + +class RootFilePathReader { + static Future getRootFilePath(Archive epubArchive) async { + const EPUB_CONTAINER_FILE_PATH = 'META-INF/container.xml'; + + var containerFileEntry = epubArchive.files.firstWhereOrNull( + (ArchiveFile file) => file.name == EPUB_CONTAINER_FILE_PATH); + if (containerFileEntry == null) { + throw Exception( + 'EPUB parsing error: $EPUB_CONTAINER_FILE_PATH file not found in archive.'); + } + + var containerDocument = + xml.XmlDocument.parse(convert.utf8.decode(containerFileEntry.content)); + var packageElement = containerDocument + .findAllElements('container', + namespace: 'urn:oasis:names:tc:opendocument:xmlns:container') + .firstWhereOrNull((xml.XmlElement? elem) => elem != null); + if (packageElement == null) { + throw Exception('EPUB parsing error: Invalid epub container'); + } + + var rootFileElement = packageElement.descendants.firstWhereOrNull( + (xml.XmlNode testElem) => + (testElem is xml.XmlElement) && + 'rootfile' == testElem.name.local) as xml.XmlElement; + + return rootFileElement.getAttribute('full-path'); + } +} diff --git a/packages/epubx/lib/src/readers/schema_reader.dart b/packages/epubx/lib/src/readers/schema_reader.dart new file mode 100644 index 0000000..948759c --- /dev/null +++ b/packages/epubx/lib/src/readers/schema_reader.dart @@ -0,0 +1,31 @@ +import 'dart:async'; + +import 'package:archive/archive.dart'; + +import '../entities/epub_schema.dart'; +import '../utils/zip_path_utils.dart'; +import 'navigation_reader.dart'; +import 'package_reader.dart'; +import 'root_file_path_reader.dart'; + +class SchemaReader { + static Future readSchema(Archive epubArchive) async { + var result = EpubSchema(); + + var rootFilePath = (await RootFilePathReader.getRootFilePath(epubArchive))!; + var contentDirectoryPath = ZipPathUtils.getDirectoryPath(rootFilePath); + result.ContentDirectoryPath = contentDirectoryPath; + + var package = await PackageReader.readPackage(epubArchive, rootFilePath); + result.Package = package; + + try { + result.Navigation = await NavigationReader.readNavigation( + epubArchive, contentDirectoryPath, package); + } catch (_) { + result.Navigation = null; + } + + return result; + } +} diff --git a/packages/epubx/lib/src/ref_entities/epub_book_ref.dart b/packages/epubx/lib/src/ref_entities/epub_book_ref.dart new file mode 100644 index 0000000..7ddd4b1 --- /dev/null +++ b/packages/epubx/lib/src/ref_entities/epub_book_ref.dart @@ -0,0 +1,62 @@ +import 'dart:async'; + +import 'package:archive/archive.dart'; +import 'package:image/image.dart'; +import 'package:quiver/collection.dart' as collections; +import 'package:quiver/core.dart'; + +import '../entities/epub_schema.dart'; +import '../readers/book_cover_reader.dart'; +import '../readers/chapter_reader.dart'; +import 'epub_chapter_ref.dart'; +import 'epub_content_ref.dart'; + +class EpubBookRef { + Archive? _epubArchive; + + String? Title; + String? Author; + List? AuthorList; + EpubSchema? Schema; + EpubContentRef? Content; + EpubBookRef(Archive epubArchive) { + _epubArchive = epubArchive; + } + + @override + int get hashCode { + var objects = [ + Title.hashCode, + Author.hashCode, + Schema.hashCode, + Content.hashCode, + ...AuthorList?.map((author) => author.hashCode) ?? [0], + ]; + return hashObjects(objects); + } + + @override + bool operator ==(other) { + if (!(other is EpubBookRef)) { + return false; + } + + return Title == other.Title && + Author == other.Author && + Schema == other.Schema && + Content == other.Content && + collections.listsEqual(AuthorList, other.AuthorList); + } + + Archive? EpubArchive() { + return _epubArchive; + } + + Future> getChapters() async { + return ChapterReader.getChapters(this); + } + + Future readCover() async { + return await BookCoverReader.readBookCover(this); + } +} diff --git a/packages/epubx/lib/src/ref_entities/epub_byte_content_file_ref.dart b/packages/epubx/lib/src/ref_entities/epub_byte_content_file_ref.dart new file mode 100644 index 0000000..85a2792 --- /dev/null +++ b/packages/epubx/lib/src/ref_entities/epub_byte_content_file_ref.dart @@ -0,0 +1,12 @@ +import 'dart:async'; + +import 'epub_book_ref.dart'; +import 'epub_content_file_ref.dart'; + +class EpubByteContentFileRef extends EpubContentFileRef { + EpubByteContentFileRef(EpubBookRef epubBookRef) : super(epubBookRef); + + Future> readContent() { + return readContentAsBytes(); + } +} diff --git a/packages/epubx/lib/src/ref_entities/epub_chapter_ref.dart b/packages/epubx/lib/src/ref_entities/epub_chapter_ref.dart new file mode 100644 index 0000000..4a0c879 --- /dev/null +++ b/packages/epubx/lib/src/ref_entities/epub_chapter_ref.dart @@ -0,0 +1,52 @@ +import 'dart:async'; + +import 'package:quiver/collection.dart' as collections; +import 'package:quiver/core.dart'; + +import 'epub_text_content_file_ref.dart'; + +class EpubChapterRef { + EpubTextContentFileRef? epubTextContentFileRef; + + String? Title; + String? ContentFileName; + String? Anchor; + List? SubChapters; + + EpubChapterRef(EpubTextContentFileRef? epubTextContentFileRef) { + this.epubTextContentFileRef = epubTextContentFileRef; + } + + @override + int get hashCode { + var objects = [ + Title.hashCode, + ContentFileName.hashCode, + Anchor.hashCode, + epubTextContentFileRef.hashCode, + ...SubChapters?.map((subChapter) => subChapter.hashCode) ?? [0], + ]; + return hashObjects(objects); + } + + @override + bool operator ==(other) { + if (!(other is EpubChapterRef)) { + return false; + } + return Title == other.Title && + ContentFileName == other.ContentFileName && + Anchor == other.Anchor && + epubTextContentFileRef == other.epubTextContentFileRef && + collections.listsEqual(SubChapters, other.SubChapters); + } + + Future readHtmlContent() async { + return epubTextContentFileRef!.readContentAsText(); + } + + @override + String toString() { + return 'Title: $Title, Subchapter count: ${SubChapters!.length}'; + } +} diff --git a/packages/epubx/lib/src/ref_entities/epub_content_file_ref.dart b/packages/epubx/lib/src/ref_entities/epub_content_file_ref.dart new file mode 100644 index 0000000..028f70b --- /dev/null +++ b/packages/epubx/lib/src/ref_entities/epub_content_file_ref.dart @@ -0,0 +1,95 @@ +import 'dart:async'; +import 'dart:convert' as convert; + +import 'package:archive/archive.dart'; +import 'package:collection/collection.dart' show IterableExtension; +import 'package:quiver/core.dart'; + +import '../entities/epub_content_type.dart'; +import '../utils/zip_path_utils.dart'; +import 'epub_book_ref.dart'; + +abstract class EpubContentFileRef { + late EpubBookRef epubBookRef; + + String? FileName; + + EpubContentType? ContentType; + String? ContentMimeType; + EpubContentFileRef(EpubBookRef epubBookRef) { + this.epubBookRef = epubBookRef; + } + + @override + int get hashCode => + hash3(FileName.hashCode, ContentMimeType.hashCode, ContentType.hashCode); + + @override + bool operator ==(other) { + if (!(other is EpubContentFileRef)) { + return false; + } + + return (other.FileName == FileName && + other.ContentMimeType == ContentMimeType && + other.ContentType == ContentType); + } + + ArchiveFile getContentFileEntry() { + var contentFilePath = ZipPathUtils.combine( + epubBookRef.Schema!.ContentDirectoryPath, FileName) ?? + ''; + final files = epubBookRef.EpubArchive()!.files; + + // Tolerant lookup. Manifest hrefs and actual zip entry names often differ + // by case or URL-encoding (e.g. spaces as %20), which a strict `==` match + // misses and then throws on — failing the whole book. Try progressively + // looser matches before giving up. (calibre-web-companion patch.) + var contentFileEntry = + files.firstWhereOrNull((ArchiveFile x) => x.name == contentFilePath); + if (contentFileEntry == null) { + final decoded = Uri.decodeFull(contentFilePath); + final lower = decoded.toLowerCase(); + contentFileEntry = files.firstWhereOrNull( + (ArchiveFile x) => Uri.decodeFull(x.name).toLowerCase() == lower); + } + if (contentFileEntry == null) { + // Last resort: match on bare filename (handles differing directory paths). + final base = Uri.decodeFull(contentFilePath).split('/').last.toLowerCase(); + contentFileEntry = files.firstWhereOrNull( + (ArchiveFile x) => + Uri.decodeFull(x.name).split('/').last.toLowerCase() == base); + } + if (contentFileEntry == null) { + throw Exception( + 'EPUB parsing error: file $contentFilePath not found in archive.'); + } + return contentFileEntry; + } + + List getContentStream() { + return openContentStream(getContentFileEntry()); + } + + List openContentStream(ArchiveFile contentFileEntry) { + var contentStream = []; + if (contentFileEntry.content == null) { + throw Exception( + 'Incorrect EPUB file: content file \"$FileName\" specified in manifest is not found.'); + } + contentStream.addAll(contentFileEntry.content); + return contentStream; + } + + Future> readContentAsBytes() async { + var contentFileEntry = getContentFileEntry(); + var content = openContentStream(contentFileEntry); + return content; + } + + Future readContentAsText() async { + var contentStream = getContentStream(); + var result = convert.utf8.decode(contentStream); + return result; + } +} diff --git a/packages/epubx/lib/src/ref_entities/epub_content_ref.dart b/packages/epubx/lib/src/ref_entities/epub_content_ref.dart new file mode 100644 index 0000000..287a6ed --- /dev/null +++ b/packages/epubx/lib/src/ref_entities/epub_content_ref.dart @@ -0,0 +1,53 @@ +import 'package:quiver/collection.dart' as collections; +import 'package:quiver/core.dart'; + +import 'epub_byte_content_file_ref.dart'; +import 'epub_content_file_ref.dart'; +import 'epub_text_content_file_ref.dart'; + +class EpubContentRef { + Map? Html; + Map? Css; + Map? Images; + Map? Fonts; + Map? AllFiles; + + EpubContentRef() { + Html = {}; + Css = {}; + Images = {}; + Fonts = {}; + AllFiles = {}; + } + + @override + int get hashCode { + var objects = [ + ...Html!.keys.map((key) => key.hashCode), + ...Html!.values.map((value) => value.hashCode), + ...Css!.keys.map((key) => key.hashCode), + ...Css!.values.map((value) => value.hashCode), + ...Images!.keys.map((key) => key.hashCode), + ...Images!.values.map((value) => value.hashCode), + ...Fonts!.keys.map((key) => key.hashCode), + ...Fonts!.values.map((value) => value.hashCode), + ...AllFiles!.keys.map((key) => key.hashCode), + ...AllFiles!.values.map((value) => value.hashCode) + ]; + + return hashObjects(objects); + } + + @override + bool operator ==(other) { + if (!(other is EpubContentRef)) { + return false; + } + + return collections.mapsEqual(Html, other.Html) && + collections.mapsEqual(Css, other.Css) && + collections.mapsEqual(Images, other.Images) && + collections.mapsEqual(Fonts, other.Fonts) && + collections.mapsEqual(AllFiles, other.AllFiles); + } +} diff --git a/packages/epubx/lib/src/ref_entities/epub_text_content_file_ref.dart b/packages/epubx/lib/src/ref_entities/epub_text_content_file_ref.dart new file mode 100644 index 0000000..647458c --- /dev/null +++ b/packages/epubx/lib/src/ref_entities/epub_text_content_file_ref.dart @@ -0,0 +1,12 @@ +import 'dart:async'; + +import 'epub_book_ref.dart'; +import 'epub_content_file_ref.dart'; + +class EpubTextContentFileRef extends EpubContentFileRef { + EpubTextContentFileRef(EpubBookRef epubBookRef) : super(epubBookRef); + + Future ReadContentAsync() async { + return readContentAsText(); + } +} diff --git a/packages/epubx/lib/src/schema/navigation/epub_metadata.dart b/packages/epubx/lib/src/schema/navigation/epub_metadata.dart new file mode 100644 index 0000000..b27ae3e --- /dev/null +++ b/packages/epubx/lib/src/schema/navigation/epub_metadata.dart @@ -0,0 +1,22 @@ +import 'package:quiver/core.dart'; + +class EpubNavigationContent { + String? Id; + String? Source; + + @override + int get hashCode => hash2(Id.hashCode, Source.hashCode); + + @override + bool operator ==(other) { + if (!(other is EpubNavigationContent)) { + return false; + } + return Id == other.Id && Source == other.Source; + } + + @override + String toString() { + return 'Source: $Source'; + } +} diff --git a/packages/epubx/lib/src/schema/navigation/epub_navigation.dart b/packages/epubx/lib/src/schema/navigation/epub_navigation.dart new file mode 100644 index 0000000..f6fc5e3 --- /dev/null +++ b/packages/epubx/lib/src/schema/navigation/epub_navigation.dart @@ -0,0 +1,51 @@ +import 'package:quiver/collection.dart' as collections; +import 'package:quiver/core.dart'; + +import 'epub_navigation_doc_author.dart'; +import 'epub_navigation_doc_title.dart'; +import 'epub_navigation_head.dart'; +import 'epub_navigation_list.dart'; +import 'epub_navigation_map.dart'; +import 'epub_navigation_page_list.dart'; + +class EpubNavigation { + EpubNavigationHead? Head; + EpubNavigationDocTitle? DocTitle; + List? DocAuthors; + EpubNavigationMap? NavMap; + EpubNavigationPageList? PageList; + List? NavLists; + + @override + int get hashCode { + var objects = [ + Head.hashCode, + DocTitle.hashCode, + NavMap.hashCode, + PageList.hashCode, + ...DocAuthors?.map((author) => author.hashCode) ?? [0], + ...NavLists?.map((navList) => navList.hashCode) ?? [0] + ]; + return hashObjects(objects); + } + + @override + bool operator ==(other) { + var otherAs = other as EpubNavigation?; + if (otherAs == null) { + return false; + } + + if (!collections.listsEqual(DocAuthors, otherAs.DocAuthors)) { + return false; + } + if (!collections.listsEqual(NavLists, otherAs.NavLists)) { + return false; + } + + return Head == otherAs.Head && + DocTitle == otherAs.DocTitle && + NavMap == otherAs.NavMap && + PageList == otherAs.PageList; + } +} diff --git a/packages/epubx/lib/src/schema/navigation/epub_navigation_doc_author.dart b/packages/epubx/lib/src/schema/navigation/epub_navigation_doc_author.dart new file mode 100644 index 0000000..b5275fe --- /dev/null +++ b/packages/epubx/lib/src/schema/navigation/epub_navigation_doc_author.dart @@ -0,0 +1,24 @@ +import 'package:quiver/collection.dart' as collections; +import 'package:quiver/core.dart'; + +class EpubNavigationDocAuthor { + List? Authors; + + EpubNavigationDocAuthor() { + Authors = []; + } + + @override + int get hashCode { + var objects = [...Authors!.map((author) => author.hashCode)]; + return hashObjects(objects); + } + + @override + bool operator ==(other) { + var otherAs = other as EpubNavigationDocAuthor?; + if (otherAs == null) return false; + + return collections.listsEqual(Authors, otherAs.Authors); + } +} diff --git a/packages/epubx/lib/src/schema/navigation/epub_navigation_doc_title.dart b/packages/epubx/lib/src/schema/navigation/epub_navigation_doc_title.dart new file mode 100644 index 0000000..3e48cfc --- /dev/null +++ b/packages/epubx/lib/src/schema/navigation/epub_navigation_doc_title.dart @@ -0,0 +1,24 @@ +import 'package:quiver/collection.dart' as collections; +import 'package:quiver/core.dart'; + +class EpubNavigationDocTitle { + List? Titles; + + EpubNavigationDocTitle() { + Titles = []; + } + + @override + int get hashCode { + var objects = [...Titles!.map((title) => title.hashCode)]; + return hashObjects(objects); + } + + @override + bool operator ==(other) { + var otherAs = other as EpubNavigationDocTitle?; + if (otherAs == null) return false; + + return collections.listsEqual(Titles, otherAs.Titles); + } +} diff --git a/packages/epubx/lib/src/schema/navigation/epub_navigation_head.dart b/packages/epubx/lib/src/schema/navigation/epub_navigation_head.dart new file mode 100644 index 0000000..4b2e2e0 --- /dev/null +++ b/packages/epubx/lib/src/schema/navigation/epub_navigation_head.dart @@ -0,0 +1,28 @@ +import 'package:quiver/collection.dart' as collections; +import 'package:quiver/core.dart'; + +import 'epub_navigation_head_meta.dart'; + +class EpubNavigationHead { + List? Metadata; + + EpubNavigationHead() { + Metadata = []; + } + + @override + int get hashCode { + var objects = [...Metadata!.map((meta) => meta.hashCode)]; + return hashObjects(objects); + } + + @override + bool operator ==(other) { + var otherAs = other as EpubNavigationHead?; + if (otherAs == null) { + return false; + } + + return collections.listsEqual(Metadata, otherAs.Metadata); + } +} diff --git a/packages/epubx/lib/src/schema/navigation/epub_navigation_head_meta.dart b/packages/epubx/lib/src/schema/navigation/epub_navigation_head_meta.dart new file mode 100644 index 0000000..a9e3bb2 --- /dev/null +++ b/packages/epubx/lib/src/schema/navigation/epub_navigation_head_meta.dart @@ -0,0 +1,22 @@ +import 'package:quiver/core.dart'; + +class EpubNavigationHeadMeta { + String? Name; + String? Content; + String? Scheme; + + @override + int get hashCode => hash3(Name.hashCode, Content.hashCode, Scheme.hashCode); + + @override + bool operator ==(other) { + var otherAs = other as EpubNavigationHeadMeta?; + if (otherAs == null) { + return false; + } + + return Name == otherAs.Name && + Content == otherAs.Content && + Scheme == otherAs.Scheme; + } +} diff --git a/packages/epubx/lib/src/schema/navigation/epub_navigation_label.dart b/packages/epubx/lib/src/schema/navigation/epub_navigation_label.dart new file mode 100644 index 0000000..910a1d5 --- /dev/null +++ b/packages/epubx/lib/src/schema/navigation/epub_navigation_label.dart @@ -0,0 +1,18 @@ +class EpubNavigationLabel { + String? Text; + + @override + int get hashCode => Text.hashCode; + + @override + bool operator ==(other) { + var otherAs = other as EpubNavigationLabel?; + if (otherAs == null) return false; + return Text == otherAs.Text; + } + + @override + String toString() { + return Text!; + } +} diff --git a/packages/epubx/lib/src/schema/navigation/epub_navigation_list.dart b/packages/epubx/lib/src/schema/navigation/epub_navigation_list.dart new file mode 100644 index 0000000..60c5504 --- /dev/null +++ b/packages/epubx/lib/src/schema/navigation/epub_navigation_list.dart @@ -0,0 +1,41 @@ +import 'package:quiver/collection.dart' as collections; +import 'package:quiver/core.dart'; + +import 'epub_navigation_label.dart'; +import 'epub_navigation_target.dart'; + +class EpubNavigationList { + String? Id; + String? Class; + List? NavigationLabels; + List? NavigationTargets; + + @override + int get hashCode { + var objects = [ + Id.hashCode, + Class.hashCode, + ...NavigationLabels?.map((label) => label.hashCode) ?? [0], + ...NavigationTargets?.map((target) => target.hashCode) ?? [0] + ]; + return hashObjects(objects); + } + + @override + bool operator ==(other) { + var otherAs = other as EpubNavigationList?; + if (otherAs == null) return false; + + if (!(Id == otherAs.Id && Class == otherAs.Class)) { + return false; + } + + if (!collections.listsEqual(NavigationLabels, otherAs.NavigationLabels)) { + return false; + } + if (!collections.listsEqual(NavigationTargets, otherAs.NavigationTargets)) { + return false; + } + return true; + } +} diff --git a/packages/epubx/lib/src/schema/navigation/epub_navigation_map.dart b/packages/epubx/lib/src/schema/navigation/epub_navigation_map.dart new file mode 100644 index 0000000..24208af --- /dev/null +++ b/packages/epubx/lib/src/schema/navigation/epub_navigation_map.dart @@ -0,0 +1,21 @@ +import 'package:quiver/collection.dart' as collections; +import 'package:quiver/core.dart'; + +import 'epub_navigation_point.dart'; + +class EpubNavigationMap { + List? Points; + + @override + int get hashCode { + return hashObjects(Points?.map((point) => point.hashCode) ?? [0]); + } + + @override + bool operator ==(other) { + var otherAs = other as EpubNavigationMap?; + if (otherAs == null) return false; + + return collections.listsEqual(Points, otherAs.Points); + } +} diff --git a/packages/epubx/lib/src/schema/navigation/epub_navigation_page_list.dart b/packages/epubx/lib/src/schema/navigation/epub_navigation_page_list.dart new file mode 100644 index 0000000..f0ae694 --- /dev/null +++ b/packages/epubx/lib/src/schema/navigation/epub_navigation_page_list.dart @@ -0,0 +1,21 @@ +import 'package:quiver/collection.dart' as collections; +import 'package:quiver/core.dart'; + +import 'epub_navigation_page_target.dart'; + +class EpubNavigationPageList { + List? Targets; + + @override + int get hashCode { + return hashObjects(Targets?.map((target) => target.hashCode) ?? [0]); + } + + @override + bool operator ==(other) { + var otherAs = other as EpubNavigationPageList?; + if (otherAs == null) return false; + + return collections.listsEqual(Targets, otherAs.Targets); + } +} diff --git a/packages/epubx/lib/src/schema/navigation/epub_navigation_page_target.dart b/packages/epubx/lib/src/schema/navigation/epub_navigation_page_target.dart new file mode 100644 index 0000000..76ebd08 --- /dev/null +++ b/packages/epubx/lib/src/schema/navigation/epub_navigation_page_target.dart @@ -0,0 +1,49 @@ +import 'package:quiver/collection.dart' as collections; +import 'package:quiver/core.dart'; + +import 'epub_metadata.dart'; +import 'epub_navigation_label.dart'; +import 'epub_navigation_page_target_type.dart'; + +class EpubNavigationPageTarget { + String? Id; + String? Value; + EpubNavigationPageTargetType? Type; + String? Class; + String? PlayOrder; + List? NavigationLabels; + EpubNavigationContent? Content; + + @override + int get hashCode { + var objects = [ + Id.hashCode, + Value.hashCode, + Type.hashCode, + Class.hashCode, + PlayOrder.hashCode, + Content.hashCode, + ...NavigationLabels?.map((label) => label.hashCode) ?? [0] + ]; + return hashObjects(objects); + } + + @override + bool operator ==(other) { + var otherAs = other as EpubNavigationPageTarget?; + if (otherAs == null) { + return false; + } + + if (!(Id == otherAs.Id && + Value == otherAs.Value && + Type == otherAs.Type && + Class == otherAs.Class && + PlayOrder == otherAs.PlayOrder && + Content == otherAs.Content)) { + return false; + } + + return collections.listsEqual(NavigationLabels, otherAs.NavigationLabels); + } +} diff --git a/packages/epubx/lib/src/schema/navigation/epub_navigation_page_target_type.dart b/packages/epubx/lib/src/schema/navigation/epub_navigation_page_target_type.dart new file mode 100644 index 0000000..49ee43b --- /dev/null +++ b/packages/epubx/lib/src/schema/navigation/epub_navigation_page_target_type.dart @@ -0,0 +1 @@ +enum EpubNavigationPageTargetType { UNDEFINED, FRONT, NORMAL, SPECIAL } diff --git a/packages/epubx/lib/src/schema/navigation/epub_navigation_point.dart b/packages/epubx/lib/src/schema/navigation/epub_navigation_point.dart new file mode 100644 index 0000000..fc12c65 --- /dev/null +++ b/packages/epubx/lib/src/schema/navigation/epub_navigation_point.dart @@ -0,0 +1,52 @@ +import 'package:quiver/collection.dart' as collections; +import 'package:quiver/core.dart'; + +import 'epub_metadata.dart'; +import 'epub_navigation_label.dart'; + +class EpubNavigationPoint { + String? Id; + String? Class; + String? PlayOrder; + List? NavigationLabels; + EpubNavigationContent? Content; + List? ChildNavigationPoints; + + @override + int get hashCode { + var objects = [ + Id.hashCode, + Class.hashCode, + PlayOrder.hashCode, + Content.hashCode, + ...NavigationLabels!.map((label) => label.hashCode), + ...ChildNavigationPoints!.map((point) => point.hashCode) + ]; + return hashObjects(objects); + } + + @override + bool operator ==(other) { + var otherAs = other as EpubNavigationPoint?; + if (otherAs == null) { + return false; + } + + if (!collections.listsEqual(NavigationLabels, otherAs.NavigationLabels)) { + return false; + } + + if (!collections.listsEqual( + ChildNavigationPoints, otherAs.ChildNavigationPoints)) return false; + + return Id == otherAs.Id && + Class == otherAs.Class && + PlayOrder == otherAs.PlayOrder && + Content == otherAs.Content; + } + + @override + String toString() { + return 'Id: $Id, Content.Source: ${Content!.Source}'; + } +} diff --git a/packages/epubx/lib/src/schema/navigation/epub_navigation_target.dart b/packages/epubx/lib/src/schema/navigation/epub_navigation_target.dart new file mode 100644 index 0000000..8ef2b7a --- /dev/null +++ b/packages/epubx/lib/src/schema/navigation/epub_navigation_target.dart @@ -0,0 +1,43 @@ +import 'package:quiver/collection.dart' as collections; +import 'package:quiver/core.dart'; + +import 'epub_metadata.dart'; +import 'epub_navigation_label.dart'; + +class EpubNavigationTarget { + String? Id; + String? Class; + String? Value; + String? PlayOrder; + List? NavigationLabels; + EpubNavigationContent? Content; + + @override + int get hashCode { + var objects = [ + Id.hashCode, + Class.hashCode, + Value.hashCode, + PlayOrder.hashCode, + Content.hashCode, + ...NavigationLabels!.map((label) => label.hashCode) + ]; + return hashObjects(objects); + } + + @override + bool operator ==(other) { + var otherAs = other as EpubNavigationTarget?; + if (otherAs == null) return false; + + if (!(Id == otherAs.Id && + Class == otherAs.Class && + Value == otherAs.Value && + PlayOrder == otherAs.PlayOrder && + Content == otherAs.Content)) { + return false; + } + + return collections.listsEqual(NavigationLabels, otherAs.NavigationLabels); + } +} diff --git a/packages/epubx/lib/src/schema/opf/epub_guide.dart b/packages/epubx/lib/src/schema/opf/epub_guide.dart new file mode 100644 index 0000000..f715bc4 --- /dev/null +++ b/packages/epubx/lib/src/schema/opf/epub_guide.dart @@ -0,0 +1,29 @@ +import 'package:quiver/collection.dart' as collections; +import 'package:quiver/core.dart'; + +import 'epub_guide_reference.dart'; + +class EpubGuide { + List? Items; + + EpubGuide() { + Items = []; + } + + @override + int get hashCode { + var objects = []; + objects.addAll(Items!.map((item) => item.hashCode)); + return hashObjects(objects); + } + + @override + bool operator ==(other) { + var otherAs = other as EpubGuide?; + if (otherAs == null) { + return false; + } + + return collections.listsEqual(Items, otherAs.Items); + } +} diff --git a/packages/epubx/lib/src/schema/opf/epub_guide_reference.dart b/packages/epubx/lib/src/schema/opf/epub_guide_reference.dart new file mode 100644 index 0000000..bb56700 --- /dev/null +++ b/packages/epubx/lib/src/schema/opf/epub_guide_reference.dart @@ -0,0 +1,27 @@ +import 'package:quiver/core.dart'; + +class EpubGuideReference { + String? Type; + String? Title; + String? Href; + + @override + int get hashCode => hash3(Type.hashCode, Title.hashCode, Href.hashCode); + + @override + bool operator ==(other) { + var otherAs = other as EpubGuideReference?; + if (otherAs == null) { + return false; + } + + return Type == otherAs.Type && + Title == otherAs.Title && + Href == otherAs.Href; + } + + @override + String toString() { + return 'Type: $Type, Href: $Href'; + } +} diff --git a/packages/epubx/lib/src/schema/opf/epub_manifest.dart b/packages/epubx/lib/src/schema/opf/epub_manifest.dart new file mode 100644 index 0000000..1c45f22 --- /dev/null +++ b/packages/epubx/lib/src/schema/opf/epub_manifest.dart @@ -0,0 +1,26 @@ +import 'package:quiver/collection.dart' as collections; +import 'package:quiver/core.dart'; + +import 'epub_manifest_item.dart'; + +class EpubManifest { + List? Items; + + EpubManifest() { + Items = []; + } + + @override + int get hashCode { + return hashObjects(Items!.map((item) => item.hashCode)); + } + + @override + bool operator ==(other) { + var otherAs = other as EpubManifest?; + if (otherAs == null) { + return false; + } + return collections.listsEqual(Items, otherAs.Items); + } +} diff --git a/packages/epubx/lib/src/schema/opf/epub_manifest_item.dart b/packages/epubx/lib/src/schema/opf/epub_manifest_item.dart new file mode 100644 index 0000000..6af69ee --- /dev/null +++ b/packages/epubx/lib/src/schema/opf/epub_manifest_item.dart @@ -0,0 +1,49 @@ +import 'package:quiver/core.dart'; + +class EpubManifestItem { + String? Id; + String? Href; + String? MediaType; + String? MediaOverlay; + String? RequiredNamespace; + String? RequiredModules; + String? Fallback; + String? FallbackStyle; + String? Properties; + + @override + int get hashCode => hashObjects([ + Id.hashCode, + Href.hashCode, + MediaType.hashCode, + MediaOverlay.hashCode, + RequiredNamespace.hashCode, + RequiredModules.hashCode, + Fallback.hashCode, + FallbackStyle.hashCode, + Properties.hashCode + ]); + + @override + bool operator ==(other) { + var otherAs = other as EpubManifestItem?; + if (otherAs == null) { + return false; + } + + return Id == otherAs.Id && + Href == otherAs.Href && + MediaType == otherAs.MediaType && + MediaOverlay == otherAs.MediaOverlay && + RequiredNamespace == otherAs.RequiredNamespace && + RequiredModules == otherAs.RequiredModules && + Fallback == otherAs.Fallback && + FallbackStyle == otherAs.FallbackStyle && + Properties == otherAs.Properties; + } + + @override + String toString() { + return 'Id: $Id, Href = $Href, MediaType = $MediaType, Properties = $Properties, MediaOverlay = $MediaOverlay'; + } +} diff --git a/packages/epubx/lib/src/schema/opf/epub_metadata.dart b/packages/epubx/lib/src/schema/opf/epub_metadata.dart new file mode 100644 index 0000000..88b99f6 --- /dev/null +++ b/packages/epubx/lib/src/schema/opf/epub_metadata.dart @@ -0,0 +1,78 @@ +import 'package:quiver/collection.dart' as collections; +import 'package:quiver/core.dart'; + +import 'epub_metadata_contributor.dart'; +import 'epub_metadata_creator.dart'; +import 'epub_metadata_date.dart'; +import 'epub_metadata_identifier.dart'; +import 'epub_metadata_meta.dart'; + +class EpubMetadata { + List? Titles; + List? Creators; + List? Subjects; + String? Description; + List? Publishers; + List? Contributors; + List? Dates; + List? Types; + List? Formats; + List? Identifiers; + List? Sources; + List? Languages; + List? Relations; + List? Coverages; + List? Rights; + List? MetaItems; + + @override + int get hashCode { + var objects = [ + ...Titles!.map((title) => title.hashCode), + ...Creators!.map((creator) => creator.hashCode), + ...Subjects!.map((subject) => subject.hashCode), + ...Publishers!.map((publisher) => publisher.hashCode), + ...Contributors!.map((contributor) => contributor.hashCode), + ...Dates!.map((date) => date.hashCode), + ...Types!.map((type) => type.hashCode), + ...Formats!.map((format) => format.hashCode), + ...Identifiers!.map((identifier) => identifier.hashCode), + ...Sources!.map((source) => source.hashCode), + ...Languages!.map((language) => language.hashCode), + ...Relations!.map((relation) => relation.hashCode), + ...Coverages!.map((coverage) => coverage.hashCode), + ...Rights!.map((right) => right.hashCode), + ...MetaItems!.map((metaItem) => metaItem.hashCode), + Description.hashCode + ]; + + return hashObjects(objects); + } + + @override + bool operator ==(other) { + var otherAs = other as EpubMetadata?; + if (otherAs == null) return false; + if (Description != otherAs.Description) return false; + + if (!collections.listsEqual(Titles, otherAs.Titles) || + !collections.listsEqual(Creators, otherAs.Creators) || + !collections.listsEqual(Subjects, otherAs.Subjects) || + !collections.listsEqual(Publishers, otherAs.Publishers) || + !collections.listsEqual(Contributors, otherAs.Contributors) || + !collections.listsEqual(Dates, otherAs.Dates) || + !collections.listsEqual(Types, otherAs.Types) || + !collections.listsEqual(Formats, otherAs.Formats) || + !collections.listsEqual(Identifiers, otherAs.Identifiers) || + !collections.listsEqual(Sources, otherAs.Sources) || + !collections.listsEqual(Languages, otherAs.Languages) || + !collections.listsEqual(Relations, otherAs.Relations) || + !collections.listsEqual(Coverages, otherAs.Coverages) || + !collections.listsEqual(Rights, otherAs.Rights) || + !collections.listsEqual(MetaItems, otherAs.MetaItems)) { + return false; + } + + return true; + } +} diff --git a/packages/epubx/lib/src/schema/opf/epub_metadata_contributor.dart b/packages/epubx/lib/src/schema/opf/epub_metadata_contributor.dart new file mode 100644 index 0000000..c10796c --- /dev/null +++ b/packages/epubx/lib/src/schema/opf/epub_metadata_contributor.dart @@ -0,0 +1,21 @@ +import 'package:quiver/core.dart'; + +class EpubMetadataContributor { + String? Contributor; + String? FileAs; + String? Role; + + @override + int get hashCode => + hash3(Contributor.hashCode, FileAs.hashCode, Role.hashCode); + + @override + bool operator ==(other) { + var otherAs = other as EpubMetadataContributor?; + if (otherAs == null) return false; + + return Contributor == otherAs.Contributor && + FileAs == otherAs.FileAs && + Role == otherAs.Role; + } +} diff --git a/packages/epubx/lib/src/schema/opf/epub_metadata_creator.dart b/packages/epubx/lib/src/schema/opf/epub_metadata_creator.dart new file mode 100644 index 0000000..d81e1ea --- /dev/null +++ b/packages/epubx/lib/src/schema/opf/epub_metadata_creator.dart @@ -0,0 +1,19 @@ +import 'package:quiver/core.dart'; + +class EpubMetadataCreator { + String? Creator; + String? FileAs; + String? Role; + + @override + int get hashCode => hash3(Creator.hashCode, FileAs.hashCode, Role.hashCode); + + @override + bool operator ==(other) { + var otherAs = other as EpubMetadataCreator?; + if (otherAs == null) return false; + return Creator == otherAs.Creator && + FileAs == otherAs.FileAs && + Role == otherAs.Role; + } +} diff --git a/packages/epubx/lib/src/schema/opf/epub_metadata_date.dart b/packages/epubx/lib/src/schema/opf/epub_metadata_date.dart new file mode 100644 index 0000000..37983e2 --- /dev/null +++ b/packages/epubx/lib/src/schema/opf/epub_metadata_date.dart @@ -0,0 +1,16 @@ +import 'package:quiver/core.dart'; + +class EpubMetadataDate { + String? Date; + String? Event; + + @override + int get hashCode => hash2(Date.hashCode, Event.hashCode); + + @override + bool operator ==(other) { + var otherAs = other as EpubMetadataDate?; + if (otherAs == null) return false; + return Date == otherAs.Date && Event == otherAs.Event; + } +} diff --git a/packages/epubx/lib/src/schema/opf/epub_metadata_identifier.dart b/packages/epubx/lib/src/schema/opf/epub_metadata_identifier.dart new file mode 100644 index 0000000..52a91e7 --- /dev/null +++ b/packages/epubx/lib/src/schema/opf/epub_metadata_identifier.dart @@ -0,0 +1,19 @@ +import 'package:quiver/core.dart'; + +class EpubMetadataIdentifier { + String? Id; + String? Scheme; + String? Identifier; + + @override + int get hashCode => hash3(Id.hashCode, Scheme.hashCode, Identifier.hashCode); + + @override + bool operator ==(other) { + var otherAs = other as EpubMetadataIdentifier?; + if (otherAs == null) return false; + return Id == otherAs.Id && + Scheme == otherAs.Scheme && + Identifier == otherAs.Identifier; + } +} diff --git a/packages/epubx/lib/src/schema/opf/epub_metadata_meta.dart b/packages/epubx/lib/src/schema/opf/epub_metadata_meta.dart new file mode 100644 index 0000000..7b32b83 --- /dev/null +++ b/packages/epubx/lib/src/schema/opf/epub_metadata_meta.dart @@ -0,0 +1,33 @@ +import 'package:quiver/core.dart'; + +class EpubMetadataMeta { + String? Name; + String? Content; + String? Id; + String? Refines; + String? Property; + String? Scheme; + Map? Attributes; + + @override + int get hashCode => hashObjects([ + Name.hashCode, + Content.hashCode, + Id.hashCode, + Refines.hashCode, + Property.hashCode, + Scheme.hashCode + ]); + + @override + bool operator ==(other) { + var otherAs = other as EpubMetadataMeta?; + if (otherAs == null) return false; + return Name == otherAs.Name && + Content == otherAs.Content && + Id == otherAs.Id && + Refines == otherAs.Refines && + Property == otherAs.Property && + Scheme == otherAs.Scheme; + } +} diff --git a/packages/epubx/lib/src/schema/opf/epub_package.dart b/packages/epubx/lib/src/schema/opf/epub_package.dart new file mode 100644 index 0000000..992839b --- /dev/null +++ b/packages/epubx/lib/src/schema/opf/epub_package.dart @@ -0,0 +1,38 @@ +import 'package:quiver/core.dart'; + +import 'epub_guide.dart'; +import 'epub_manifest.dart'; +import 'epub_metadata.dart'; +import 'epub_spine.dart'; +import 'epub_version.dart'; + +class EpubPackage { + EpubVersion? Version; + EpubMetadata? Metadata; + EpubManifest? Manifest; + EpubSpine? Spine; + EpubGuide? Guide; + + @override + int get hashCode => hashObjects([ + Version.hashCode, + Metadata.hashCode, + Manifest.hashCode, + Spine.hashCode, + Guide.hashCode + ]); + + @override + bool operator ==(other) { + var otherAs = other as EpubPackage?; + if (otherAs == null) { + return false; + } + + return Version == otherAs.Version && + Metadata == otherAs.Metadata && + Manifest == otherAs.Manifest && + Spine == otherAs.Spine && + Guide == otherAs.Guide; + } +} diff --git a/packages/epubx/lib/src/schema/opf/epub_spine.dart b/packages/epubx/lib/src/schema/opf/epub_spine.dart new file mode 100644 index 0000000..777a4ce --- /dev/null +++ b/packages/epubx/lib/src/schema/opf/epub_spine.dart @@ -0,0 +1,32 @@ +import 'package:quiver/collection.dart' as collections; +import 'package:quiver/core.dart'; + +import 'epub_spine_item_ref.dart'; + +class EpubSpine { + String? TableOfContents; + List? Items; + bool? ltr; + + @override + int get hashCode { + var objs = [ + TableOfContents.hashCode, + ltr.hashCode, + ...Items!.map((item) => item.hashCode) + ]; + return hashObjects(objs); + } + + @override + bool operator ==(other) { + var otherAs = other as EpubSpine?; + if (otherAs == null) return false; + + if (!collections.listsEqual(Items, otherAs.Items)) { + return false; + } + return ((TableOfContents == otherAs.TableOfContents) && + (ltr == otherAs.ltr)); + } +} diff --git a/packages/epubx/lib/src/schema/opf/epub_spine_item_ref.dart b/packages/epubx/lib/src/schema/opf/epub_spine_item_ref.dart new file mode 100644 index 0000000..2e7a9ce --- /dev/null +++ b/packages/epubx/lib/src/schema/opf/epub_spine_item_ref.dart @@ -0,0 +1,24 @@ +import 'package:quiver/core.dart'; + +class EpubSpineItemRef { + String? IdRef; + bool? IsLinear; + + @override + int get hashCode => hash2(IdRef.hashCode, IsLinear.hashCode); + + @override + bool operator ==(other) { + var otherAs = other as EpubSpineItemRef?; + if (otherAs == null) { + return false; + } + + return IdRef == otherAs.IdRef && IsLinear == otherAs.IsLinear; + } + + @override + String toString() { + return 'IdRef: $IdRef'; + } +} diff --git a/packages/epubx/lib/src/schema/opf/epub_version.dart b/packages/epubx/lib/src/schema/opf/epub_version.dart new file mode 100644 index 0000000..bb0eaee --- /dev/null +++ b/packages/epubx/lib/src/schema/opf/epub_version.dart @@ -0,0 +1 @@ +enum EpubVersion { Epub2, Epub3 } diff --git a/packages/epubx/lib/src/utils/enum_from_string.dart b/packages/epubx/lib/src/utils/enum_from_string.dart new file mode 100644 index 0000000..02e1f51 --- /dev/null +++ b/packages/epubx/lib/src/utils/enum_from_string.dart @@ -0,0 +1,16 @@ +class EnumFromString { + List enumValues; + + EnumFromString(this.enumValues); + + T? get(String value) { + value = '$T.$value'; + try { + var x = enumValues + .firstWhere((f) => f.toString().toUpperCase() == value.toUpperCase()); + return x; + } catch (e) { + return null; + } + } +} diff --git a/packages/epubx/lib/src/utils/zip_path_utils.dart b/packages/epubx/lib/src/utils/zip_path_utils.dart new file mode 100644 index 0000000..a3910d0 --- /dev/null +++ b/packages/epubx/lib/src/utils/zip_path_utils.dart @@ -0,0 +1,18 @@ +class ZipPathUtils { + static String getDirectoryPath(String filePath) { + var lastSlashIndex = filePath.lastIndexOf('/'); + if (lastSlashIndex == -1) { + return ''; + } else { + return filePath.substring(0, lastSlashIndex); + } + } + + static String? combine(String? directory, String? fileName) { + if (directory == null || directory == '') { + return fileName; + } else { + return directory + '/' + fileName!; + } + } +} diff --git a/packages/epubx/lib/src/writers/epub_guide_writer.dart b/packages/epubx/lib/src/writers/epub_guide_writer.dart new file mode 100644 index 0000000..8dec0fb --- /dev/null +++ b/packages/epubx/lib/src/writers/epub_guide_writer.dart @@ -0,0 +1,15 @@ +import 'package:epubx/src/schema/opf/epub_guide.dart'; +import 'package:xml/src/xml/builder.dart' show XmlBuilder; + +class EpubGuideWriter { + static void writeGuide(XmlBuilder builder, EpubGuide? guide) { + builder.element('guide', nest: () { + guide!.Items!.forEach((guideItem) => builder.element('reference', + attributes: { + 'type': guideItem.Type!, + 'title': guideItem.Title!, + 'href': guideItem.Href! + })); + }); + } +} diff --git a/packages/epubx/lib/src/writers/epub_manifest_writer.dart b/packages/epubx/lib/src/writers/epub_manifest_writer.dart new file mode 100644 index 0000000..deee2de --- /dev/null +++ b/packages/epubx/lib/src/writers/epub_manifest_writer.dart @@ -0,0 +1,17 @@ +import 'package:epubx/src/schema/opf/epub_manifest.dart'; +import 'package:xml/src/xml/builder.dart' show XmlBuilder; + +class EpubManifestWriter { + static void writeManifest(XmlBuilder builder, EpubManifest? manifest) { + builder.element('manifest', nest: () { + manifest!.Items!.forEach((item) { + builder.element('item', nest: () { + builder + ..attribute('id', item.Id!) + ..attribute('href', item.Href!) + ..attribute('media-type', item.MediaType!); + }); + }); + }); + } +} diff --git a/packages/epubx/lib/src/writers/epub_metadata_writer.dart b/packages/epubx/lib/src/writers/epub_metadata_writer.dart new file mode 100644 index 0000000..299dc4a --- /dev/null +++ b/packages/epubx/lib/src/writers/epub_metadata_writer.dart @@ -0,0 +1,105 @@ +import 'package:epubx/src/schema/opf/epub_metadata.dart'; +import 'package:epubx/src/schema/opf/epub_version.dart'; +import 'package:xml/src/xml/builder.dart' show XmlBuilder; + +class EpubMetadataWriter { + static const _dc_namespace = 'http://purl.org/dc/elements/1.1/'; + static const _opf_namespace = 'http://www.idpf.org/2007/opf'; + + static void writeMetadata( + XmlBuilder builder, EpubMetadata? meta, EpubVersion? version) { + builder.element('metadata', + namespaces: {_opf_namespace: 'opf', _dc_namespace: 'dc'}, nest: () { + meta! + ..Titles?.forEach((item) => + builder.element('title', nest: item, namespace: _dc_namespace)) + ..Creators?.forEach((item) => + builder.element('creator', namespace: _dc_namespace, nest: () { + if (item.Role != null) { + builder.attribute('role', item.Role!, + namespace: _opf_namespace); + } + if (item.FileAs != null) { + builder.attribute('file-as', item.FileAs!, + namespace: _opf_namespace); + } + builder.text(item.Creator!); + })) + ..Subjects?.forEach((item) => + builder.element('subject', namespace: _dc_namespace, nest: item)) + ..Publishers?.forEach((item) => + builder.element('publisher', namespace: _dc_namespace, nest: item)) + ..Contributors?.forEach((item) => + builder.element('contributor', namespace: _dc_namespace, nest: () { + if (item.Role != null) { + builder.attribute('role', item.Role!, + namespace: _opf_namespace); + } + if (item.FileAs != null) { + builder.attribute('file-as', item.FileAs!, + namespace: _opf_namespace); + } + builder.text(item.Contributor!); + })) + ..Dates?.forEach((date) => + builder.element('date', namespace: _dc_namespace, nest: () { + if (date.Event != null) { + builder.attribute('event', date.Event!, + namespace: _opf_namespace); + } + builder.text(date.Date!); + })) + ..Types?.forEach((type) => + builder.element('type', namespace: _dc_namespace, nest: type)) + ..Formats?.forEach((format) => + builder.element('format', namespace: _dc_namespace, nest: format)) + ..Identifiers?.forEach((id) => + builder.element('identifier', namespace: _dc_namespace, nest: () { + if (id.Id != null) builder.attribute('id', id.Id!); + if (id.Scheme != null) { + builder.attribute('scheme', id.Scheme!, + namespace: _opf_namespace); + } + builder.text(id.Identifier!); + })) + ..Sources?.forEach((item) => + builder.element('source', namespace: _dc_namespace, nest: item)) + ..Languages?.forEach((item) => + builder.element('language', namespace: _dc_namespace, nest: item)) + ..Relations?.forEach((item) => + builder.element('relation', namespace: _dc_namespace, nest: item)) + ..Coverages?.forEach((item) => + builder.element('coverage', namespace: _dc_namespace, nest: item)) + ..Rights?.forEach((item) => + builder.element('rights', namespace: _dc_namespace, nest: item)) + ..MetaItems?.forEach((metaitem) => builder.element('meta', nest: () { + if (version == EpubVersion.Epub2) { + if (metaitem.Name != null) { + builder.attribute('name', metaitem.Name!); + } + if (metaitem.Content != null) { + builder.attribute('content', metaitem.Content!); + } + } else if (version == EpubVersion.Epub3) { + if (metaitem.Id != null) { + builder.attribute('id', metaitem.Id!); + } + if (metaitem.Refines != null) { + builder.attribute('refines', metaitem.Refines!); + } + if (metaitem.Property != null) { + builder.attribute('property', metaitem.Property!); + } + if (metaitem.Scheme != null) { + builder.attribute('scheme', metaitem.Scheme!); + } + } + })); + + if (meta.Description != null) { + builder.element('description', + namespace: _dc_namespace, nest: meta.Description); + } + }); + } +} diff --git a/packages/epubx/lib/src/writers/epub_navigation_writer.dart b/packages/epubx/lib/src/writers/epub_navigation_writer.dart new file mode 100644 index 0000000..d71e1e4 --- /dev/null +++ b/packages/epubx/lib/src/writers/epub_navigation_writer.dart @@ -0,0 +1,67 @@ +import 'package:epubx/src/schema/navigation/epub_navigation.dart'; +import 'package:epubx/src/schema/navigation/epub_navigation_doc_title.dart'; +import 'package:epubx/src/schema/navigation/epub_navigation_head.dart'; +import 'package:epubx/src/schema/navigation/epub_navigation_map.dart'; +import 'package:epubx/src/schema/navigation/epub_navigation_point.dart'; +import 'package:xml/src/xml/builder.dart' show XmlBuilder; + +class EpubNavigationWriter { + static const String _namespace = 'http://www.daisy.org/z3986/2005/ncx/'; + + static String writeNavigation(EpubNavigation navigation) { + var builder = XmlBuilder(); + builder.processing('xml', 'version="1.0"'); + + builder.element('ncx', attributes: { + 'version': '2005-1', + 'lang': 'en', + }, nest: () { + builder.namespace(_namespace); + + writeNavigationHead(builder, navigation.Head!); + writeNavigationDocTitle(builder, navigation.DocTitle!); + writeNavigationMap(builder, navigation.NavMap!); + }); + + return builder.buildDocument().toXmlString(pretty: false); + } + + static void writeNavigationDocTitle( + XmlBuilder builder, EpubNavigationDocTitle title) { + builder.element('docTitle', nest: () { + title.Titles!.forEach((element) { + builder.text(element); + }); + }); + } + + static void writeNavigationHead(XmlBuilder builder, EpubNavigationHead head) { + builder.element('head', nest: () { + head.Metadata!.forEach((item) => builder.element('meta', + attributes: {'content': item.Content!, 'name': item.Name!})); + }); + } + + static void writeNavigationMap(XmlBuilder builder, EpubNavigationMap map) { + builder.element('navMap', nest: () { + map.Points!.forEach((item) => writeNavigationPoint(builder, item)); + }); + } + + static void writeNavigationPoint( + XmlBuilder builder, EpubNavigationPoint point) { + builder.element('navPoint', attributes: { + 'id': point.Id!, + 'playOrder': point.PlayOrder!, + }, nest: () { + point.NavigationLabels!.forEach((element) { + builder.element('navLabel', nest: () { + builder.element('text', nest: () { + builder.text(element.Text!); + }); + }); + }); + builder.element('content', attributes: {'src': point.Content!.Source!}); + }); + } +} diff --git a/packages/epubx/lib/src/writers/epub_package_writer.dart b/packages/epubx/lib/src/writers/epub_package_writer.dart new file mode 100644 index 0000000..97fb427 --- /dev/null +++ b/packages/epubx/lib/src/writers/epub_package_writer.dart @@ -0,0 +1,31 @@ +import 'package:epubx/src/schema/opf/epub_package.dart'; +import 'package:epubx/src/schema/opf/epub_version.dart'; +import 'package:epubx/src/writers/epub_guide_writer.dart'; +import 'package:epubx/src/writers/epub_manifest_writer.dart'; +import 'package:epubx/src/writers/epub_spine_writer.dart'; +import 'package:xml/src/xml/builder.dart' show XmlBuilder; +import 'epub_metadata_writer.dart'; + +class EpubPackageWriter { + static const String _namespace = 'http://www.idpf.org/2007/opf'; + + static String writeContent(EpubPackage package) { + var builder = XmlBuilder(); + builder.processing('xml', 'version="1.0"'); + + builder.element('package', attributes: { + 'version': package.Version == EpubVersion.Epub2 ? '2.0' : '3.0', + 'unique-identifier': 'etextno', + }, nest: () { + builder.namespace(_namespace); + + EpubMetadataWriter.writeMetadata( + builder, package.Metadata, package.Version); + EpubManifestWriter.writeManifest(builder, package.Manifest); + EpubSpineWriter.writeSpine(builder, package.Spine!); + EpubGuideWriter.writeGuide(builder, package.Guide); + }); + + return builder.buildDocument().toXmlString(pretty: false); + } +} diff --git a/packages/epubx/lib/src/writers/epub_spine_writer.dart b/packages/epubx/lib/src/writers/epub_spine_writer.dart new file mode 100644 index 0000000..3e0ee31 --- /dev/null +++ b/packages/epubx/lib/src/writers/epub_spine_writer.dart @@ -0,0 +1,15 @@ +import 'package:epubx/src/schema/opf/epub_spine.dart'; +import 'package:xml/src/xml/builder.dart' show XmlBuilder; + +class EpubSpineWriter { + static void writeSpine(XmlBuilder builder, EpubSpine spine) { + builder.element('spine', attributes: {'toc': spine.TableOfContents!}, + nest: () { + spine.Items!.forEach((spineitem) => builder.element('itemref', + attributes: { + 'idref': spineitem.IdRef!, + 'linear': spineitem.IsLinear! ? 'yes' : 'no' + })); + }); + } +} diff --git a/packages/epubx/pubspec.lock b/packages/epubx/pubspec.lock new file mode 100644 index 0000000..dc567b5 --- /dev/null +++ b/packages/epubx/pubspec.lock @@ -0,0 +1,429 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "1b0e6a07425a3e460666e88bf1c949ccc7bb0116ad562ce94a1eca60fe820725" + url: "https://pub.dev" + source: hosted + version: "103.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "61c04d0c1bfed555c681ea079519933f071a5a026578ff73c4ff0df2d3462e5e" + url: "https://pub.dev" + source: hosted + version: "13.3.0" + archive: + dependency: "direct main" + description: + name: archive + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d + url: "https://pub.dev" + source: hosted + version: "3.6.1" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" + collection: + dependency: "direct main" + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "956a3de0725ca232ad353565a8290d3357592bf4250f6f298a185e2d949c5d3d" + url: "https://pub.dev" + source: hosted + version: "1.15.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + image: + dependency: "direct main" + description: + name: image + sha256: "8e9d133755c3e84c73288363e6343157c383a0c6c56fc51afcc5d4d7180306d6" + url: "https://pub.dev" + source: hosted + version: "3.3.0" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd" + url: "https://pub.dev" + source: hosted + version: "0.12.20" + meta: + dependency: transitive + description: + name: meta + sha256: c82594181e3312f3d0695fc95aaaf7758d75b8d4ae2bbecf223b9fd5109a059d + url: "https://pub.dev" + source: hosted + version: "1.18.3" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path: + dependency: "direct main" + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + pedantic: + dependency: "direct dev" + description: + name: pedantic + sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + quiver: + dependency: "direct main" + description: + name: quiver + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.dev" + source: hosted + version: "0.10.13" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test: + dependency: "direct dev" + description: + name: test + sha256: ca578dc12bb8b2f40b67b7d3bd2fac4f31c01a6ff7130a14e2597b919934507f + url: "https://pub.dev" + source: hosted + version: "1.31.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11" + url: "https://pub.dev" + source: hosted + version: "0.7.12" + test_core: + dependency: transitive + description: + name: test_core + sha256: d2e98ec12998368dc59ddd47ab709f2cd55acd6b66dc7db764455a44082f4bc5 + url: "https://pub.dev" + source: hosted + version: "0.6.18" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.dev" + source: hosted + version: "15.2.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + xml: + dependency: "direct main" + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.11.0 <4.0.0" diff --git a/packages/epubx/pubspec.yaml b/packages/epubx/pubspec.yaml new file mode 100644 index 0000000..24cdfe5 --- /dev/null +++ b/packages/epubx/pubspec.yaml @@ -0,0 +1,21 @@ +name: epubx +description: Epub Parser for Dart. Epub package fork. Suitable for use on the Server, the Web, or in Flutter +homepage: https://github.com/rbcprolabs/epubx.dart +issue_tracker: https://github.com/rbcprolabs/epubx.dart +version: 4.0.0 + +environment: + sdk: '>=2.12.0 <4.0.0' + +dependencies: + archive: ^3.1.6 + quiver: ^3.0.1+1 + path: ^1.8.1 + xml: ^6.0.1 + image: ^3.0.8 + collection: ^1.15.0 + +dev_dependencies: + test: ^1.16.7 + path: ^1.8.0 + pedantic: ^1.11.0 diff --git a/packages/epubx/tool/publish.sh b/packages/epubx/tool/publish.sh new file mode 100644 index 0000000..fa3709c --- /dev/null +++ b/packages/epubx/tool/publish.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +mkdir -p .pub-cache + +cat < ~/.pub-cache/credentials.json +{ + "accessToken":"$accessToken", + "refreshToken":"$refreshToken", + "tokenEndpoint":"$tokenEndpoint", + "scopes":["$scopes"], + "expiration":$expiration +} +EOF + +pub publish -f \ No newline at end of file diff --git a/packages/epubx/tool/travis.sh b/packages/epubx/tool/travis.sh new file mode 100755 index 0000000..0995671 --- /dev/null +++ b/packages/epubx/tool/travis.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# Fast fail the script on failures. +set -e + +# Analyze the code. +dartanalyzer --strong --fatal-warnings . + +# Test the entire test directory +pub run test test/ \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index c432162..357e31c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -169,6 +169,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.9" + dbus: + dependency: transitive + description: + name: dbus + sha256: "0ce9b0a839e6dee59a37a623d2fc26a35bbbe6404213e419b0d6411023d62645" + url: "https://pub.dev" + source: hosted + version: "0.7.14" dio: dependency: "direct main" description: @@ -210,12 +218,11 @@ packages: source: hosted version: "4.4.0" epubx: - dependency: transitive + dependency: "direct overridden" description: - name: epubx - sha256: "0ab9354efa177c4be52c46f857bc15bf83f83a92667fb673465c8f89fca26db3" - url: "https://pub.dev" - source: hosted + path: "packages/epubx" + relative: true + source: path version: "4.0.0" equatable: dependency: "direct main" @@ -249,14 +256,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" - ffi_leak_tracker: - dependency: transitive - description: - name: ffi_leak_tracker - sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97" - url: "https://pub.dev" - source: hosted - version: "0.1.2" file: dependency: transitive description: @@ -269,10 +268,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: f9245fc33aeba9e0b938d7f3785f10b7a7230e05b8fc40f5a6a8342d7899e391 + sha256: f13a03000d942e476bc1ff0a736d2e9de711d2f89a95cd4c1d88f861c3348387 url: "https://pub.dev" source: hosted - version: "3.0.4" + version: "11.0.2" file_selector_linux: dependency: transitive description: @@ -849,18 +848,18 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "4bf625947f6c7713ee242296a682e23e44823c09cf9d79e4f1238923c92db852" + sha256: "468c26b4254ab01979fa5e4a98cb343ea3631b9acee6f21028997419a80e1a20" url: "https://pub.dev" source: hosted - version: "10.1.0" + version: "9.0.1" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: db762cb2f4f25ee60fb6359773861b0f199e00b90d237bd85a76a1e806b46ef4 + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "3.2.1" path: dependency: "direct main" description: @@ -1342,10 +1341,10 @@ packages: dependency: transitive description: name: win32 - sha256: ba6f4bba816c8d7e3c1580e170f3786d216951cc6b94babc3b814c08d2cb2738 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "5.15.0" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 4a9239f..5c5865a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -45,7 +45,7 @@ dependencies: xml2json: cached_network_image: intl: - skeletonizer: ^2.1.0+1 + skeletonizer: flutter_launcher_icons: package_info_plus: url_launcher: @@ -67,7 +67,7 @@ dependencies: dio: cosmos_epub: archive: - emoji_picker_flutter: ^4.4.0 + emoji_picker_flutter: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. @@ -84,6 +84,15 @@ dev_dependencies: # rules and activating additional ones. flutter_lints: ^6.0.0 +# Use a locally vendored, patched epubx. Upstream epubx 4.0.0 fails the entire +# book parse when the TOC/navigation document can't be resolved (common with +# EPUB3 nav.xhtml / missing files), which crashed the internal reader. The local +# copy makes navigation/chapter parsing non-fatal so the reader falls back to +# spine order. See packages/epubx/lib/src/readers/schema_reader.dart. +dependency_overrides: + epubx: + path: packages/epubx + # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec