fix: internal e-reader epubx issues
fix: gradle issues due to flutter migration fix: minor colortheme adjustments
This commit is contained in:
@@ -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`
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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
|
||||
|
||||
@@ -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<BookDetailsEvent, BookDetailsState> {
|
||||
),
|
||||
);
|
||||
|
||||
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(
|
||||
|
||||
@@ -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<Object> get props => [selectedDirectory, schema, book, format];
|
||||
List<Object> get props => [book, format];
|
||||
}
|
||||
|
||||
class OpenBookInBrowser extends BookDetailsEvent {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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<String> downloadBookForReader(
|
||||
BookDetailsModel book,
|
||||
DocumentFile selectedDirectory,
|
||||
DownloadSchema schema, {
|
||||
Future<Uint8List> 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<int> 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<Uint8List?> 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -180,21 +180,18 @@ class BookDetailsRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> openInInternalReader(
|
||||
DocumentFile selectedDirectory,
|
||||
DownloadSchema schema,
|
||||
Future<Uint8List> 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<Uint8List?> readLocalEpubBytes(String path) =>
|
||||
datasource.readLocalEpubBytes(path);
|
||||
|
||||
Future<String?> getSeriesPath(String seriesName) async {
|
||||
try {
|
||||
|
||||
@@ -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<BookDetailsPage> {
|
||||
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<BookDetailsPage> {
|
||||
|
||||
Future<void> _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<BookDetailsPage> {
|
||||
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<void> _restoreReaderProgressFromCloud(String bookUuid) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (!(prefs.getBool('webdav_enabled') ?? false)) return;
|
||||
@@ -376,12 +404,12 @@ class _BookDetailsPageState extends State<BookDetailsPage> {
|
||||
|
||||
if (state.openInInternalReaderState ==
|
||||
OpenInInternalReaderState.success &&
|
||||
state.downloadFilePath != null) {
|
||||
state.readerBytes != null) {
|
||||
context.read<BookDetailsBloc>().add(const ClearSnackBarStates());
|
||||
|
||||
_openInternalReader(
|
||||
context,
|
||||
state.downloadFilePath!,
|
||||
state.readerBytes!,
|
||||
state.bookDetails!,
|
||||
);
|
||||
}
|
||||
@@ -1051,40 +1079,9 @@ class _BookDetailsPageState extends State<BookDetailsPage> {
|
||||
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<BookDetailsBloc>().add(
|
||||
OpenBookInInternalReader(
|
||||
selectedDirectory: selectedDirectory,
|
||||
schema: settingsState.downloadSchema,
|
||||
book: book,
|
||||
format: selectedFormat,
|
||||
),
|
||||
|
||||
@@ -440,7 +440,7 @@ class _BookViewPageState extends State<BookViewPage> {
|
||||
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,
|
||||
|
||||
@@ -391,7 +391,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
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<SettingsPage> {
|
||||
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<SettingsPage> {
|
||||
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<SettingsPage> {
|
||||
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<SettingsPage> {
|
||||
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<SettingsPage> {
|
||||
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<SettingsPage> {
|
||||
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<SettingsPage> {
|
||||
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<SettingsPage> {
|
||||
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<SettingsPage> {
|
||||
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<SettingsPage> {
|
||||
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<SettingsPage> {
|
||||
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<SettingsPage> {
|
||||
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<SettingsPage> {
|
||||
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<SettingsPage> {
|
||||
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<SettingsPage> {
|
||||
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<SettingsPage> {
|
||||
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<SettingsPage> {
|
||||
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<SettingsPage> {
|
||||
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<SettingsPage> {
|
||||
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<SettingsPage> {
|
||||
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<SettingsPage> {
|
||||
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<SettingsPage> {
|
||||
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<SettingsPage> {
|
||||
(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<SettingsPage> {
|
||||
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<SettingsPage> {
|
||||
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<SettingsPage> {
|
||||
children: [
|
||||
Icon(
|
||||
Icons.cloud_sync_rounded,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -354,12 +354,10 @@ class _SyncFilterBottomSheetState extends State<SyncFilterBottomSheet> {
|
||||
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,
|
||||
|
||||
@@ -135,7 +135,7 @@ class _SyncSettingsWidgetState extends State<SyncSettingsWidget> {
|
||||
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<SyncSettingsWidget> {
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
@@ -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/
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
[](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<int> 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<String> 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<EpubChapter> 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<String, EpubByteContentFile> 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<String, EpubTextContentFile> htmlFiles = bookContent.Html;
|
||||
|
||||
// All CSS files in the book (file name is the key)
|
||||
Map<String, EpubTextContentFile> 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<String, EpubByteContentFile> fonts = bookContent.Fonts;
|
||||
|
||||
// All files in the book (including HTML, CSS, images, fonts, and other types of files)
|
||||
Map<String, EpubContentFile> 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);
|
||||
```
|
||||
@@ -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/**
|
||||
@@ -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;
|
||||
@@ -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<String?>? AuthorList;
|
||||
EpubSchema? Schema;
|
||||
EpubContent? Content;
|
||||
Image? CoverImage;
|
||||
List<EpubChapter>? 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);
|
||||
}
|
||||
}
|
||||
@@ -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<int>? 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;
|
||||
}
|
||||
}
|
||||
@@ -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<EpubChapter>? 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}';
|
||||
}
|
||||
}
|
||||
@@ -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<String, EpubTextContentFile>? Html;
|
||||
Map<String, EpubTextContentFile>? Css;
|
||||
Map<String, EpubByteContentFile>? Images;
|
||||
Map<String, EpubByteContentFile>? Fonts;
|
||||
Map<String, EpubContentFile>? AllFiles;
|
||||
|
||||
EpubContent() {
|
||||
Html = <String, EpubTextContentFile>{};
|
||||
Css = <String, EpubTextContentFile>{};
|
||||
Images = <String, EpubByteContentFile>{};
|
||||
Fonts = <String, EpubByteContentFile>{};
|
||||
AllFiles = <String, EpubContentFile>{};
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<EpubBookRef> openBook(FutureOr<List<int>> bytes) async {
|
||||
List<int> 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<EpubBook> readBook(FutureOr<List<int>> bytes) async {
|
||||
var result = EpubBook();
|
||||
List<int> 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 = <EpubChapterRef>[];
|
||||
try {
|
||||
chapterRefs = await epubBookRef.getChapters();
|
||||
} catch (_) {
|
||||
chapterRefs = <EpubChapterRef>[];
|
||||
}
|
||||
result.Chapters = await readChapters(chapterRefs);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static Future<EpubContent> 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 = <String, EpubContentFile>{};
|
||||
|
||||
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<Map<String, EpubTextContentFile>> readTextContentFiles(
|
||||
Map<String, EpubTextContentFileRef> textContentFileRefs) async {
|
||||
var result = <String, EpubTextContentFile>{};
|
||||
|
||||
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<Map<String, EpubByteContentFile>> readByteContentFiles(
|
||||
Map<String, EpubByteContentFileRef> byteContentFileRefs) async {
|
||||
var result = <String, EpubByteContentFile>{};
|
||||
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<EpubByteContentFile> 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<List<EpubChapter>> readChapters(
|
||||
List<EpubChapterRef> chapterRefs) async {
|
||||
var result = <EpubChapter>[];
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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 =
|
||||
'<?xml version="1.0"?><container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container"><rootfiles><rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/></rootfiles></container>';
|
||||
|
||||
// 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<int>? 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<int>? writeBook(EpubBook book) {
|
||||
var arch = _createArchive(book);
|
||||
|
||||
return ZipEncoder().encode(arch);
|
||||
}
|
||||
}
|
||||
@@ -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<images.Image?> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<EpubChapterRef> getChapters(EpubBookRef bookRef) {
|
||||
if (bookRef.Schema!.Navigation == null) {
|
||||
return <EpubChapterRef>[];
|
||||
}
|
||||
return getChaptersImpl(
|
||||
bookRef, bookRef.Schema!.Navigation!.NavMap!.Points!);
|
||||
}
|
||||
|
||||
static List<EpubChapterRef> getChaptersImpl(
|
||||
EpubBookRef bookRef, List<EpubNavigationPoint> navigationPoints) {
|
||||
var result = <EpubChapterRef>[];
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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 = <String, EpubTextContentFileRef>{};
|
||||
result.Css = <String, EpubTextContentFileRef>{};
|
||||
result.Images = <String, EpubByteContentFileRef>{};
|
||||
result.Fonts = <String, EpubByteContentFileRef>{};
|
||||
result.AllFiles = <String, EpubContentFileRef>{};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<EpubNavigation> 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<EpubManifestItem?>().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<ArchiveFile?>().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<xml.XmlElement?>()
|
||||
.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<xml.XmlElement?>()
|
||||
.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<xml.XmlElement?>()
|
||||
.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 = <EpubNavigationDocAuthor>[];
|
||||
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<xml.XmlElement?>()
|
||||
.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<xml.XmlElement?>()
|
||||
.firstWhere((xml.XmlElement? elem) => elem != null, orElse: () => null);
|
||||
if (pageListNode != null) {
|
||||
var pageList = readNavigationPageList(pageListNode);
|
||||
result.PageList = pageList;
|
||||
}
|
||||
|
||||
result.NavLists = <EpubNavigationList>[];
|
||||
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<EpubManifestItem?>()
|
||||
.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<ArchiveFile?>().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<xml.XmlElement?>()
|
||||
.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 = <EpubNavigationDocAuthor>[];
|
||||
|
||||
var navNode = containerDocument
|
||||
.findAllElements('nav')
|
||||
.cast<xml.XmlElement?>()
|
||||
.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 = <String>[];
|
||||
docAuthorNode.children.whereType<xml.XmlElement>().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 = <String>[];
|
||||
docTitleNode.children.whereType<xml.XmlElement>().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 = <EpubNavigationHeadMeta>[];
|
||||
|
||||
headNode.children.whereType<xml.XmlElement>().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<xml.XmlElement>().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 = <EpubNavigationPoint>[];
|
||||
navigationMapNode.children.whereType<xml.XmlElement>().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 = <EpubNavigationPoint>[];
|
||||
navigationMapNode.children.whereType<xml.XmlElement>().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 = <EpubNavigationPageTarget>[];
|
||||
navigationPageListNode.children.whereType<xml.XmlElement>().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 = <EpubNavigationLabel>[];
|
||||
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>(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<xml.XmlElement>()
|
||||
.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 = <EpubNavigationLabel>[];
|
||||
result.ChildNavigationPoints = <EpubNavigationPoint>[];
|
||||
navigationPointNode.children.whereType<xml.XmlElement>().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 = <EpubNavigationLabel>[];
|
||||
result.ChildNavigationPoints = <EpubNavigationPoint>[];
|
||||
navigationPointNode.children.whereType<xml.XmlElement>().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<xml.XmlElement>().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;
|
||||
}
|
||||
}
|
||||
@@ -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 = <EpubGuideReference>[];
|
||||
guideNode.children
|
||||
.whereType<XmlElement>()
|
||||
.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 = <EpubManifestItem>[];
|
||||
manifestNode.children
|
||||
.whereType<XmlElement>()
|
||||
.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 = <String>[];
|
||||
result.Creators = <EpubMetadataCreator>[];
|
||||
result.Subjects = <String>[];
|
||||
result.Publishers = <String>[];
|
||||
result.Contributors = <EpubMetadataContributor>[];
|
||||
result.Dates = <EpubMetadataDate>[];
|
||||
result.Types = <String>[];
|
||||
result.Formats = <String>[];
|
||||
result.Identifiers = <EpubMetadataIdentifier>[];
|
||||
result.Sources = <String>[];
|
||||
result.Languages = <String>[];
|
||||
result.Relations = <String>[];
|
||||
result.Coverages = <String>[];
|
||||
result.Rights = <String>[];
|
||||
result.MetaItems = <EpubMetadataMeta>[];
|
||||
metadataNode.children
|
||||
.whereType<XmlElement>()
|
||||
.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<EpubPackage> 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<XmlElement?>()
|
||||
.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<XmlElement?>()
|
||||
.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<XmlElement?>()
|
||||
.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 = <EpubSpineItemRef>[];
|
||||
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<XmlElement>()
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@@ -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<String?> 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');
|
||||
}
|
||||
}
|
||||
@@ -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<EpubSchema> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<String?>? 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<List<EpubChapterRef>> getChapters() async {
|
||||
return ChapterReader.getChapters(this);
|
||||
}
|
||||
|
||||
Future<Image?> readCover() async {
|
||||
return await BookCoverReader.readBookCover(this);
|
||||
}
|
||||
}
|
||||
@@ -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<List<int>> readContent() {
|
||||
return readContentAsBytes();
|
||||
}
|
||||
}
|
||||
@@ -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<EpubChapterRef>? 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<String> readHtmlContent() async {
|
||||
return epubTextContentFileRef!.readContentAsText();
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Title: $Title, Subchapter count: ${SubChapters!.length}';
|
||||
}
|
||||
}
|
||||
@@ -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<int> getContentStream() {
|
||||
return openContentStream(getContentFileEntry());
|
||||
}
|
||||
|
||||
List<int> openContentStream(ArchiveFile contentFileEntry) {
|
||||
var contentStream = <int>[];
|
||||
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<List<int>> readContentAsBytes() async {
|
||||
var contentFileEntry = getContentFileEntry();
|
||||
var content = openContentStream(contentFileEntry);
|
||||
return content;
|
||||
}
|
||||
|
||||
Future<String> readContentAsText() async {
|
||||
var contentStream = getContentStream();
|
||||
var result = convert.utf8.decode(contentStream);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -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<String, EpubTextContentFileRef>? Html;
|
||||
Map<String, EpubTextContentFileRef>? Css;
|
||||
Map<String, EpubByteContentFileRef>? Images;
|
||||
Map<String, EpubByteContentFileRef>? Fonts;
|
||||
Map<String, EpubContentFileRef>? AllFiles;
|
||||
|
||||
EpubContentRef() {
|
||||
Html = <String, EpubTextContentFileRef>{};
|
||||
Css = <String, EpubTextContentFileRef>{};
|
||||
Images = <String, EpubByteContentFileRef>{};
|
||||
Fonts = <String, EpubByteContentFileRef>{};
|
||||
AllFiles = <String, EpubContentFileRef>{};
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
@@ -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<String> ReadContentAsync() async {
|
||||
return readContentAsText();
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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<EpubNavigationDocAuthor>? DocAuthors;
|
||||
EpubNavigationMap? NavMap;
|
||||
EpubNavigationPageList? PageList;
|
||||
List<EpubNavigationList>? 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import 'package:quiver/collection.dart' as collections;
|
||||
import 'package:quiver/core.dart';
|
||||
|
||||
class EpubNavigationDocAuthor {
|
||||
List<String>? Authors;
|
||||
|
||||
EpubNavigationDocAuthor() {
|
||||
Authors = <String>[];
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import 'package:quiver/collection.dart' as collections;
|
||||
import 'package:quiver/core.dart';
|
||||
|
||||
class EpubNavigationDocTitle {
|
||||
List<String>? Titles;
|
||||
|
||||
EpubNavigationDocTitle() {
|
||||
Titles = <String>[];
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
@@ -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<EpubNavigationHeadMeta>? Metadata;
|
||||
|
||||
EpubNavigationHead() {
|
||||
Metadata = <EpubNavigationHeadMeta>[];
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
}
|
||||
@@ -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<EpubNavigationLabel>? NavigationLabels;
|
||||
List<EpubNavigationTarget>? 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import 'package:quiver/collection.dart' as collections;
|
||||
import 'package:quiver/core.dart';
|
||||
|
||||
import 'epub_navigation_point.dart';
|
||||
|
||||
class EpubNavigationMap {
|
||||
List<EpubNavigationPoint>? 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);
|
||||
}
|
||||
}
|
||||
@@ -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<EpubNavigationPageTarget>? 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);
|
||||
}
|
||||
}
|
||||
@@ -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<EpubNavigationLabel>? 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
enum EpubNavigationPageTargetType { UNDEFINED, FRONT, NORMAL, SPECIAL }
|
||||
@@ -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<EpubNavigationLabel>? NavigationLabels;
|
||||
EpubNavigationContent? Content;
|
||||
List<EpubNavigationPoint>? 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}';
|
||||
}
|
||||
}
|
||||
@@ -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<EpubNavigationLabel>? 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'package:quiver/collection.dart' as collections;
|
||||
import 'package:quiver/core.dart';
|
||||
|
||||
import 'epub_guide_reference.dart';
|
||||
|
||||
class EpubGuide {
|
||||
List<EpubGuideReference>? Items;
|
||||
|
||||
EpubGuide() {
|
||||
Items = <EpubGuideReference>[];
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'package:quiver/collection.dart' as collections;
|
||||
import 'package:quiver/core.dart';
|
||||
|
||||
import 'epub_manifest_item.dart';
|
||||
|
||||
class EpubManifest {
|
||||
List<EpubManifestItem>? Items;
|
||||
|
||||
EpubManifest() {
|
||||
Items = <EpubManifestItem>[];
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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<String>? Titles;
|
||||
List<EpubMetadataCreator>? Creators;
|
||||
List<String>? Subjects;
|
||||
String? Description;
|
||||
List<String>? Publishers;
|
||||
List<EpubMetadataContributor>? Contributors;
|
||||
List<EpubMetadataDate>? Dates;
|
||||
List<String>? Types;
|
||||
List<String>? Formats;
|
||||
List<EpubMetadataIdentifier>? Identifiers;
|
||||
List<String>? Sources;
|
||||
List<String>? Languages;
|
||||
List<String>? Relations;
|
||||
List<String>? Coverages;
|
||||
List<String>? Rights;
|
||||
List<EpubMetadataMeta>? 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import 'package:quiver/core.dart';
|
||||
|
||||
class EpubMetadataMeta {
|
||||
String? Name;
|
||||
String? Content;
|
||||
String? Id;
|
||||
String? Refines;
|
||||
String? Property;
|
||||
String? Scheme;
|
||||
Map<String, String>? 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<EpubSpineItemRef>? 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));
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
enum EpubVersion { Epub2, Epub3 }
|
||||
@@ -0,0 +1,16 @@
|
||||
class EnumFromString<T> {
|
||||
List<T> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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!
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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!);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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!});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
mkdir -p .pub-cache
|
||||
|
||||
cat <<EOF > ~/.pub-cache/credentials.json
|
||||
{
|
||||
"accessToken":"$accessToken",
|
||||
"refreshToken":"$refreshToken",
|
||||
"tokenEndpoint":"$tokenEndpoint",
|
||||
"scopes":["$scopes"],
|
||||
"expiration":$expiration
|
||||
}
|
||||
EOF
|
||||
|
||||
pub publish -f
|
||||
Executable
+10
@@ -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/
|
||||
+20
-21
@@ -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:
|
||||
|
||||
+11
-2
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user