fix: internal e-reader epubx issues

fix: gradle issues due to flutter migration
fix: minor colortheme adjustments
This commit is contained in:
Daniel
2026-06-14 10:40:07 +02:00
parent 1f7d5e4324
commit 40445fd83f
87 changed files with 4276 additions and 874 deletions
+4
View File
@@ -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
+2
View File
@@ -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(
+148
View File
@@ -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
+46
View File
@@ -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/
+21
View File
@@ -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.
+134
View File
@@ -0,0 +1,134 @@
# epubx
It package is [dart-epub](https://github.com/orthros/dart-epub) fork
[Flutter UI implementation](https://pub.dev/packages/epub_view)
Epub Reader and Writer for Dart inspired by [this fantastic C# Epub Reader](https://github.com/versfx/EpubReader)
This does not rely on the ```dart:io``` package in any way, so it is avilable for both desktop and web-based implementations
[![pub package](https://img.shields.io/pub/v/epubx.svg)](https://pub.dartlang.org/packages/epubx)
## Installing
Add the package to the ```dependencies``` section of your pubspec.yaml
```
dependencies:
epubx: any
```
## Example
```dart
//Get the epub into memory somehow
String fileName = 'sample.epub';
String fullPath = path.join(io.Directory.current.path, fileName);
var targetFile = new io.File(fullPath);
List<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);
```
+15
View File
@@ -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/**
+36
View File
@@ -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;
}
}
+208
View File
@@ -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;
}
}
+59
View File
@@ -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'
}));
});
}
}
+429
View File
@@ -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"
+21
View File
@@ -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
+14
View File
@@ -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
+10
View File
@@ -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
View File
@@ -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
View File
@@ -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