New epub reader (cosmo ereader) + integtration tests for api + many small fixes
This commit is contained in:
+3
-1
@@ -48,4 +48,6 @@ app.*.map.json
|
||||
de.doen1el.calibreWebCompanion.yml
|
||||
|
||||
# Don't track generated localization files
|
||||
lib/l10n/app_localizations*.dart
|
||||
lib/l10n/app_localizations*.dart
|
||||
lib/l10n/app_localizations*.darttest/test_env.dart
|
||||
test/test_env.dart
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<base-config cleartextTrafficPermitted="true">
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
</network-security-config>
|
||||
+1
-1
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip
|
||||
|
||||
@@ -35,7 +35,6 @@ class ApiService {
|
||||
/// Returns the base URL with base path if available
|
||||
String getBaseUrl() {
|
||||
if (_basePath == null || _basePath!.isEmpty) {
|
||||
_logger.d('Base URL (no path): $_baseUrl');
|
||||
return _baseUrl!;
|
||||
} else {
|
||||
final normalizedBasePath = _basePath!.trim();
|
||||
@@ -228,12 +227,6 @@ class ApiService {
|
||||
queryParams: queryParams,
|
||||
);
|
||||
try {
|
||||
if (response.body.length > 50) {
|
||||
_logger.d('Response body: ${response.body.substring(0, 50)}...');
|
||||
} else {
|
||||
_logger.d('Response body: ${response.body}');
|
||||
}
|
||||
|
||||
return _sanitizeJsonResponse(response.body);
|
||||
} catch (e) {
|
||||
_logger.e('Failed to parse JSON response: $e');
|
||||
@@ -287,12 +280,6 @@ class ApiService {
|
||||
queryParams: queryParams,
|
||||
);
|
||||
try {
|
||||
if (response.body.length > 50) {
|
||||
_logger.d('Response body: ${response.body.substring(0, 50)}...');
|
||||
} else {
|
||||
_logger.d('Response body: ${response.body}');
|
||||
}
|
||||
|
||||
transformer.parse(response.body);
|
||||
|
||||
String jsonString = transformer.toParkerWithAttrs();
|
||||
@@ -333,12 +320,9 @@ class ApiService {
|
||||
headers.addAll(customHeaders);
|
||||
|
||||
if (followRedirects) {
|
||||
_logger.d('GET request to: $uri');
|
||||
_logger.d('Headers: $headers');
|
||||
|
||||
try {
|
||||
final response = await _client!.get(uri, headers: headers);
|
||||
_logger.i('Response status: ${response.statusCode}');
|
||||
_logger.d('GET $uri -> ${response.statusCode}');
|
||||
|
||||
if (response.headers.containsKey('set-cookie')) {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
@@ -625,12 +609,6 @@ class ApiService {
|
||||
}
|
||||
request.files.addAll(files);
|
||||
|
||||
_logger.d('Multipart POST request headers: ${request.headers}');
|
||||
_logger.d('Multipart POST request fields: ${request.fields}');
|
||||
_logger.d(
|
||||
'Multipart POST request files: ${request.files.length} files',
|
||||
);
|
||||
|
||||
try {
|
||||
final streamedResponse = await request.send();
|
||||
final response = await http.Response.fromStream(streamedResponse);
|
||||
@@ -694,9 +672,6 @@ class ApiService {
|
||||
contentType: contentType,
|
||||
);
|
||||
|
||||
_logger.d('CSRF-protected POST headers: $postHeaders');
|
||||
_logger.d('CSRF-protected POST body: $encodedBody');
|
||||
|
||||
try {
|
||||
final response = await _client!.post(
|
||||
uri,
|
||||
@@ -749,13 +724,10 @@ class ApiService {
|
||||
|
||||
request.files.addAll(files);
|
||||
|
||||
_logger.d('Multipart POST request to: $uri');
|
||||
_logger.d('Multipart headers: ${request.headers}');
|
||||
|
||||
try {
|
||||
final streamedResponse = await request.send();
|
||||
final response = await http.Response.fromStream(streamedResponse);
|
||||
_logger.i('Multipart POST response status: ${response.statusCode}');
|
||||
_logger.d('Multipart POST $uri -> ${response.statusCode}');
|
||||
|
||||
if (response.headers.containsKey('set-cookie')) {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
@@ -785,9 +757,6 @@ class ApiService {
|
||||
|
||||
headers.addAll(customHeaders);
|
||||
|
||||
_logger.d('POST request to: $uri');
|
||||
_logger.d('Headers: $headers');
|
||||
|
||||
final encodedBody = _encodeBody(body: body, contentType: contentType);
|
||||
|
||||
try {
|
||||
@@ -796,7 +765,7 @@ class ApiService {
|
||||
headers: headers,
|
||||
body: encodedBody ?? "",
|
||||
);
|
||||
_logger.i('POST response status: ${response.statusCode}');
|
||||
_logger.d('POST $uri -> ${response.statusCode}');
|
||||
|
||||
if (response.headers.containsKey('set-cookie')) {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
@@ -847,12 +816,9 @@ class ApiService {
|
||||
|
||||
request.headers.addAll(headers);
|
||||
|
||||
_logger.d('GET stream request to: ${uri.toString()}');
|
||||
_logger.d('Headers: $headers');
|
||||
|
||||
try {
|
||||
final response = await _client!.send(request);
|
||||
_logger.i('Stream response status: ${response.statusCode}');
|
||||
_logger.d('GET (stream) $uri -> ${response.statusCode}');
|
||||
_checkResponseStatus(statusCode: response.statusCode);
|
||||
return response;
|
||||
} catch (e) {
|
||||
@@ -945,7 +911,6 @@ class ApiService {
|
||||
fullPath = '/$endpoint';
|
||||
}
|
||||
|
||||
_logger.d('Built URL: $_baseUrl$fullPath');
|
||||
return Uri.parse(
|
||||
'$_baseUrl$fullPath',
|
||||
).replace(queryParameters: queryParams);
|
||||
@@ -996,13 +961,10 @@ class ApiService {
|
||||
if (resolvedAuthMethod == AuthMethod.auto) {
|
||||
if (_username != null && _username!.isNotEmpty && _password != null) {
|
||||
resolvedAuthMethod = AuthMethod.basic;
|
||||
_logger.d('Auto-Auth: Resolved to Basic (Credentials available)');
|
||||
} else if (_cookie != null && _cookie!.isNotEmpty) {
|
||||
resolvedAuthMethod = AuthMethod.cookie;
|
||||
_logger.d('Auto-Auth: Resolved to Cookie (No credentials)');
|
||||
} else {
|
||||
resolvedAuthMethod = AuthMethod.none;
|
||||
_logger.d('Auto-Auth: Resolved to None');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1104,10 +1066,21 @@ class ApiService {
|
||||
throw Exception('Failed to get CSRF token for upload');
|
||||
}
|
||||
|
||||
// Start with the stored session cookie and merge any new cookies from the
|
||||
// CSRF GET response. Without this, the upload POST sends an empty Cookie
|
||||
// header whenever the server doesn't re-issue cookies on the CSRF fetch
|
||||
// (i.e. when the session is already valid), causing a 400.
|
||||
String sessionCookie = _cookie ?? '';
|
||||
final rawSetCookie = csrfResult['cookies'];
|
||||
if (rawSetCookie != null && rawSetCookie.isNotEmpty) {
|
||||
final newCookie = buildCookieHeaderFromSetCookie(rawSetCookie);
|
||||
sessionCookie = _mergeCookieHeaders(sessionCookie, newCookie);
|
||||
}
|
||||
|
||||
final uri = _buildUri(endpoint: endpoint);
|
||||
final request = http.MultipartRequest('POST', uri);
|
||||
|
||||
request.headers['Cookie'] = csrfResult['cookies'] ?? '';
|
||||
request.headers['Cookie'] = sessionCookie;
|
||||
|
||||
request.fields['csrf_token'] = csrfToken;
|
||||
|
||||
@@ -1118,7 +1091,22 @@ class ApiService {
|
||||
final customHeaders = await _processCustomHeaders();
|
||||
request.headers.addAll(customHeaders);
|
||||
|
||||
final fileName = file.path.split('/').last;
|
||||
final rawFileName = file.path.split('/').last;
|
||||
// Sanitize filename to match werkzeug secure_filename behavior: calibre-web
|
||||
// rejects filenames with parentheses, brackets, spaces, and other special
|
||||
// characters, returning a 400. Strip to ASCII alphanumeric + hyphens + dots.
|
||||
final dotIndex = rawFileName.lastIndexOf('.');
|
||||
final rawName =
|
||||
dotIndex != -1 ? rawFileName.substring(0, dotIndex) : rawFileName;
|
||||
final ext = dotIndex != -1 ? rawFileName.substring(dotIndex) : '';
|
||||
final sanitizedName = rawName
|
||||
.replaceAll(RegExp(r'[^a-zA-Z0-9_\-]'), '_')
|
||||
.replaceAll(RegExp(r'_+'), '_')
|
||||
.replaceAll(RegExp(r'^_+|_+$'), '');
|
||||
final fileName = '${sanitizedName.isEmpty ? 'upload' : sanitizedName}$ext';
|
||||
if (fileName != rawFileName) {
|
||||
_logger.i('Sanitized filename: $rawFileName → $fileName');
|
||||
}
|
||||
final fileExtension = fileName.split('.').last.toLowerCase();
|
||||
|
||||
String contentType = 'application/octet-stream';
|
||||
|
||||
@@ -779,9 +779,9 @@ class BookDetailsBloc extends Bloc<BookDetailsEvent, BookDetailsState> {
|
||||
LoadReadingProgress event,
|
||||
Emitter<BookDetailsState> emit,
|
||||
) async {
|
||||
final location = await progressRepository.getBestLocation(event.bookUuid);
|
||||
final cfi = await progressRepository.getBestLocation(event.bookUuid);
|
||||
|
||||
emit(state.copyWith(startLocation: location));
|
||||
emit(state.copyWith(startCfi: cfi));
|
||||
}
|
||||
|
||||
Future<void> _onSyncReadingProgress(
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'package:equatable/equatable.dart';
|
||||
|
||||
import 'package:calibre_web_companion/features/book_details/data/models/book_details_model.dart';
|
||||
import 'package:calibre_web_companion/features/book_view/data/models/book_view_model.dart';
|
||||
import 'package:vocsy_epub_viewer/epub_viewer.dart';
|
||||
|
||||
enum BookDetailsStatus { initial, loading, loaded, error }
|
||||
|
||||
@@ -64,7 +63,7 @@ class BookDetailsState extends Equatable {
|
||||
final BookViewModel? bookViewModel;
|
||||
final SeriesNavigationStatus seriesNavigationStatus;
|
||||
final String? seriesNavigationPath;
|
||||
final EpubLocator? startLocation;
|
||||
final String? startCfi;
|
||||
final bool isDownloaded;
|
||||
|
||||
const BookDetailsState({
|
||||
@@ -90,7 +89,7 @@ class BookDetailsState extends Equatable {
|
||||
this.bookViewModel,
|
||||
this.seriesNavigationStatus = SeriesNavigationStatus.initial,
|
||||
this.seriesNavigationPath,
|
||||
this.startLocation,
|
||||
this.startCfi,
|
||||
this.isDownloaded = false,
|
||||
});
|
||||
|
||||
@@ -117,7 +116,7 @@ class BookDetailsState extends Equatable {
|
||||
BookViewModel? bookViewModel,
|
||||
SeriesNavigationStatus? seriesNavigationStatus,
|
||||
String? seriesNavigationPath,
|
||||
EpubLocator? startLocation,
|
||||
String? startCfi,
|
||||
bool? isDownloaded,
|
||||
}) {
|
||||
return BookDetailsState(
|
||||
@@ -146,7 +145,7 @@ class BookDetailsState extends Equatable {
|
||||
seriesNavigationStatus:
|
||||
seriesNavigationStatus ?? this.seriesNavigationStatus,
|
||||
seriesNavigationPath: seriesNavigationPath ?? this.seriesNavigationPath,
|
||||
startLocation: startLocation ?? this.startLocation,
|
||||
startCfi: startCfi ?? this.startCfi,
|
||||
isDownloaded: isDownloaded ?? this.isDownloaded,
|
||||
);
|
||||
}
|
||||
@@ -175,7 +174,7 @@ class BookDetailsState extends Equatable {
|
||||
bookViewModel,
|
||||
seriesNavigationStatus,
|
||||
seriesNavigationPath,
|
||||
startLocation,
|
||||
startCfi,
|
||||
isDownloaded,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -293,6 +293,7 @@ class BookDetailsRemoteDatasource {
|
||||
body: body,
|
||||
authMethod: AuthMethod.cookie,
|
||||
useCsrf: true,
|
||||
csrfTokenUrl: '/me',
|
||||
csrfOnlyInHeader: true,
|
||||
contentType: 'application/x-www-form-urlencoded',
|
||||
);
|
||||
@@ -726,7 +727,9 @@ class BookDetailsRemoteDatasource {
|
||||
selectedDirectory: selectedDirectory,
|
||||
schema: schema,
|
||||
format: selectedFormat,
|
||||
reuseExistingFile: false,
|
||||
// Reuse an already-downloaded file instead of fetching it again every
|
||||
// time the reader is opened.
|
||||
reuseExistingFile: true,
|
||||
progressCallback: progressCallback,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import 'dart:convert';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:calibre_web_companion/core/services/webdav_sync_service.dart';
|
||||
import 'package:vocsy_epub_viewer/epub_viewer.dart';
|
||||
|
||||
class ReadingProgressRepository {
|
||||
final WebDavSyncService webDavService;
|
||||
|
||||
ReadingProgressRepository({required this.webDavService});
|
||||
|
||||
Future<EpubLocator?> getBestLocation(String bookUuid) async {
|
||||
Future<String?> getBestLocation(String bookUuid) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final String progressKey = 'book_progress_$bookUuid';
|
||||
final String timestampKey = 'book_timestamp_$bookUuid';
|
||||
@@ -45,16 +44,31 @@ class ReadingProgressRepository {
|
||||
}
|
||||
|
||||
if (locationJsonToUse != null && locationJsonToUse.isNotEmpty) {
|
||||
try {
|
||||
final Map<String, dynamic> decodedMap = jsonDecode(locationJsonToUse);
|
||||
return EpubLocator.fromJson(decodedMap);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
return _extractCfi(locationJsonToUse);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _extractCfi(String stored) {
|
||||
final trimmed = stored.trim();
|
||||
|
||||
if (!trimmed.startsWith('{')) {
|
||||
return trimmed.isEmpty ? null : trimmed;
|
||||
}
|
||||
|
||||
try {
|
||||
final decoded = jsonDecode(trimmed);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
final locations = decoded['locations'];
|
||||
if (locations is Map && locations['cfi'] is String) {
|
||||
final cfi = locations['cfi'] as String;
|
||||
return cfi.isEmpty ? null : cfi;
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> saveProgress(String bookUuid, String locatorJson) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final String progressKey = 'book_progress_$bookUuid';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:docman/docman.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -32,7 +33,10 @@ import 'package:calibre_web_companion/features/settings/presentation/pages/setti
|
||||
import 'package:calibre_web_companion/features/book_details/data/models/book_details_model.dart';
|
||||
import 'package:calibre_web_companion/shared/widgets/book_cover_widget.dart';
|
||||
import 'package:calibre_web_companion/l10n/app_localizations.dart';
|
||||
import 'package:vocsy_epub_viewer/epub_viewer.dart';
|
||||
import 'package:cosmos_epub/cosmos_epub.dart';
|
||||
// Exposes cosmos_epub's `bookProgress` singleton for cross-device WebDAV sync.
|
||||
import 'package:cosmos_epub/show_epub.dart' as cosmos_reader;
|
||||
import 'package:calibre_web_companion/shared/widgets/app_dialog_button.dart';
|
||||
import 'package:calibre_web_companion/core/services/webdav_sync_service.dart';
|
||||
|
||||
class BookDetailsPage extends StatefulWidget {
|
||||
@@ -51,7 +55,6 @@ class BookDetailsPage extends StatefulWidget {
|
||||
|
||||
class _BookDetailsPageState extends State<BookDetailsPage> {
|
||||
bool _didUpdateMetadata = false;
|
||||
StreamSubscription<dynamic>? _locatorSubscription;
|
||||
late final WebDavSyncService _webDavService;
|
||||
|
||||
bool _isInternalReaderSupportedFormat(String format) {
|
||||
@@ -184,23 +187,13 @@ class _BookDetailsPageState extends State<BookDetailsPage> {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_locatorSubscription?.cancel();
|
||||
VocsyEpub.closeReader();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _openInternalReader(
|
||||
BuildContext context,
|
||||
String filePath,
|
||||
BookDetailsModel bookDetailsModel,
|
||||
) async {
|
||||
final lastLocation = context.read<BookDetailsBloc>().state.startLocation;
|
||||
final settingsState = context.read<SettingsBloc>().state;
|
||||
|
||||
final localization = AppLocalizations.of(context)!;
|
||||
if (!filePath.toLowerCase().endsWith('.epub')) {
|
||||
final localization = AppLocalizations.of(context)!;
|
||||
context.showSnackBar(
|
||||
localization.errorOpeningBookInInternalReaderPdf,
|
||||
isError: true,
|
||||
@@ -209,28 +202,88 @@ class _BookDetailsPageState extends State<BookDetailsPage> {
|
||||
return;
|
||||
}
|
||||
|
||||
VocsyEpub.setConfig(
|
||||
themeColor: Theme.of(context).primaryColor,
|
||||
identifier: "book_${bookDetailsModel.uuid}",
|
||||
scrollDirection:
|
||||
settingsState.epubScrollDirection == 'horizontal'
|
||||
? EpubScrollDirection.HORIZONTAL
|
||||
: EpubScrollDirection.VERTICAL,
|
||||
allowSharing: true,
|
||||
enableTts: true,
|
||||
nightMode: Theme.of(context).brightness == Brightness.dark,
|
||||
);
|
||||
final bookUuid = bookDetailsModel.uuid;
|
||||
|
||||
await _locatorSubscription?.cancel();
|
||||
await _restoreReaderProgressFromCloud(bookUuid);
|
||||
if (!context.mounted) return;
|
||||
|
||||
_locatorSubscription = VocsyEpub.locatorStream.listen((locator) {
|
||||
// ignore: use_build_context_synchronously
|
||||
context.read<BookDetailsBloc>().add(
|
||||
SyncReadingProgress(bookDetailsModel.uuid, locator),
|
||||
try {
|
||||
await CosmosEpub.openLocalBook(
|
||||
context: context,
|
||||
localPath: filePath,
|
||||
bookId: bookUuid,
|
||||
accentColor: Theme.of(context).colorScheme.primary,
|
||||
);
|
||||
});
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
context.showSnackBar(
|
||||
'${localization.errorOpeningBookInInternalReader} $e',
|
||||
isError: true,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
VocsyEpub.open(filePath, lastLocation: lastLocation);
|
||||
await _saveReaderProgressToCloud(bookUuid);
|
||||
}
|
||||
|
||||
Future<void> _restoreReaderProgressFromCloud(String bookUuid) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (!(prefs.getBool('webdav_enabled') ?? false)) return;
|
||||
final url = prefs.getString('webdav_url') ?? '';
|
||||
if (url.isEmpty) return;
|
||||
try {
|
||||
_webDavService.init(
|
||||
url,
|
||||
prefs.getString('webdav_username') ?? '',
|
||||
prefs.getString('webdav_password') ?? '',
|
||||
);
|
||||
|
||||
final localTs = prefs.getInt('reader_progress_ts_$bookUuid') ?? 0;
|
||||
final serverData = await _webDavService.fetchProgress();
|
||||
final entry = serverData[bookUuid];
|
||||
if (entry is! Map) return;
|
||||
final serverTs = (entry['timestamp'] as int?) ?? 0;
|
||||
if (serverTs <= localTs) return;
|
||||
|
||||
final decoded = jsonDecode(entry['locator'] as String);
|
||||
if (decoded is! Map) return;
|
||||
final chapter = decoded['chapter'] as int?;
|
||||
final page = decoded['page'] as int?;
|
||||
if (chapter != null) {
|
||||
await cosmos_reader.bookProgress.setCurrentChapterIndex(
|
||||
bookUuid,
|
||||
chapter,
|
||||
);
|
||||
}
|
||||
if (page != null) {
|
||||
await cosmos_reader.bookProgress.setCurrentPageIndex(bookUuid, page);
|
||||
}
|
||||
await prefs.setInt('reader_progress_ts_$bookUuid', serverTs);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<void> _saveReaderProgressToCloud(String bookUuid) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (!(prefs.getBool('webdav_enabled') ?? false)) return;
|
||||
final url = prefs.getString('webdav_url') ?? '';
|
||||
if (url.isEmpty) return;
|
||||
try {
|
||||
_webDavService.init(
|
||||
url,
|
||||
prefs.getString('webdav_username') ?? '',
|
||||
prefs.getString('webdav_password') ?? '',
|
||||
);
|
||||
|
||||
final progress = cosmos_reader.bookProgress.getBookProgress(bookUuid);
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
final locator = jsonEncode({
|
||||
'chapter': progress.currentChapterIndex ?? 0,
|
||||
'page': progress.currentPageIndex ?? 0,
|
||||
});
|
||||
await _webDavService.saveProgress(bookUuid, locator, now);
|
||||
await prefs.setInt('reader_progress_ts_$bookUuid', now);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1139,26 +1192,16 @@ class _BookDetailsPageState extends State<BookDetailsPage> {
|
||||
title: Text(localizations.deleteBook),
|
||||
content: Text(localizations.deleteBookConfirmation),
|
||||
actions: [
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.error,
|
||||
),
|
||||
onPressed:
|
||||
() => Navigator.of(context).pop(true),
|
||||
child: Text(
|
||||
localizations.delete,
|
||||
style: TextStyle(
|
||||
color:
|
||||
Theme.of(context).colorScheme.onError,
|
||||
),
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
TextButton(
|
||||
onPressed:
|
||||
() => Navigator.of(context).pop(false),
|
||||
child: Text(localizations.cancel),
|
||||
),
|
||||
AppDialogButton.destructive(
|
||||
onPressed:
|
||||
() => Navigator.of(context).pop(true),
|
||||
label: localizations.delete,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:calibre_web_companion/shared/widgets/app_skeletonizer.dart';
|
||||
import 'package:calibre_web_companion/shared/widgets/app_dialog_button.dart';
|
||||
|
||||
import 'package:calibre_web_companion/core/services/snackbar.dart';
|
||||
import 'package:calibre_web_companion/features/book_details/data/models/book_details_model.dart';
|
||||
@@ -275,9 +276,9 @@ class _AddToShelfWidgetState extends State<AddToShelfWidget> {
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
ElevatedButton(
|
||||
AppDialogButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(localizations.close),
|
||||
label: localizations.close,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:calibre_web_companion/core/di/injection_container.dart';
|
||||
import 'package:calibre_web_companion/features/book_details/data/datasources/book_details_remote_datasource.dart';
|
||||
import 'package:calibre_web_companion/features/book_details/data/models/metadata_models.dart';
|
||||
import 'package:calibre_web_companion/l10n/app_localizations.dart';
|
||||
import 'package:calibre_web_companion/shared/widgets/app_dialog_button.dart';
|
||||
|
||||
class MetadataSearchDialog extends StatefulWidget {
|
||||
final String initialQuery;
|
||||
@@ -242,8 +243,9 @@ class _MetadataMergeDialogState extends State<_MetadataMergeDialog> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localizations = AppLocalizations.of(context)!;
|
||||
return AlertDialog(
|
||||
title: const Text("Select Metadata to Import"),
|
||||
title: Text(localizations.selectMetadataToImport),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -305,11 +307,11 @@ class _MetadataMergeDialogState extends State<_MetadataMergeDialog> {
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text("Cancel"),
|
||||
child: Text(localizations.cancel),
|
||||
),
|
||||
ElevatedButton(
|
||||
AppDialogButton(
|
||||
onPressed: () => Navigator.of(context).pop(_selection),
|
||||
child: const Text("Apply"),
|
||||
label: localizations.apply,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:calibre_web_companion/features/book_details/bloc/book_details_st
|
||||
|
||||
import 'package:calibre_web_companion/l10n/app_localizations.dart';
|
||||
import 'package:calibre_web_companion/core/services/snackbar.dart';
|
||||
import 'package:calibre_web_companion/shared/widgets/app_dialog_button.dart';
|
||||
import 'package:calibre_web_companion/features/book_details/data/models/book_details_model.dart';
|
||||
import 'package:calibre_web_companion/features/settings/bloc/settings_bloc.dart';
|
||||
import 'package:calibre_web_companion/features/settings/bloc/settings_state.dart';
|
||||
@@ -181,11 +182,7 @@ class SendToEreaderWidget extends StatelessWidget {
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(localizations.cancel),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primaryContainer,
|
||||
),
|
||||
AppDialogButton(
|
||||
onPressed:
|
||||
() => _handleSendAction(
|
||||
parentContext,
|
||||
@@ -195,13 +192,7 @@ class SendToEreaderWidget extends StatelessWidget {
|
||||
codeController,
|
||||
isKindle,
|
||||
),
|
||||
child: Text(
|
||||
localizations.send,
|
||||
style: TextStyle(
|
||||
color:
|
||||
Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
label: localizations.send,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:calibre_web_companion/l10n/app_localizations.dart';
|
||||
import 'package:calibre_web_companion/shared/widgets/app_dialog_button.dart';
|
||||
|
||||
class SearchDialog extends StatefulWidget {
|
||||
const SearchDialog({super.key});
|
||||
@@ -47,14 +48,11 @@ class SearchDialogState extends State<SearchDialog> {
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(localizations.cancel),
|
||||
),
|
||||
ElevatedButton(
|
||||
AppDialogButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(_controller.text);
|
||||
},
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [Text(localizations.search)],
|
||||
),
|
||||
label: localizations.search,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:calibre_web_companion/shared/widgets/app_skeletonizer.dart';
|
||||
import 'package:calibre_web_companion/shared/utils/status_colors.dart';
|
||||
|
||||
import 'package:calibre_web_companion/features/download_service/bloc/download_service_bloc.dart';
|
||||
import 'package:calibre_web_companion/features/download_service/bloc/download_service_event.dart';
|
||||
@@ -317,32 +318,32 @@ class _BookCardWidgetState extends State<BookCardWidget> {
|
||||
|
||||
switch (widget.book.status) {
|
||||
case DownloaderStatus.available:
|
||||
statusColor = Colors.blue;
|
||||
statusColor = StatusColors.info(context);
|
||||
statusIcon = Icons.download_rounded;
|
||||
statusText = localizations.available;
|
||||
break;
|
||||
case DownloaderStatus.downloading:
|
||||
statusColor = Colors.amber;
|
||||
statusColor = StatusColors.warning(context);
|
||||
statusIcon = Icons.downloading_rounded;
|
||||
statusText = localizations.downloading;
|
||||
break;
|
||||
case DownloaderStatus.done:
|
||||
statusColor = Colors.green;
|
||||
statusColor = StatusColors.success(context);
|
||||
statusIcon = Icons.check_circle_outline_rounded;
|
||||
statusText = localizations.completed;
|
||||
break;
|
||||
case DownloaderStatus.error:
|
||||
statusColor = Colors.red;
|
||||
statusColor = StatusColors.error(context);
|
||||
statusIcon = Icons.error_outline_rounded;
|
||||
statusText = localizations.failed;
|
||||
break;
|
||||
case DownloaderStatus.queued:
|
||||
statusColor = Colors.purple;
|
||||
statusColor = StatusColors.pending(context);
|
||||
statusIcon = Icons.queue_rounded;
|
||||
statusText = localizations.queued;
|
||||
break;
|
||||
case DownloaderStatus.notDownloaded:
|
||||
statusColor = Colors.grey;
|
||||
statusColor = StatusColors.neutral(context);
|
||||
statusIcon = Icons.download_rounded;
|
||||
statusText = localizations.notDownloaded;
|
||||
break;
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:calibre_web_companion/features/login/bloc/login_state.dart';
|
||||
|
||||
import 'package:calibre_web_companion/l10n/app_localizations.dart';
|
||||
import 'package:calibre_web_companion/core/services/snackbar.dart';
|
||||
import 'package:calibre_web_companion/shared/widgets/app_dialog_button.dart';
|
||||
import 'package:calibre_web_companion/features/login/presentation/widgets/login_text_field.dart';
|
||||
import 'package:calibre_web_companion/core/services/app_transition.dart';
|
||||
import 'package:calibre_web_companion/features/login_settings/presentation/pages/login_settings_page.dart';
|
||||
@@ -517,20 +518,12 @@ class _LoginFormState extends State<LoginForm> {
|
||||
title: Text(localizations.credentialsRequiredForSSO),
|
||||
content: Text(localizations.enterUsernamePasswordForSSO),
|
||||
actions: [
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
AppDialogButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
if (_usernameController.text.isEmpty) {}
|
||||
},
|
||||
child: Text(
|
||||
localizations.ok,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
label: localizations.ok,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:calibre_web_companion/features/login_settings/bloc/login_setting
|
||||
|
||||
import 'package:calibre_web_companion/l10n/app_localizations.dart';
|
||||
import 'package:calibre_web_companion/core/services/snackbar.dart';
|
||||
import 'package:calibre_web_companion/shared/widgets/app_dialog_button.dart';
|
||||
import 'package:calibre_web_companion/features/login_settings/presentation/widgets/header_section_widget.dart';
|
||||
|
||||
class LoginSettingsPage extends StatefulWidget {
|
||||
@@ -177,8 +178,8 @@ class _LoginSettingsPage extends State<LoginSettingsPage> {
|
||||
child: Text(localizations.cancel),
|
||||
onPressed: () => Navigator.of(ctx).pop(false),
|
||||
),
|
||||
ElevatedButton(
|
||||
child: Text(localizations.ok),
|
||||
AppDialogButton(
|
||||
label: localizations.ok,
|
||||
onPressed: () => Navigator.of(ctx).pop(true),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -16,6 +16,7 @@ import 'package:calibre_web_companion/core/services/app_transition.dart';
|
||||
import 'package:calibre_web_companion/core/services/snackbar.dart';
|
||||
import 'package:calibre_web_companion/features/me/presentation/widgets/stats_card_widget.dart';
|
||||
import 'package:calibre_web_companion/shared/widgets/long_button_widget.dart';
|
||||
import 'package:calibre_web_companion/shared/widgets/app_dialog_button.dart';
|
||||
import 'package:calibre_web_companion/features/login/presentation/pages/login_page.dart';
|
||||
import 'package:calibre_web_companion/features/settings/presentation/pages/settings_page.dart';
|
||||
import 'package:calibre_web_companion/features/shelf_view.dart/presentation/pages/shelf_view_page.dart';
|
||||
@@ -358,23 +359,14 @@ class MePage extends StatelessWidget {
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
child: Text(localizations.cancel),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primaryContainer,
|
||||
),
|
||||
AppDialogButton(
|
||||
onPressed: () {
|
||||
Navigator.of(dialogContext).pop();
|
||||
Navigator.of(sheetContext).pop();
|
||||
|
||||
context.read<LoginBloc>().add(SwitchAccount(account));
|
||||
},
|
||||
child: Text(
|
||||
localizations.switchAccount,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
label: localizations.switchAccount,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -397,17 +389,12 @@ class MePage extends StatelessWidget {
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
child: Text(localizations.cancel),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.errorContainer,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onErrorContainer,
|
||||
),
|
||||
AppDialogButton.destructive(
|
||||
onPressed: () {
|
||||
Navigator.of(dialogContext).pop();
|
||||
context.read<LoginBloc>().add(DeleteAccount(account));
|
||||
},
|
||||
child: Text(localizations.delete),
|
||||
label: localizations.delete,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -431,21 +418,12 @@ class MePage extends StatelessWidget {
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
child: Text(localizations.cancel),
|
||||
),
|
||||
ElevatedButton(
|
||||
AppDialogButton(
|
||||
onPressed: () {
|
||||
Navigator.of(dialogContext).pop();
|
||||
_performLogout(context);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primaryContainer,
|
||||
),
|
||||
child: Text(
|
||||
localizations.logout,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
label: localizations.logout,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -36,7 +36,6 @@ class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
|
||||
on<SetWebDavCredentials>(_onSetWebDavCredentials);
|
||||
on<TestDownloaderConnection>(_onTestDownloaderConnection);
|
||||
on<TestWebDavConnection>(_onTestWebDavConnection);
|
||||
on<SetEpubScrollDirection>(_onSetEpubScrollDirection);
|
||||
on<ResetConnectionTestStatus>(_onResetConnectionTestStatus);
|
||||
on<SetShowSendToEReaderButton>(_onSetShowSendToEReaderButton);
|
||||
on<SetEInkMode>(_onSetEInkMode);
|
||||
@@ -92,7 +91,6 @@ class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
|
||||
webDavUsername: settings.webDavUsername,
|
||||
webDavPassword: settings.webDavPassword,
|
||||
isWebDavSyncEnabled: settings.isWebDavSyncEnabled,
|
||||
epubScrollDirection: settings.epubScrollDirection,
|
||||
isEInkMode: settings.isEInkMode,
|
||||
bookActionsOrder: settings.bookActionsOrder,
|
||||
enabledBookActions: settings.enabledBookActions,
|
||||
@@ -499,23 +497,6 @@ class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onSetEpubScrollDirection(
|
||||
SetEpubScrollDirection event,
|
||||
Emitter<SettingsState> emit,
|
||||
) async {
|
||||
try {
|
||||
await repository.setEpubScrollDirection(event.direction);
|
||||
emit(state.copyWith(epubScrollDirection: event.direction));
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: SettingsStatus.error,
|
||||
errorMessage: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onResetConnectionTestStatus(
|
||||
ResetConnectionTestStatus event,
|
||||
Emitter<SettingsState> emit,
|
||||
|
||||
@@ -200,15 +200,6 @@ class TestWebDavConnection extends SettingsEvent {
|
||||
List<Object?> get props => [url, username, password];
|
||||
}
|
||||
|
||||
class SetEpubScrollDirection extends SettingsEvent {
|
||||
final String direction;
|
||||
|
||||
const SetEpubScrollDirection(this.direction);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [direction];
|
||||
}
|
||||
|
||||
class ResetConnectionTestStatus extends SettingsEvent {}
|
||||
|
||||
class SetShowSendToEReaderButton extends SettingsEvent {
|
||||
|
||||
@@ -36,7 +36,6 @@ class SettingsState extends Equatable {
|
||||
final String webDavUrl;
|
||||
final String webDavUsername;
|
||||
final String webDavPassword;
|
||||
final String epubScrollDirection;
|
||||
final bool isEInkMode;
|
||||
final List<String> bookActionsOrder;
|
||||
final List<String> enabledBookActions;
|
||||
@@ -78,7 +77,6 @@ class SettingsState extends Equatable {
|
||||
this.webDavUrl = '',
|
||||
this.webDavUsername = '',
|
||||
this.webDavPassword = '',
|
||||
this.epubScrollDirection = 'vertical',
|
||||
this.isEInkMode = false,
|
||||
this.bookActionsOrder = const [],
|
||||
this.enabledBookActions = const [],
|
||||
@@ -123,7 +121,6 @@ class SettingsState extends Equatable {
|
||||
String? webDavUrl,
|
||||
String? webDavUsername,
|
||||
String? webDavPassword,
|
||||
String? epubScrollDirection,
|
||||
bool? isEInkMode,
|
||||
List<String>? bookActionsOrder,
|
||||
List<String>? enabledBookActions,
|
||||
@@ -168,7 +165,6 @@ class SettingsState extends Equatable {
|
||||
webDavUrl: webDavUrl ?? this.webDavUrl,
|
||||
webDavUsername: webDavUsername ?? this.webDavUsername,
|
||||
webDavPassword: webDavPassword ?? this.webDavPassword,
|
||||
epubScrollDirection: epubScrollDirection ?? this.epubScrollDirection,
|
||||
isEInkMode: isEInkMode ?? this.isEInkMode,
|
||||
bookActionsOrder: bookActionsOrder ?? this.bookActionsOrder,
|
||||
enabledBookActions: enabledBookActions ?? this.enabledBookActions,
|
||||
@@ -216,7 +212,6 @@ class SettingsState extends Equatable {
|
||||
webDavUrl,
|
||||
webDavUsername,
|
||||
webDavPassword,
|
||||
epubScrollDirection,
|
||||
isEInkMode,
|
||||
bookActionsOrder,
|
||||
enabledBookActions,
|
||||
|
||||
@@ -310,15 +310,6 @@ class SettingsLocalDataSource {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveEpubScrollDirection(String direction) async {
|
||||
try {
|
||||
await sharedPreferences.setString('epub_scroll_direction', direction);
|
||||
} catch (e) {
|
||||
logger.e('Error saving epub scroll direction: $e');
|
||||
throw Exception('Failed to save epub scroll direction: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveShowSendToEReaderButton(bool enabled) async {
|
||||
try {
|
||||
await sharedPreferences.setBool('show_send_to_ereader_button', enabled);
|
||||
|
||||
@@ -27,7 +27,6 @@ class SettingsModel extends Equatable {
|
||||
final String webDavUsername;
|
||||
final String webDavPassword;
|
||||
final bool isWebDavSyncEnabled;
|
||||
final String epubScrollDirection;
|
||||
final bool isEInkMode;
|
||||
final List<String> bookActionsOrder;
|
||||
final List<String> enabledBookActions;
|
||||
@@ -60,7 +59,6 @@ class SettingsModel extends Equatable {
|
||||
required this.webDavUsername,
|
||||
required this.webDavPassword,
|
||||
required this.isWebDavSyncEnabled,
|
||||
required this.epubScrollDirection,
|
||||
required this.isEInkMode,
|
||||
required this.bookActionsOrder,
|
||||
required this.enabledBookActions,
|
||||
@@ -96,7 +94,6 @@ class SettingsModel extends Equatable {
|
||||
webDavUsername: json['webdav_username'] ?? '',
|
||||
webDavPassword: json['webdav_password'] ?? '',
|
||||
isWebDavSyncEnabled: json['webdav_enabled'] ?? false,
|
||||
epubScrollDirection: json['epub_scroll_direction'] ?? 'vertical',
|
||||
isEInkMode: json['is_eink_mode'] ?? false,
|
||||
bookActionsOrder: BookDetailsActionConfig.normalizeOrder(
|
||||
List<String>.from(
|
||||
@@ -182,7 +179,6 @@ class SettingsModel extends Equatable {
|
||||
webDavUsername,
|
||||
webDavPassword,
|
||||
isWebDavSyncEnabled,
|
||||
epubScrollDirection,
|
||||
isEInkMode,
|
||||
bookActionsOrder,
|
||||
enabledBookActions,
|
||||
|
||||
@@ -235,14 +235,6 @@ class SettingsRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setEpubScrollDirection(String direction) async {
|
||||
try {
|
||||
await dataSource.saveEpubScrollDirection(direction);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setEInkMode(bool enabled) async {
|
||||
try {
|
||||
await dataSource.saveEInkMode(enabled);
|
||||
|
||||
@@ -143,7 +143,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
_buildSettingsCategoryNavCard(
|
||||
context,
|
||||
title: localizations.readerSettings,
|
||||
subtitle: localizations.scrollDirection,
|
||||
subtitle: localizations.webDavSync,
|
||||
icon: Icons.chrome_reader_mode_rounded,
|
||||
onTap: () => _openReaderSettingsSubPage(context),
|
||||
),
|
||||
@@ -334,8 +334,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
title: localizations.readerSettings,
|
||||
bodyBuilder:
|
||||
(context, state, localizations) => [
|
||||
_buildSectionTitle(context, localizations.readerSettings),
|
||||
_buildReaderSettings(context, state, localizations),
|
||||
_buildSectionTitle(context, localizations.webDavSync),
|
||||
_buildWebDavSettings(context, state, localizations),
|
||||
],
|
||||
);
|
||||
@@ -2131,76 +2130,4 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildReaderSettings(
|
||||
BuildContext context,
|
||||
SettingsState state,
|
||||
AppLocalizations localizations,
|
||||
) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
elevation: 3,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.chrome_reader_mode_rounded,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Text(
|
||||
localizations.scrollDirection,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<String>(
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
),
|
||||
initialValue: state.epubScrollDirection,
|
||||
icon: const Icon(Icons.arrow_drop_down),
|
||||
elevation: 16,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontSize: 16,
|
||||
),
|
||||
onChanged: (String? newValue) {
|
||||
if (newValue != null) {
|
||||
context.read<SettingsBloc>().add(
|
||||
SetEpubScrollDirection(newValue),
|
||||
);
|
||||
}
|
||||
},
|
||||
items: [
|
||||
DropdownMenuItem<String>(
|
||||
value: 'vertical',
|
||||
child: Text(localizations.vertical),
|
||||
),
|
||||
DropdownMenuItem<String>(
|
||||
value: 'horizontal',
|
||||
child: Text(localizations.horizontal),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:calibre_web_companion/features/settings/bloc/settings_event.dart
|
||||
import 'package:calibre_web_companion/features/settings/bloc/settings_state.dart';
|
||||
|
||||
import 'package:calibre_web_companion/core/services/snackbar.dart';
|
||||
import 'package:calibre_web_companion/shared/widgets/app_dialog_button.dart';
|
||||
import 'package:calibre_web_companion/features/settings/data/models/download_schema.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:calibre_web_companion/l10n/app_localizations.dart';
|
||||
@@ -329,11 +330,11 @@ class DownloadOptionsWidget extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
ElevatedButton(
|
||||
child: Text(localizations.cancel),
|
||||
AppDialogButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
label: localizations.cancel,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:calibre_web_companion/l10n/app_localizations.dart';
|
||||
import 'package:calibre_web_companion/shared/widgets/app_dialog_button.dart';
|
||||
|
||||
import 'package:calibre_web_companion/features/settings/bloc/settings_bloc.dart';
|
||||
import 'package:calibre_web_companion/features/settings/bloc/settings_event.dart';
|
||||
@@ -141,7 +142,9 @@ class FeedbackWidget extends StatelessWidget {
|
||||
final isSubmitting =
|
||||
state.feedbackStatus == SettingsFeedbackStatus.loading;
|
||||
|
||||
return ElevatedButton(
|
||||
return AppDialogButton(
|
||||
isLoading: isSubmitting,
|
||||
label: localizations.submit,
|
||||
onPressed:
|
||||
isSubmitting
|
||||
? null
|
||||
@@ -172,19 +175,6 @@ class FeedbackWidget extends StatelessWidget {
|
||||
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child:
|
||||
isSubmitting
|
||||
? SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Text(localizations.submit),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'package:calibre_web_companion/l10n/app_localizations.dart';
|
||||
import 'package:calibre_web_companion/shared/utils/status_colors.dart';
|
||||
import 'package:calibre_web_companion/features/sync/data/models/sync_filter.dart';
|
||||
import 'package:calibre_web_companion/features/sync/bloc/sync_bloc.dart';
|
||||
import 'package:calibre_web_companion/features/sync/bloc/sync_event.dart';
|
||||
@@ -44,7 +45,7 @@ class _SyncSettingsWidgetState extends State<SyncSettingsWidget> {
|
||||
content: Text(
|
||||
state.errorMessage ?? localization.syncError,
|
||||
),
|
||||
backgroundColor: Colors.red,
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
);
|
||||
} else if (state.status == SyncStatus.completed) {
|
||||
@@ -59,7 +60,7 @@ class _SyncSettingsWidgetState extends State<SyncSettingsWidget> {
|
||||
content: Text(
|
||||
localization.syncFinishedWithXErrors(errorCount),
|
||||
),
|
||||
backgroundColor: Colors.red,
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
duration: const Duration(seconds: 5),
|
||||
),
|
||||
);
|
||||
@@ -319,7 +320,7 @@ class _SyncSettingsWidgetState extends State<SyncSettingsWidget> {
|
||||
switch (item.status) {
|
||||
case 'done':
|
||||
icon = Icons.check_circle;
|
||||
color = Colors.green;
|
||||
color = StatusColors.success(context);
|
||||
break;
|
||||
case 'downloading':
|
||||
icon = Icons.downloading;
|
||||
@@ -332,12 +333,12 @@ class _SyncSettingsWidgetState extends State<SyncSettingsWidget> {
|
||||
break;
|
||||
case 'error':
|
||||
icon = Icons.error_outline;
|
||||
color = Colors.red;
|
||||
color = StatusColors.error(context);
|
||||
break;
|
||||
case 'pending':
|
||||
default:
|
||||
icon = Icons.hourglass_empty;
|
||||
color = Colors.grey;
|
||||
color = StatusColors.neutral(context);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -53,10 +53,13 @@ class ShelfDetailsRemoteDataSource {
|
||||
endpoint: '/shelf/remove/$shelfId/$bookId',
|
||||
authMethod: AuthMethod.cookie,
|
||||
useCsrf: true,
|
||||
csrfTokenUrl: '/me',
|
||||
contentType: 'application/x-www-form-urlencoded',
|
||||
);
|
||||
|
||||
return response.statusCode == 204;
|
||||
return response.statusCode == 200 ||
|
||||
response.statusCode == 204 ||
|
||||
response.statusCode == 302;
|
||||
} catch (e) {
|
||||
logger.e('Error removing from shelf: $e');
|
||||
throw Exception('Failed to remove from shelf: $e');
|
||||
|
||||
@@ -15,6 +15,8 @@ import 'package:calibre_web_companion/features/shelf_view.dart/bloc/shelf_view_b
|
||||
import 'package:calibre_web_companion/features/shelf_view.dart/bloc/shelf_view_event.dart';
|
||||
|
||||
import 'package:calibre_web_companion/core/services/snackbar.dart';
|
||||
import 'package:calibre_web_companion/core/services/image_cache_manager.dart';
|
||||
import 'package:calibre_web_companion/shared/widgets/app_dialog_button.dart';
|
||||
import 'package:calibre_web_companion/main.dart';
|
||||
import 'package:calibre_web_companion/l10n/app_localizations.dart';
|
||||
import 'package:calibre_web_companion/features/shelf_details/data/models/shelf_book_item_model.dart';
|
||||
@@ -515,7 +517,7 @@ class ShelfDetailsPage extends StatelessWidget {
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
child: Text(localizations.cancel),
|
||||
),
|
||||
ElevatedButton(
|
||||
AppDialogButton.destructive(
|
||||
onPressed: () {
|
||||
Navigator.of(dialogContext).pop();
|
||||
context.read<ShelfDetailsBloc>().add(DeleteShelf(shelfId));
|
||||
@@ -526,11 +528,7 @@ class ShelfDetailsPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
foregroundColor: Theme.of(context).colorScheme.onError,
|
||||
),
|
||||
child: Text(localizations.delete),
|
||||
label: localizations.delete,
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -606,6 +604,8 @@ class ShelfDetailsPage extends StatelessWidget {
|
||||
builder: (context, snapshot) {
|
||||
final headers = snapshot.data ?? const <String, String>{};
|
||||
return CachedNetworkImage(
|
||||
cacheManager: CustomCacheManager(),
|
||||
key: ValueKey('${bookId}_$imageUrl'),
|
||||
imageUrl: imageUrl,
|
||||
httpHeaders: headers,
|
||||
fit: BoxFit.cover,
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:calibre_web_companion/l10n/app_localizations.dart';
|
||||
import 'package:calibre_web_companion/core/services/snackbar.dart';
|
||||
import 'package:calibre_web_companion/shared/widgets/app_dialog_button.dart';
|
||||
|
||||
class EditShelfDialog extends StatefulWidget {
|
||||
final String currentName;
|
||||
@@ -60,7 +61,7 @@ class _EditShelfDialogState extends State<EditShelfDialog> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SwitchListTile(
|
||||
title: const Text('Public'),
|
||||
title: Text(localizations.public),
|
||||
value: _isPublic,
|
||||
onChanged:
|
||||
_isEditing
|
||||
@@ -80,24 +81,11 @@ class _EditShelfDialogState extends State<EditShelfDialog> {
|
||||
onPressed: _isEditing ? null : () => Navigator.of(context).pop(),
|
||||
child: Text(localizations.cancel),
|
||||
),
|
||||
ElevatedButton(
|
||||
AppDialogButton(
|
||||
onPressed: _isEditing ? null : _editShelf,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (_isEditing)
|
||||
SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
if (_isEditing) const SizedBox(width: 8),
|
||||
Text(_isEditing ? localizations.editing : localizations.edit),
|
||||
],
|
||||
),
|
||||
isLoading: _isEditing,
|
||||
loadingLabel: localizations.editing,
|
||||
label: localizations.edit,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -87,9 +87,13 @@ class ShelfViewRemoteDataSource {
|
||||
endpoint: '/shelf/remove/$shelfId/$bookId',
|
||||
authMethod: AuthMethod.cookie,
|
||||
useCsrf: true,
|
||||
csrfTokenUrl: '/me',
|
||||
contentType: 'application/x-www-form-urlencoded',
|
||||
);
|
||||
|
||||
if (response.statusCode != 204) {
|
||||
if (response.statusCode != 200 &&
|
||||
response.statusCode != 204 &&
|
||||
response.statusCode != 302) {
|
||||
logger.e('Failed to remove book from shelf: ${response.body}');
|
||||
throw Exception('Failed to remove book from shelf: ${response.body}');
|
||||
}
|
||||
@@ -108,9 +112,13 @@ class ShelfViewRemoteDataSource {
|
||||
endpoint: '/shelf/add/$shelfId/$bookId',
|
||||
authMethod: AuthMethod.cookie,
|
||||
useCsrf: true,
|
||||
csrfTokenUrl: '/me',
|
||||
contentType: 'application/x-www-form-urlencoded',
|
||||
);
|
||||
|
||||
if (response.statusCode != 204) {
|
||||
if (response.statusCode != 200 &&
|
||||
response.statusCode != 204 &&
|
||||
response.statusCode != 302) {
|
||||
logger.e('Failed to add book to shelf: ${response.body}');
|
||||
throw Exception('Failed to add book to shelf: ${response.body}');
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:calibre_web_companion/l10n/app_localizations.dart';
|
||||
import 'package:calibre_web_companion/core/services/snackbar.dart';
|
||||
import 'package:calibre_web_companion/shared/widgets/app_dialog_button.dart';
|
||||
|
||||
class CreateShelfDialog extends StatefulWidget {
|
||||
final Function(String, bool) onCreateShelf;
|
||||
@@ -46,7 +47,7 @@ class _CreateShelfDialogState extends State<CreateShelfDialog> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SwitchListTile(
|
||||
title: const Text('Public'),
|
||||
title: Text(localizations.public),
|
||||
value: _isPublic,
|
||||
onChanged:
|
||||
_isCreating
|
||||
@@ -66,24 +67,11 @@ class _CreateShelfDialogState extends State<CreateShelfDialog> {
|
||||
onPressed: _isCreating ? null : () => Navigator.of(context).pop(),
|
||||
child: Text(localizations.cancel),
|
||||
),
|
||||
ElevatedButton(
|
||||
AppDialogButton(
|
||||
onPressed: _isCreating ? null : _createShelf,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (_isCreating)
|
||||
SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
if (_isCreating) const SizedBox(width: 8),
|
||||
Text(_isCreating ? localizations.creating : localizations.create),
|
||||
],
|
||||
),
|
||||
isLoading: _isCreating,
|
||||
loadingLabel: localizations.creating,
|
||||
label: localizations.create,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -94,6 +94,10 @@
|
||||
"libraryStatistics": "Bibliotheksstatistiken",
|
||||
"cancel": "Abbrechen",
|
||||
"close": "Schließen",
|
||||
"public": "Öffentlich",
|
||||
"apply": "Übernehmen",
|
||||
"comingSoon": "Demnächst!",
|
||||
"selectMetadataToImport": "Metadaten zum Importieren auswählen",
|
||||
"retry": "Wiederholen",
|
||||
"tryAgain": "Erneut versuchen",
|
||||
"error": "Fehler",
|
||||
|
||||
@@ -94,6 +94,10 @@
|
||||
"libraryStatistics": "Library statistics",
|
||||
"cancel": "Cancel",
|
||||
"close": "Close",
|
||||
"public": "Public",
|
||||
"apply": "Apply",
|
||||
"comingSoon": "Coming Soon!",
|
||||
"selectMetadataToImport": "Select Metadata to Import",
|
||||
"retry": "Retry",
|
||||
"tryAgain": "Try again",
|
||||
"error": "Error",
|
||||
|
||||
@@ -8,6 +8,8 @@ import 'package:get_it/get_it.dart';
|
||||
import 'package:logger/web.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
|
||||
import 'package:cosmos_epub/cosmos_epub.dart';
|
||||
|
||||
import 'package:calibre_web_companion/l10n/app_localizations.dart';
|
||||
import 'package:calibre_web_companion/core/di/injection_container.dart' as di;
|
||||
import 'package:calibre_web_companion/core/services/download_manager.dart'; // Import
|
||||
@@ -61,6 +63,9 @@ void main() async {
|
||||
|
||||
await di.getIt<DownloadManager>().initialize();
|
||||
|
||||
// Initialize the in-app EPUB reader (cosmos_epub) once before it is used.
|
||||
await CosmosEpub.initialize();
|
||||
|
||||
final savedThemeMode = await AdaptiveTheme.getThemeMode();
|
||||
|
||||
runZonedGuarded(
|
||||
@@ -192,11 +197,19 @@ class _MyAppState extends State<MyApp> {
|
||||
final lightTheme = ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: lightScheme,
|
||||
cardTheme: CardThemeData(
|
||||
color: lightScheme.surfaceContainerHigh,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
),
|
||||
);
|
||||
|
||||
final darkTheme = ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: darkScheme,
|
||||
cardTheme: CardThemeData(
|
||||
color: darkScheme.surfaceContainerHigh,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
),
|
||||
);
|
||||
|
||||
return SkeletonizerConfig(
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class StatusColors {
|
||||
const StatusColors._();
|
||||
|
||||
/// Success / completed (green).
|
||||
static Color success(BuildContext context) => _shade(context, Colors.green);
|
||||
|
||||
/// In-progress / warning (amber).
|
||||
static Color warning(BuildContext context) => _shade(context, Colors.amber);
|
||||
|
||||
/// Informational / available (blue).
|
||||
static Color info(BuildContext context) => _shade(context, Colors.blue);
|
||||
|
||||
/// Queued / waiting (purple).
|
||||
static Color pending(BuildContext context) => _shade(context, Colors.purple);
|
||||
|
||||
/// Error / failed
|
||||
static Color error(BuildContext context) =>
|
||||
Theme.of(context).colorScheme.error;
|
||||
|
||||
/// Neutral / inactive
|
||||
static Color neutral(BuildContext context) =>
|
||||
Theme.of(context).colorScheme.onSurfaceVariant;
|
||||
|
||||
static Color _shade(BuildContext context, MaterialColor base) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
return isDark ? base.shade300 : base.shade700;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppDialogButton extends StatelessWidget {
|
||||
final String label;
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
final bool isLoading;
|
||||
final String? loadingLabel;
|
||||
|
||||
final IconData? icon;
|
||||
|
||||
final bool isDestructive;
|
||||
|
||||
const AppDialogButton({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.onPressed,
|
||||
this.isLoading = false,
|
||||
this.loadingLabel,
|
||||
this.icon,
|
||||
this.isDestructive = false,
|
||||
});
|
||||
|
||||
const AppDialogButton.destructive({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.onPressed,
|
||||
this.isLoading = false,
|
||||
this.loadingLabel,
|
||||
this.icon,
|
||||
}) : isDestructive = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
|
||||
final Color background =
|
||||
isDestructive ? scheme.errorContainer : scheme.primaryContainer;
|
||||
final Color foreground =
|
||||
isDestructive ? scheme.onErrorContainer : scheme.onPrimaryContainer;
|
||||
|
||||
final style = FilledButton.styleFrom(
|
||||
backgroundColor: background,
|
||||
foregroundColor: foreground,
|
||||
disabledBackgroundColor: background.withValues(alpha: 0.5),
|
||||
disabledForegroundColor: foreground.withValues(alpha: 0.5),
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return FilledButton(
|
||||
onPressed: null,
|
||||
style: style,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: foreground,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(loadingLabel ?? label),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (icon != null) {
|
||||
return FilledButton.icon(
|
||||
onPressed: onPressed,
|
||||
style: style,
|
||||
icon: Icon(icon),
|
||||
label: Text(label),
|
||||
);
|
||||
}
|
||||
|
||||
return FilledButton(onPressed: onPressed, style: style, child: Text(label));
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:calibre_web_companion/l10n/app_localizations.dart';
|
||||
import 'package:calibre_web_companion/shared/widgets/app_dialog_button.dart';
|
||||
|
||||
Future<void> showComingSoonDialog(
|
||||
BuildContext context,
|
||||
String contentText,
|
||||
) async {
|
||||
final localizations = AppLocalizations.of(context)!;
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return AlertDialog(
|
||||
title: const Text('Coming Soon!'),
|
||||
title: Text(localizations.comingSoon),
|
||||
content: Text(contentText),
|
||||
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: const Text('OK'),
|
||||
AppDialogButton(
|
||||
onPressed: () {
|
||||
Navigator.of(dialogContext).pop();
|
||||
},
|
||||
label: localizations.ok,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [2020] [vocsy]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,186 +0,0 @@
|
||||
# Vocsy Epub Viewer [](https://pub.dartlang.org/packages/vocsy_epub_viewer)
|
||||
|
||||
originally a fork of [epub_kitty](https://github.com/451518849/epub_kitty) with few more features. i
|
||||
made this out of epub_kitty because the author was inactive(he isn't merging PRs or attending to
|
||||
issues) and i started having alot of issues with the plugin
|
||||
|
||||
vocsy_epub_viewer is an epub ebook reader that encapsulates
|
||||
the [folioreader](https://folioreader.github.io/FolioReaderKit/) framework. It supports iOS and
|
||||
android.
|
||||
|
||||
## Features
|
||||
|
||||
| Name | Android | iOS |
|
||||
|------|-------|------|
|
||||
| Reading Time Left / Pages left | ✅ | ✅ |
|
||||
| Last Read Locator | ✅ | ✅ |
|
||||
| Distraction Free Reading | ✅ | ❌ |
|
||||
| Load E-Pub from Asset | ✅ | ✅ |
|
||||
| Copy and Share Text | ✅ | ✅ |
|
||||
| Highlight Text | ✅ | ✅ |
|
||||
| Multiple Theme [Light / Dark] | ✅ | ❌ |
|
||||
| Support Multiple Device Language | ✅ | ✅ |
|
||||
| Change FontStyle | ✅ | ❌ |
|
||||
| Android 13 Supported | ✅ | ❌ |
|
||||
|
||||
## ScreenShots
|
||||
|
||||
+ Light
|
||||
<a href="#screenshots">
|
||||
<img src="https://raw.githubusercontent.com/kaushikgodhani/vocsy_epub_viewer/main/screenshots/S1.jpg" width="200px">
|
||||
</a>
|
||||
<a href="#screenshots">
|
||||
<img src="https://raw.githubusercontent.com/kaushikgodhani/vocsy_epub_viewer/main/screenshots/S3.jpg" width="200px">
|
||||
</a>
|
||||
<a href="#screenshots">
|
||||
<img src="https://raw.githubusercontent.com/kaushikgodhani/vocsy_epub_viewer/main/screenshots/S11.jpg" width="200px">
|
||||
</a>
|
||||
<a href="#screenshots">
|
||||
<img src="https://raw.githubusercontent.com/kaushikgodhani/vocsy_epub_viewer/main/screenshots/S4.jpg" width="200px">
|
||||
</a>
|
||||
<a href="#screenshots">
|
||||
<img src="https://raw.githubusercontent.com/kaushikgodhani/vocsy_epub_viewer/main/screenshots/S5.jpg" width="200px">
|
||||
</a>
|
||||
<a href="#screenshots">
|
||||
<img src="https://raw.githubusercontent.com/kaushikgodhani/vocsy_epub_viewer/main/screenshots/S6.jpg" width="200px">
|
||||
</a>
|
||||
|
||||
+ Dark
|
||||
|
||||
|
||||
<a href="#screenshots">
|
||||
<img src="https://raw.githubusercontent.com/kaushikgodhani/vocsy_epub_viewer/main/screenshots/S2.jpg" width="200px">
|
||||
</a>
|
||||
<a href="#screenshots">
|
||||
<img src="https://raw.githubusercontent.com/kaushikgodhani/vocsy_epub_viewer/main/screenshots/S7.jpg" width="200px">
|
||||
</a>
|
||||
<a href="#screenshots">
|
||||
<img src="https://raw.githubusercontent.com/kaushikgodhani/vocsy_epub_viewer/main/screenshots/S8.jpg" width="200px">
|
||||
</a>
|
||||
<a href="#screenshots">
|
||||
<img src="https://raw.githubusercontent.com/kaushikgodhani/vocsy_epub_viewer/main/screenshots/S9.jpg" width="200px">
|
||||
</a>
|
||||
<a href="#screenshots">
|
||||
<img src="https://raw.githubusercontent.com/kaushikgodhani/vocsy_epub_viewer/main/screenshots/S10.jpg" width="200px">
|
||||
</a>
|
||||
|
||||
## Install
|
||||
|
||||
This plugin requires `Swift` to work on iOS. Also, the minimum deployment target is 9.0
|
||||
|
||||
```
|
||||
platform :ios, '9.0'
|
||||
```
|
||||
|
||||
Import into pubspec.yaml
|
||||
|
||||
```
|
||||
dependencies:
|
||||
vocsy_epub_viewer: latest_version
|
||||
```
|
||||
|
||||
**NOTE** Please add this to the release build type in your app build.gradle to avoid crashes on android
|
||||
release builds
|
||||
|
||||
```
|
||||
minifyEnabled false
|
||||
shrinkResources false
|
||||
```
|
||||
|
||||
**NOTE** Add These Lines In manifest
|
||||
|
||||
<a href="#screenshots">
|
||||
<img src="https://raw.githubusercontent.com/kaushikgodhani/vocsy_epub_viewer/main/screenshots/img.png" >
|
||||
</a>
|
||||
|
||||
+ 1
|
||||
```java
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
```
|
||||
+ 2
|
||||
```java
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
```
|
||||
+ 3
|
||||
```java
|
||||
android:usesCleartextTraffic="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
```
|
||||
+ 4
|
||||
```java
|
||||
android:exported="true"
|
||||
```
|
||||
|
||||
**NOTE** `android` -> `app` -> `src` -> `main` -> `res` -> `xml` Inside xml Folder create xml file [network_security_config.xml](https://github.com/kaushikgodhani/vocsy_epub_viewer/tree/main/example/android/app/src/main/res/xml)
|
||||
## Usage
|
||||
|
||||
```dart
|
||||
VocsyEpub.setConfig(
|
||||
themeColor: Theme.of(context).primaryColor,
|
||||
identifier: "iosBook",
|
||||
scrollDirection: EpubScrollDirection.ALLDIRECTIONS,
|
||||
allowSharing: true,
|
||||
enableTts: true,
|
||||
nightMode: true,
|
||||
);
|
||||
|
||||
/**
|
||||
* @bookPath
|
||||
* @lastLocation (optional and only android)
|
||||
*/
|
||||
VocsyEpub.open(
|
||||
'bookPath',
|
||||
lastLocation: EpubLocator.fromJson({
|
||||
"bookId": "2239",
|
||||
"href": "/OEBPS/ch06.xhtml",
|
||||
"created": 1539934158390,
|
||||
"locations": {
|
||||
"cfi": "epubcfi(/0!/4/4[simple_book]/2/2/6)"
|
||||
}
|
||||
}), // first page will open up if the value is null
|
||||
);
|
||||
|
||||
// Get locator which you can save in your database
|
||||
|
||||
VocsyEpub.locatorStream.listen((locator) {
|
||||
print('LOCATOR: ${EpubLocator.fromJson(jsonDecode(locator))}');
|
||||
// convert locator from string to json and save to your database to be retrieved later
|
||||
});
|
||||
|
||||
```
|
||||
You can also load epub from your assets using `EpubViewer.openAsset()`
|
||||
|
||||
```dart
|
||||
await VocsyEpub.openAsset('assets/3.epub',
|
||||
lastLocation: EpubLocator.fromJson({
|
||||
"bookId": "2239",
|
||||
"href": "/OEBPS/ch06.xhtml",
|
||||
"created": 1539934158390,
|
||||
"locations": {
|
||||
"cfi": "epubcfi(/0!/4/4[simple_book]/2/2/6)"
|
||||
}
|
||||
}), // first page will open up if the value is null
|
||||
);
|
||||
|
||||
// Get locator which you can save in your database
|
||||
|
||||
VocsyEpub.locatorStream.listen((locator) {
|
||||
print('LOCATOR: ${EpubLocator.fromJson(jsonDecode(locator))}');
|
||||
// convert locator from string to json and save to your database to be retrieved later
|
||||
});
|
||||
|
||||
```
|
||||
Check the [Example](https://github.com/kaushikgodhani/vocsy_epub_viewer/blob/main/example/lib/main.dart) for implementation
|
||||
|
||||
## Issues
|
||||
|
||||
If you encounter any problems feel free to open an issue. If you feel the library is missing a
|
||||
feature, please raise a ticket on Github and I'll look into it. Pull request are also welcome.
|
||||
|
||||
For help getting started with Flutter, view the online
|
||||
[documentation](https://flutter.io/).
|
||||
|
||||
For help on editing plugin code, view
|
||||
the [documentation](https://flutter.io/platform-plugins/#edit-code).
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/workspace.xml
|
||||
/.idea/libraries
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
@@ -1,47 +0,0 @@
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.8.22'
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.4.2'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
maven { url "https://jitpack.io" }
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
namespace 'com.vocsy.epub_viewer'
|
||||
compileSdkVersion 33
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
lintOptions {
|
||||
disable 'InvalidPackage'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.github.kaushikgodhani:vocsy_epub_viewer_android_folioreader:V4'
|
||||
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.9.8'
|
||||
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.7'
|
||||
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
implementation 'com.github.kittinunf.fuel:fuel-android:2.3.1'
|
||||
implementation 'com.github.kittinunf.fuel:fuel:2.3.1'
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
org.gradle.jvmargs=-Xmx1536M
|
||||
android.useAndroidX=true
|
||||
android.enableR8=true
|
||||
android.enableJetifier=true
|
||||
@@ -1,5 +0,0 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
|
||||
@@ -1 +0,0 @@
|
||||
rootProject.name = 'epub_viewer'
|
||||
@@ -1,4 +0,0 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.vocsy.epub_viewer">
|
||||
|
||||
</manifest>
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
[]
|
||||
-121
@@ -1,121 +0,0 @@
|
||||
package com.vocsy.epub_viewer;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin;
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
|
||||
import io.flutter.plugin.common.BinaryMessenger;
|
||||
import io.flutter.plugin.common.EventChannel;
|
||||
import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class EpubViewerPlugin implements FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
|
||||
|
||||
private Reader reader;
|
||||
private ReaderConfig config;
|
||||
private MethodChannel channel;
|
||||
private static Activity activity;
|
||||
private static Context context;
|
||||
private static BinaryMessenger messenger;
|
||||
private static EventChannel eventChannel;
|
||||
private static EventChannel.EventSink sink;
|
||||
private static final String channelName = "vocsy_epub_viewer";
|
||||
|
||||
@Override
|
||||
public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
|
||||
messenger = binding.getBinaryMessenger();
|
||||
context = binding.getApplicationContext();
|
||||
|
||||
// Event channel setup
|
||||
eventChannel = new EventChannel(messenger, "page");
|
||||
eventChannel.setStreamHandler(new EventChannel.StreamHandler() {
|
||||
@Override
|
||||
public void onListen(Object o, EventChannel.EventSink eventSink) {
|
||||
sink = eventSink;
|
||||
if (sink == null) {
|
||||
Log.i("empty", "Sink is empty");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel(Object o) {
|
||||
sink = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Method channel setup
|
||||
channel = new MethodChannel(messenger, channelName);
|
||||
channel.setMethodCallHandler(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
|
||||
channel.setMethodCallHandler(null);
|
||||
eventChannel.setStreamHandler(null);
|
||||
messenger = null;
|
||||
context = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
|
||||
activity = binding.getActivity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromActivityForConfigChanges() {}
|
||||
|
||||
@Override
|
||||
public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {
|
||||
activity = binding.getActivity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromActivity() {
|
||||
activity = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
|
||||
if (call.method.equals("setChannel")) {
|
||||
result.success(null);
|
||||
|
||||
} else if (call.method.equals("setConfig")) {
|
||||
Map<String, Object> arguments = (Map<String, Object>) call.arguments;
|
||||
String identifier = arguments.get("identifier").toString();
|
||||
String themeColor = arguments.get("themeColor").toString();
|
||||
String scrollDirection = arguments.get("scrollDirection").toString();
|
||||
Boolean nightMode = Boolean.parseBoolean(arguments.get("nightMode").toString());
|
||||
Boolean allowSharing = Boolean.parseBoolean(arguments.get("allowSharing").toString());
|
||||
Boolean enableTts = Boolean.parseBoolean(arguments.get("enableTts").toString());
|
||||
config = new ReaderConfig(context, identifier, themeColor, scrollDirection, allowSharing, enableTts, nightMode);
|
||||
|
||||
result.success(null);
|
||||
|
||||
} else if (call.method.equals("open")) {
|
||||
Map<String, Object> arguments = (Map<String, Object>) call.arguments;
|
||||
String bookPath = arguments.get("bookPath").toString();
|
||||
String lastLocation = arguments.get("lastLocation").toString();
|
||||
|
||||
reader = new Reader(context, messenger, config, sink);
|
||||
reader.open(bookPath, lastLocation);
|
||||
|
||||
result.success(null);
|
||||
|
||||
} else if (call.method.equals("close")) {
|
||||
if (reader != null) {
|
||||
reader.close();
|
||||
}
|
||||
result.success(null);
|
||||
|
||||
} else {
|
||||
result.notImplemented();
|
||||
}
|
||||
}
|
||||
}
|
||||
-86
@@ -1,86 +0,0 @@
|
||||
package com.vocsy.epub_viewer;
|
||||
|
||||
import com.folioreader.model.HighLight;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Class contain data structure for highlight data. If user want to
|
||||
* provide external highlight data to folio activity. class should implement to
|
||||
* {@link HighLight} with contains required members.
|
||||
* <p>
|
||||
* Created by gautam chibde on 12/10/17.
|
||||
*/
|
||||
|
||||
public class HighlightData implements HighLight {
|
||||
|
||||
private String bookId;
|
||||
private String content;
|
||||
private Date date;
|
||||
private String type;
|
||||
private int pageNumber;
|
||||
private String pageId;
|
||||
private String rangy;
|
||||
private String uuid;
|
||||
private String note;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "HighlightData{" +
|
||||
"bookId='" + bookId + '\'' +
|
||||
", content='" + content + '\'' +
|
||||
", date=" + date +
|
||||
", type='" + type + '\'' +
|
||||
", pageNumber=" + pageNumber +
|
||||
", pageId='" + pageId + '\'' +
|
||||
", rangy='" + rangy + '\'' +
|
||||
", uuid='" + uuid + '\'' +
|
||||
", note='" + note + '\'' +
|
||||
'}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBookId() {
|
||||
return bookId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPageNumber() {
|
||||
return pageNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPageId() {
|
||||
return pageId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRangy() {
|
||||
return rangy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUUID() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNote() {
|
||||
return note;
|
||||
}
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
package com.vocsy.epub_viewer;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.folioreader.Config;
|
||||
import com.folioreader.FolioReader;
|
||||
import com.folioreader.model.HighLight;
|
||||
import com.folioreader.model.locators.ReadLocator;
|
||||
import com.folioreader.ui.base.OnSaveHighlight;
|
||||
import com.folioreader.util.OnHighlightListener;
|
||||
import com.folioreader.util.ReadLocatorListener;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.flutter.plugin.common.BinaryMessenger;
|
||||
import io.flutter.plugin.common.EventChannel;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
public class Reader implements OnHighlightListener, ReadLocatorListener, FolioReader.OnClosedListener {
|
||||
|
||||
private ReaderConfig readerConfig;
|
||||
public FolioReader folioReader;
|
||||
private Context context;
|
||||
public MethodChannel.Result result;
|
||||
private EventChannel eventChannel;
|
||||
private EventChannel.EventSink pageEventSink;
|
||||
private BinaryMessenger messenger;
|
||||
private ReadLocator read_locator;
|
||||
private static final String PAGE_CHANNEL = "sage";
|
||||
|
||||
Reader(Context context, BinaryMessenger messenger, ReaderConfig config, EventChannel.EventSink sink) {
|
||||
this.context = context;
|
||||
readerConfig = config;
|
||||
|
||||
getHighlightsAndSave();
|
||||
//setPageHandler(messenger);
|
||||
|
||||
folioReader = FolioReader.get()
|
||||
.setOnHighlightListener(this)
|
||||
.setReadLocatorListener(this)
|
||||
.setOnClosedListener(this);
|
||||
pageEventSink = sink;
|
||||
}
|
||||
|
||||
public void open(String bookPath, String lastLocation) {
|
||||
final String path = bookPath;
|
||||
final String location = lastLocation;
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
Log.i("SavedLocation", "-> savedLocation -> " + location);
|
||||
if (location != null && !location.isEmpty()) {
|
||||
ReadLocator readLocator = ReadLocator.fromJson(location);
|
||||
folioReader.setReadLocator(readLocator);
|
||||
}
|
||||
folioReader.setConfig(readerConfig.config, true)
|
||||
.openBook(path);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
|
||||
}
|
||||
|
||||
public void close() {
|
||||
folioReader.close();
|
||||
}
|
||||
|
||||
private void setPageHandler(BinaryMessenger messenger) {
|
||||
// final MethodChannel channel = new MethodChannel(registrar.messenger(), "page");
|
||||
// channel.setMethodCallHandler(new EpubKittyPlugin());
|
||||
Log.i("event sink is", "in set page handler:");
|
||||
eventChannel = new EventChannel(messenger, PAGE_CHANNEL);
|
||||
|
||||
try {
|
||||
|
||||
eventChannel.setStreamHandler(new EventChannel.StreamHandler() {
|
||||
|
||||
@Override
|
||||
public void onListen(Object o, EventChannel.EventSink eventSink) {
|
||||
|
||||
Log.i("event sink is", "this is eveent sink:");
|
||||
|
||||
pageEventSink = eventSink;
|
||||
if (pageEventSink == null) {
|
||||
Log.i("empty", "Sink is empty");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel(Object o) {
|
||||
|
||||
}
|
||||
});
|
||||
} catch (Error err) {
|
||||
Log.i("and error", "error is " + err.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private void getHighlightsAndSave() {
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ArrayList<HighLight> highlightList = null;
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
try {
|
||||
highlightList = objectMapper.readValue(
|
||||
loadAssetTextAsString("highlights/highlights_data.json"),
|
||||
new TypeReference<List<HighlightData>>() {
|
||||
});
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
if (highlightList == null) {
|
||||
folioReader.saveReceivedHighLights(highlightList, new OnSaveHighlight() {
|
||||
@Override
|
||||
public void onFinished() {
|
||||
//You can do anything on successful saving highlight list
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
|
||||
private String loadAssetTextAsString(String name) {
|
||||
BufferedReader in = null;
|
||||
try {
|
||||
StringBuilder buf = new StringBuilder();
|
||||
InputStream is = context.getAssets().open(name);
|
||||
in = new BufferedReader(new InputStreamReader(is));
|
||||
|
||||
String str;
|
||||
boolean isFirst = true;
|
||||
while ((str = in.readLine()) != null) {
|
||||
if (isFirst)
|
||||
isFirst = false;
|
||||
else
|
||||
buf.append('\n');
|
||||
buf.append(str);
|
||||
}
|
||||
return buf.toString();
|
||||
} catch (IOException e) {
|
||||
Log.e("Reader", "Error opening asset " + name);
|
||||
} finally {
|
||||
if (in != null) {
|
||||
try {
|
||||
in.close();
|
||||
} catch (IOException e) {
|
||||
Log.e("Reader", "Error closing asset " + name);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFolioReaderClosed() {
|
||||
Log.i("readLocator", "-> saveReadLocator -> " + read_locator.toJson());
|
||||
|
||||
if (pageEventSink != null) {
|
||||
pageEventSink.success(read_locator.toJson());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHighlight(HighLight highlight, HighLight.HighLightAction type) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveReadLocator(ReadLocator readLocator) {
|
||||
read_locator = readLocator;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
package com.vocsy.epub_viewer;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.util.Log;
|
||||
|
||||
import com.folioreader.Config;
|
||||
import com.folioreader.util.AppUtil;
|
||||
|
||||
public class ReaderConfig {
|
||||
private String identifier;
|
||||
private String themeColor;
|
||||
private String scrollDirection;
|
||||
private boolean allowSharing;
|
||||
private boolean showTts;
|
||||
private boolean nightMode;
|
||||
|
||||
public Config config;
|
||||
|
||||
public ReaderConfig(Context context, String identifier, String themeColor,
|
||||
String scrollDirection, boolean allowSharing, boolean showTts , boolean nightMode){
|
||||
|
||||
// config = AppUtil.getSavedConfig(context);
|
||||
// if (config == null)
|
||||
config = new Config();
|
||||
if (scrollDirection.equals("vertical")){
|
||||
config.setAllowedDirection(Config.AllowedDirection.ONLY_VERTICAL);
|
||||
}else if(scrollDirection.equals("horizontal")){
|
||||
config.setAllowedDirection(Config.AllowedDirection.ONLY_HORIZONTAL);
|
||||
}else{
|
||||
config.setAllowedDirection(Config.AllowedDirection.VERTICAL_AND_HORIZONTAL);
|
||||
}
|
||||
config.setThemeColorInt(Color.parseColor(themeColor));
|
||||
config.setNightThemeColorInt(Color.parseColor(themeColor));
|
||||
config.setShowRemainingIndicator(true);
|
||||
config.setShowTts(showTts);
|
||||
config.setNightMode(nightMode);
|
||||
}
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
//
|
||||
// EpubConfig.swift
|
||||
// AEXML
|
||||
//
|
||||
// Created by 小发工作室 on 2019/11/21.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import EpubViewerKit
|
||||
|
||||
class EpubConfig: NSObject {
|
||||
|
||||
open var config: FolioReaderConfig!
|
||||
|
||||
open var tintColor: UIColor = UIColor.init(rgba:"#fdd82c")
|
||||
open var allowSharing: Bool = false
|
||||
open var scrollDirection: FolioReaderScrollDirection = FolioReaderScrollDirection.vertical
|
||||
|
||||
init(Identifier: String,tintColor: String, allowSharing: Bool,
|
||||
scrollDirection: String, enableTts: Bool, nightMode: Bool) {
|
||||
self.config = FolioReaderConfig(withIdentifier: Identifier)
|
||||
self.tintColor = UIColor.init(rgba: tintColor)
|
||||
self.allowSharing = allowSharing
|
||||
self.config.canChangeScrollDirection = false
|
||||
if scrollDirection == "vertical"{
|
||||
self.config.scrollDirection = FolioReaderScrollDirection.vertical
|
||||
}else if (scrollDirection == "horizontal"){
|
||||
self.config.scrollDirection = FolioReaderScrollDirection.horizontal
|
||||
}else{
|
||||
self.config.canChangeScrollDirection = true
|
||||
}
|
||||
self.config.enableTTS = enableTts
|
||||
self.config.hidePageIndicator = false
|
||||
super.init()
|
||||
|
||||
self.readerConfiguration()
|
||||
}
|
||||
|
||||
private func readerConfiguration() {
|
||||
self.config.shouldHideNavigationOnTap = false
|
||||
self.config.scrollDirection = self.scrollDirection
|
||||
self.config.enableTTS = false
|
||||
self.config.displayTitle = true
|
||||
self.config.allowSharing = self.allowSharing
|
||||
self.config.tintColor = self.tintColor
|
||||
self.config.canChangeFontStyle = false
|
||||
self.config.hideBars = false
|
||||
// Custom sharing quote background
|
||||
self.config.quoteCustomBackgrounds = []
|
||||
if let image = UIImage(named: "demo-bg") {
|
||||
let customImageQuote = QuoteImage(withImage: image, alpha: 0.6, backgroundColor: UIColor.black)
|
||||
self.config.quoteCustomBackgrounds.append(customImageQuote)
|
||||
}
|
||||
|
||||
let textColor = UIColor(red:0.86, green:0.73, blue:0.70, alpha:1.0)
|
||||
let customColor = UIColor(red:0.30, green:0.26, blue:0.20, alpha:1.0)
|
||||
let customQuote = QuoteImage(withColor: customColor, alpha: 1.0, textColor: textColor)
|
||||
self.config.quoteCustomBackgrounds.append(customQuote)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
internal extension UIColor {
|
||||
convenience init(rgba: String) {
|
||||
var red: CGFloat = 0.0
|
||||
var green: CGFloat = 0.0
|
||||
var blue: CGFloat = 0.0
|
||||
var alpha: CGFloat = 1.0
|
||||
|
||||
if rgba.hasPrefix("#") {
|
||||
let index = rgba.index(rgba.startIndex, offsetBy: 1)
|
||||
let hex = String(rgba[index...])
|
||||
let scanner = Scanner(string: hex)
|
||||
var hexValue: CUnsignedLongLong = 0
|
||||
if scanner.scanHexInt64(&hexValue) {
|
||||
switch (hex.count) {
|
||||
case 3:
|
||||
red = CGFloat((hexValue & 0xF00) >> 8) / 15.0
|
||||
green = CGFloat((hexValue & 0x0F0) >> 4) / 15.0
|
||||
blue = CGFloat(hexValue & 0x00F) / 15.0
|
||||
break
|
||||
case 4:
|
||||
red = CGFloat((hexValue & 0xF000) >> 12) / 15.0
|
||||
green = CGFloat((hexValue & 0x0F00) >> 8) / 15.0
|
||||
blue = CGFloat((hexValue & 0x00F0) >> 4) / 15.0
|
||||
alpha = CGFloat(hexValue & 0x000F) / 15.0
|
||||
break
|
||||
case 6:
|
||||
red = CGFloat((hexValue & 0xFF0000) >> 16) / 255.0
|
||||
green = CGFloat((hexValue & 0x00FF00) >> 8) / 255.0
|
||||
blue = CGFloat(hexValue & 0x0000FF) / 255.0
|
||||
break
|
||||
case 8:
|
||||
red = CGFloat((hexValue & 0xFF000000) >> 24) / 255.0
|
||||
green = CGFloat((hexValue & 0x00FF0000) >> 16) / 255.0
|
||||
blue = CGFloat((hexValue & 0x0000FF00) >> 8) / 255.0
|
||||
alpha = CGFloat(hexValue & 0x000000FF) / 255.0
|
||||
break
|
||||
default:
|
||||
print("Invalid RGB string, number of characters after '#' should be either 3, 4, 6 or 8", terminator: "")
|
||||
break
|
||||
}
|
||||
} else {
|
||||
print("Scan hex error")
|
||||
}
|
||||
} else {
|
||||
print("Invalid RGB string, missing '#' as prefix", terminator: "")
|
||||
}
|
||||
self.init(red:red, green:green, blue:blue, alpha:alpha)
|
||||
}
|
||||
|
||||
//
|
||||
/// Hex string of a UIColor instance.
|
||||
///
|
||||
/// from: https://github.com/yeahdongcn/UIColor-Hex-Swift
|
||||
///
|
||||
/// - Parameter includeAlpha: Whether the alpha should be included.
|
||||
/// - Returns: Hexa string
|
||||
func hexString(_ includeAlpha: Bool) -> String {
|
||||
var r: CGFloat = 0
|
||||
var g: CGFloat = 0
|
||||
var b: CGFloat = 0
|
||||
var a: CGFloat = 0
|
||||
self.getRed(&r, green: &g, blue: &b, alpha: &a)
|
||||
|
||||
if (includeAlpha == true) {
|
||||
return String(format: "#%02X%02X%02X%02X", Int(r * 255), Int(g * 255), Int(b * 255), Int(a * 255))
|
||||
} else {
|
||||
return String(format: "#%02X%02X%02X", Int(r * 255), Int(g * 255), Int(b * 255))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - color shades
|
||||
// https://gist.github.com/mbigatti/c6be210a6bbc0ff25972
|
||||
|
||||
func highlightColor() -> UIColor {
|
||||
|
||||
var hue : CGFloat = 0
|
||||
var saturation : CGFloat = 0
|
||||
var brightness : CGFloat = 0
|
||||
var alpha : CGFloat = 0
|
||||
|
||||
if getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) {
|
||||
return UIColor(hue: hue, saturation: 0.30, brightness: 1, alpha: alpha)
|
||||
} else {
|
||||
return self;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
Returns a lighter color by the provided percentage
|
||||
|
||||
:param: lighting percent percentage
|
||||
:returns: lighter UIColor
|
||||
*/
|
||||
func lighterColor(_ percent : Double) -> UIColor {
|
||||
return colorWithBrightnessFactor(CGFloat(1 + percent));
|
||||
}
|
||||
|
||||
/**
|
||||
Returns a darker color by the provided percentage
|
||||
|
||||
:param: darking percent percentage
|
||||
:returns: darker UIColor
|
||||
*/
|
||||
func darkerColor(_ percent : Double) -> UIColor {
|
||||
return colorWithBrightnessFactor(CGFloat(1 - percent));
|
||||
}
|
||||
|
||||
/**
|
||||
Return a modified color using the brightness factor provided
|
||||
|
||||
:param: factor brightness factor
|
||||
:returns: modified color
|
||||
*/
|
||||
func colorWithBrightnessFactor(_ factor: CGFloat) -> UIColor {
|
||||
var hue : CGFloat = 0
|
||||
var saturation : CGFloat = 0
|
||||
var brightness : CGFloat = 0
|
||||
var alpha : CGFloat = 0
|
||||
|
||||
if getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) {
|
||||
return UIColor(hue: hue, saturation: saturation, brightness: brightness * factor, alpha: alpha)
|
||||
} else {
|
||||
return self;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
#import <Flutter/Flutter.h>
|
||||
|
||||
@interface EpubViewerPlugin : NSObject<FlutterPlugin>
|
||||
@end
|
||||
@@ -1,8 +0,0 @@
|
||||
#import "EpubViewerPlugin.h"
|
||||
#import <vocsy_epub_viewer/vocsy_epub_viewer-Swift.h>
|
||||
|
||||
@implementation EpubViewerPlugin
|
||||
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
|
||||
[SwiftEpubViewerPlugin registerWithRegistrar:registrar];
|
||||
}
|
||||
@end
|
||||
@@ -1,97 +0,0 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
import EpubViewerKit
|
||||
|
||||
public class SwiftEpubViewerPlugin: NSObject, FlutterPlugin,FolioReaderPageDelegate,FlutterStreamHandler {
|
||||
|
||||
let folioReader = FolioReader()
|
||||
static var pageResult: FlutterResult? = nil
|
||||
static var pageChannel:FlutterEventChannel? = nil
|
||||
|
||||
var config: EpubConfig?
|
||||
|
||||
|
||||
//12.13
|
||||
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||
let channel = FlutterMethodChannel(name: "vocsy_epub_viewer", binaryMessenger: registrar.messenger())
|
||||
let instance = SwiftEpubViewerPlugin()
|
||||
|
||||
pageChannel = FlutterEventChannel.init(name: "page",
|
||||
binaryMessenger: registrar.messenger());
|
||||
|
||||
registrar.addMethodCallDelegate(instance, channel: channel)
|
||||
}
|
||||
|
||||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
|
||||
|
||||
switch call.method {
|
||||
case "setConfig":
|
||||
let arguments = call.arguments as![String:Any]
|
||||
let Identifier = arguments["identifier"] as! String
|
||||
let scrollDirection = arguments["scrollDirection"] as! String
|
||||
let color = arguments["themeColor"] as! String
|
||||
let allowSharing = arguments["allowSharing"] as! Bool
|
||||
let enableTts = arguments["enableTts"] as! Bool
|
||||
let nightMode = arguments["nightMode"] as! Bool
|
||||
|
||||
self.config = EpubConfig.init(Identifier: Identifier,tintColor: color,allowSharing:
|
||||
allowSharing,scrollDirection: scrollDirection, enableTts: enableTts, nightMode: nightMode)
|
||||
|
||||
break
|
||||
case "open":
|
||||
setPageHandler()
|
||||
let arguments = call.arguments as![String:Any]
|
||||
let bookPath = arguments["bookPath"] as! String
|
||||
self.open(epubPath: bookPath)
|
||||
|
||||
break
|
||||
case "close":
|
||||
self.close()
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
// result("iOS " + UIDevice.current.systemVersion)
|
||||
}
|
||||
|
||||
private func setPageHandler(){
|
||||
SwiftEpubViewerPlugin.pageChannel?.setStreamHandler(self)
|
||||
|
||||
}
|
||||
|
||||
public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
|
||||
SwiftEpubViewerPlugin.pageResult = events
|
||||
return nil
|
||||
}
|
||||
|
||||
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
fileprivate func open(epubPath: String) {
|
||||
if epubPath == "" {
|
||||
return
|
||||
}
|
||||
|
||||
let readerVc = UIApplication.shared.keyWindow!.rootViewController ?? UIViewController()
|
||||
folioReader.presentReader(parentViewController: readerVc, withEpubPath: epubPath, andConfig: self.config!.config, shouldRemoveEpub: false)
|
||||
folioReader.readerCenter?.pageDelegate = self
|
||||
}
|
||||
|
||||
public func pageWillLoad(_ page: FolioReaderPage) {
|
||||
|
||||
print("page.pageNumber:"+String(page.pageNumber))
|
||||
|
||||
if (SwiftEpubViewerPlugin.pageResult != nil){
|
||||
SwiftEpubViewerPlugin.pageResult!(String(page.pageNumber))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func close(){
|
||||
folioReader.readerContainer?.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'vocsy_epub_viewer'
|
||||
s.version = '2.0.0'
|
||||
s.summary = 'A Vocsy epub reader flutter plugin project.'
|
||||
s.description = <<-DESC
|
||||
A new flutter plugin project.
|
||||
DESC
|
||||
s.homepage = 'https://github.com/kaushikgodhani/vocsy_epub_viewer.git'
|
||||
s.license = { :file => '../LICENSE' }
|
||||
s.author = { 'dudecoder' => 'kaushik64494@gmail.com' }
|
||||
s.source = { :path => '.' }
|
||||
|
||||
|
||||
s.source_files = [
|
||||
'Classes/**/*',
|
||||
]
|
||||
|
||||
s.dependency 'Flutter'
|
||||
|
||||
s.dependency 'EpubViewerKit', '~> 0.1.3'
|
||||
s.ios.deployment_target = '9.0'
|
||||
|
||||
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
|
||||
s.swift_version = '5.0'
|
||||
end
|
||||
@@ -1,85 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
part 'model/enum/epub_scroll_direction.dart';
|
||||
part 'model/epub_locator.dart';
|
||||
part 'utils/util.dart';
|
||||
|
||||
class VocsyEpub {
|
||||
static const MethodChannel _channel = MethodChannel('vocsy_epub_viewer');
|
||||
static const EventChannel _pageChannel = EventChannel('page');
|
||||
|
||||
/// Configure Viewer's with available values
|
||||
///
|
||||
/// themeColor is the color of the reader
|
||||
/// scrollDirection uses the [EpubScrollDirection] enum
|
||||
/// allowSharing
|
||||
/// enableTts is an option to enable the inbuilt Text-to-Speech
|
||||
static void setConfig(
|
||||
{Color themeColor = Colors.blue,
|
||||
String identifier = 'book',
|
||||
bool nightMode = false,
|
||||
EpubScrollDirection scrollDirection = EpubScrollDirection.ALLDIRECTIONS,
|
||||
bool allowSharing = false,
|
||||
bool enableTts = false}) async {
|
||||
Map<String, dynamic> agrs = {
|
||||
"identifier": identifier,
|
||||
"themeColor": Util.getHexFromColor(themeColor),
|
||||
"scrollDirection": Util.getDirection(scrollDirection),
|
||||
"allowSharing": allowSharing,
|
||||
'enableTts': enableTts,
|
||||
'nightMode': nightMode
|
||||
};
|
||||
await _channel.invokeMethod('setConfig', agrs);
|
||||
}
|
||||
|
||||
/// bookPath should be a local file.
|
||||
/// Last location is only available for android.
|
||||
static void open(String bookPath, {EpubLocator? lastLocation}) async {
|
||||
Map<String, dynamic> agrs = {
|
||||
"bookPath": bookPath,
|
||||
'lastLocation':
|
||||
lastLocation == null ? '' : jsonEncode(lastLocation.toJson()),
|
||||
};
|
||||
_channel.invokeMethod('setChannel');
|
||||
await _channel.invokeMethod('open', agrs);
|
||||
}
|
||||
|
||||
static void closeReader() async {
|
||||
_channel.invokeMethod('setChannel');
|
||||
await _channel.invokeMethod('close');
|
||||
}
|
||||
|
||||
/// bookPath should be an asset file path.
|
||||
/// Last location is only available for android.
|
||||
static Future openAsset(String bookPath, {EpubLocator? lastLocation}) async {
|
||||
if (extension(bookPath) == '.epub') {
|
||||
Map<String, dynamic> agrs = {
|
||||
"bookPath": (await Util.getFileFromAsset(bookPath)).path,
|
||||
'lastLocation':
|
||||
lastLocation == null ? '' : jsonEncode(lastLocation.toJson()),
|
||||
};
|
||||
_channel.invokeMethod('setChannel');
|
||||
await _channel.invokeMethod('open', agrs);
|
||||
} else {
|
||||
throw ('${extension(bookPath)} cannot be opened, use an EPUB File');
|
||||
}
|
||||
}
|
||||
|
||||
static Future setChannel() async {
|
||||
await _channel.invokeMethod('setChannel');
|
||||
}
|
||||
|
||||
/// Stream to get EpubLocator for android and pageNumber for iOS
|
||||
static Stream get locatorStream {
|
||||
Stream pageStream =
|
||||
_pageChannel.receiveBroadcastStream().map((value) => value);
|
||||
|
||||
return pageStream;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
part of 'package:vocsy_epub_viewer/epub_viewer.dart';
|
||||
|
||||
/// enum from scrollDirection to make it easier for users
|
||||
enum EpubScrollDirection { HORIZONTAL, VERTICAL, ALLDIRECTIONS }
|
||||
@@ -1,48 +0,0 @@
|
||||
part of 'package:vocsy_epub_viewer/epub_viewer.dart';
|
||||
|
||||
/// Model for android EpubLocator
|
||||
class EpubLocator {
|
||||
String? bookId;
|
||||
String? href;
|
||||
int? created;
|
||||
Locations? locations;
|
||||
|
||||
EpubLocator({this.bookId, this.href, this.created, this.locations});
|
||||
|
||||
EpubLocator.fromJson(Map<String, dynamic> json) {
|
||||
bookId = json['bookId'];
|
||||
href = json['href'];
|
||||
created = json['created'];
|
||||
locations = json['locations'] != null
|
||||
? Locations.fromJson(json['locations'])
|
||||
: null;
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['bookId'] = bookId;
|
||||
data['href'] = href;
|
||||
data['created'] = created;
|
||||
if (locations != null) {
|
||||
data['locations'] = locations!.toJson();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
/// Model for Locations in [EpubLocator]
|
||||
class Locations {
|
||||
String? cfi;
|
||||
|
||||
Locations({this.cfi});
|
||||
|
||||
Locations.fromJson(Map<String, dynamic> json) {
|
||||
cfi = json['cfi'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['cfi'] = cfi;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
part of 'package:vocsy_epub_viewer/epub_viewer.dart';
|
||||
|
||||
class Util {
|
||||
/// Get HEX code from [Colors], [MaterialColor],
|
||||
/// [Color] and [MaterialAccentColor]
|
||||
static String getHexFromColor(Color color) {
|
||||
return '#${color.value.toRadixString(16).padLeft(8, '0').substring(2)}';
|
||||
}
|
||||
|
||||
/// Convert [EpubScrollDirection] to FolioReader reader String
|
||||
static String getDirection(EpubScrollDirection direction) {
|
||||
switch (direction) {
|
||||
case EpubScrollDirection.VERTICAL:
|
||||
return 'vertical';
|
||||
case EpubScrollDirection.HORIZONTAL:
|
||||
return 'horizontal';
|
||||
case EpubScrollDirection.ALLDIRECTIONS:
|
||||
return 'alldirections';
|
||||
default:
|
||||
return 'alldirections';
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a temporary [File] from an asset epub
|
||||
/// to be opened by [VocsyEpub]
|
||||
static Future<File> getFileFromAsset(String asset) async {
|
||||
ByteData data = await rootBundle.load(asset);
|
||||
String dir = (await getTemporaryDirectory()).path;
|
||||
String path = '$dir/${basename(asset)}';
|
||||
final buffer = data.buffer;
|
||||
return File(path).writeAsBytes(
|
||||
buffer.asUint8List(data.offsetInBytes, data.lengthInBytes));
|
||||
}
|
||||
}
|
||||
@@ -1,293 +0,0 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.11.0"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: "9fd2163d866769f60f4df8ac1dc59f52498d810c356fe78022e383dd3c57c0e1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.0.2"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.10"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.17"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.11.1"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
path:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.15"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: "1dab723dd8feeb80afb39c7be894f09df1457243d930010f6f328fb8c660c5e1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.21"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "1995d88ec2948dac43edf8fe58eb434d35d22a2940ecee1a9fefcd62beee6eb3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.3"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
sha256: ab0987bf95bc591da42dffb38c77398fc43309f0b9b894dcc5d6f40c4b26c379
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.7"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: c2af5a8a6369992d915f8933dfc23172071001359d17896e83db8be57db8a397
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: bcabbe399d4042b8ee687e17548d5d3f527255253b4a639f5f8d2094a9c2b45c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: c2c49e16d42fd6983eb55e44b7f197fdf16b4da7aab7f8e1d21da307cad3fb02
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
process:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: process
|
||||
sha256: dc3c073b5bc0db4e0f3dbc6b69f8e9cf2f336dafb3db996242ebdacf94c295dd
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.1"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
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: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.7"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "13.0.0"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: d13ac5deea7327f027b3b97ee19ee210f68256ecf3f1a304bcfb992ee947637c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: "0186b3f2d66be9a12b0295bddcf8b6f8c0b0cc2f85c6287344e2a6366bc28457"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
sdks:
|
||||
dart: ">=3.8.0-0 <4.0.0"
|
||||
flutter: ">=3.18.0-18.0.pre.54"
|
||||
@@ -1,66 +0,0 @@
|
||||
name: vocsy_epub_viewer
|
||||
description: 'vocsy_epub_viewer is an epub ebook reader that encapsulates the folioreader framework.'
|
||||
version: 3.0.0
|
||||
homepage: https://github.com/kaushikgodhani/vocsy_epub_viewer
|
||||
|
||||
environment:
|
||||
sdk: '>=2.12.0 <4.0.0'
|
||||
flutter: '>=1.17.0'
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
path_provider:
|
||||
path:
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
# The following section is specific to Flutter.
|
||||
flutter:
|
||||
# This section identifies this Flutter project as a plugin project.
|
||||
# The androidPackage and pluginClass identifiers should not ordinarily
|
||||
# be modified. They are used by the tooling to maintain consistency when
|
||||
# adding or updating assets for this project.
|
||||
plugin:
|
||||
platforms:
|
||||
android:
|
||||
package: com.vocsy.epub_viewer
|
||||
pluginClass: EpubViewerPlugin
|
||||
ios:
|
||||
pluginClass: EpubViewerPlugin
|
||||
|
||||
# To add assets to your plugin package, add an assets section, like this:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
#
|
||||
# For details regarding assets in packages, see
|
||||
# https://flutter.dev/assets-and-images/#from-packages
|
||||
#
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/assets-and-images/#resolution-aware.
|
||||
|
||||
# To add custom fonts to your plugin package, add a fonts section here,
|
||||
# in this "flutter" section. Each entry in this list should have a
|
||||
# "family" key with the font family name, and a "fonts" key with a
|
||||
# list giving the asset and other descriptors for the font. For
|
||||
# example:
|
||||
# fonts:
|
||||
# - family: Schyler
|
||||
# fonts:
|
||||
# - asset: fonts/Schyler-Regular.ttf
|
||||
# - asset: fonts/Schyler-Italic.ttf
|
||||
# style: italic
|
||||
# - family: Trajan Pro
|
||||
# fonts:
|
||||
# - asset: fonts/TrajanPro.ttf
|
||||
# - asset: fonts/TrajanPro_Bold.ttf
|
||||
# weight: 700
|
||||
#
|
||||
# For details regarding fonts in packages, see
|
||||
# https://flutter.dev/custom-fonts/#from-packages
|
||||
+171
-10
@@ -121,6 +121,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
cosmos_epub:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cosmos_epub
|
||||
sha256: "68603def7311cf2c79335aed7c0a2a2db172a36b60c7f3a44928ca3d0f499f0b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0+1"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -193,6 +201,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.8.1"
|
||||
epubx:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: epubx
|
||||
sha256: "0ab9354efa177c4be52c46f857bc15bf83f83a92667fb673465c8f89fca26db3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
equatable:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -201,6 +217,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.7"
|
||||
fading_edge_scrollview:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fading_edge_scrollview
|
||||
sha256: "1f84fe3ea8e251d00d5735e27502a6a250e4aa3d3b330d3fdcb475af741464ef"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.1"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -395,6 +419,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
flutter_screenutil:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_screenutil
|
||||
sha256: "8239210dd68bee6b0577aa4a090890342d04a136ce1c81f98ee513fc0ce891de"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.9.3"
|
||||
flutter_svg:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_svg
|
||||
sha256: "35882981abcbfb8c15b286f0cd690ff25bac12d95eff3e25ee207f37d4c42e7f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@@ -409,10 +449,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: fluttertoast
|
||||
sha256: "144ddd74d49c865eba47abe31cbc746c7b311c82d6c32e571fd73c4264b740e2"
|
||||
sha256: "90778fe0497fe3a09166e8cf2e0867310ff434b794526589e77ec03cf08ba8e8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.0.0"
|
||||
version: "8.2.14"
|
||||
get:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: get
|
||||
sha256: "5ed34a7925b85336e15d472cc4cfe7d9ebf4ab8e8b9f688585bf6b50f4c3d79a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.7.3"
|
||||
get_it:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -421,6 +469,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.2.0"
|
||||
get_storage:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: get_storage
|
||||
sha256: "39db1fffe779d0c22b3a744376e86febe4ade43bf65e06eab5af707dc84185a2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
html:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -533,6 +589,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.20.2"
|
||||
isar_community:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: isar_community
|
||||
sha256: "683fd093956eac3a660776afb70d65d50a3391b3587b40b89e3d12586b8cc47f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.2"
|
||||
isar_community_flutter_libs:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: isar_community_flutter_libs
|
||||
sha256: c44340fa38c81ef16d924202d443bbe799cde4826be9a31a9dc92ee612e1966f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.2"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.2"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -717,6 +797,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_parsing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_parsing
|
||||
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
path_provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -797,6 +885,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.5+1"
|
||||
quiver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: quiver
|
||||
sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
rxdart:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -805,6 +901,54 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.28.0"
|
||||
screen_brightness:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: screen_brightness
|
||||
sha256: "7d4ac84ae26b37c01d6f5db7123a72db7933e1f2a2a8c369a51e08f81b3178d8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
screen_brightness_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: screen_brightness_android
|
||||
sha256: "8c69d3ac475e4d625e7fa682a3a51a69ff59abe5b4a9e57f6ec7d830a6c69bd6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
screen_brightness_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: screen_brightness_ios
|
||||
sha256: f08f70ca1ac3e30719764b5cfb8b3fe1e28163065018a41b3e6f243ab146c2f1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
screen_brightness_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: screen_brightness_macos
|
||||
sha256: "70c2efa4534e22b927e82693488f127dd4a0f008469fccf4f0eefe9061bbdd6a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
screen_brightness_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: screen_brightness_platform_interface
|
||||
sha256: "9f3ebf7f22d5487e7676fe9ddaf3fc55b6ff8057707cf6dc0121c7dfda346a16"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
screen_brightness_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: screen_brightness_windows
|
||||
sha256: c8e12a91cf6dd912a48bd41fcf749282a51afa17f536c3460d8d05702fb89ffa
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1050,6 +1194,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.2"
|
||||
vector_graphics:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics
|
||||
sha256: "2306c03da2ba81724afeb589c351ebbc0aa7d86005925be8f8735856dbe5e42d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
vector_graphics_codec:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics_codec
|
||||
sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.13"
|
||||
vector_graphics_compiler:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics_compiler
|
||||
sha256: "7ee12e6dffe0fc8e755179d6d91b3b34f5924223fc104d85572ef9180d73d172"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.5"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1066,13 +1234,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.0.2"
|
||||
vocsy_epub_viewer:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "packages/vocsy_epub_viewer"
|
||||
relative: true
|
||||
source: path
|
||||
version: "3.0.0"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1130,5 +1291,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.9.0 <4.0.0"
|
||||
dart: ">=3.10.0 <4.0.0"
|
||||
flutter: ">=3.35.0"
|
||||
|
||||
+1
-2
@@ -63,8 +63,7 @@ dependencies:
|
||||
flutter_rating:
|
||||
html_unescape:
|
||||
webdav_client:
|
||||
vocsy_epub_viewer:
|
||||
path: ./packages/vocsy_epub_viewer
|
||||
cosmos_epub:
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
@Tags(['integration'])
|
||||
library;
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
import 'package:calibre_web_companion/core/services/api_service.dart';
|
||||
import 'package:calibre_web_companion/core/services/tag_service.dart';
|
||||
import 'package:calibre_web_companion/features/book_details/data/datasources/book_details_remote_datasource.dart';
|
||||
|
||||
import '../../helpers/test_setup.dart';
|
||||
|
||||
void main() {
|
||||
late ApiService api;
|
||||
late BookDetailsRemoteDatasource dataSource;
|
||||
|
||||
Future<void> setUpDataSource() async {
|
||||
api = await setupIntegrationTest();
|
||||
final logger = Logger(level: Level.off);
|
||||
dataSource = BookDetailsRemoteDatasource(
|
||||
apiService: api,
|
||||
logger: logger,
|
||||
tagService: TagService(apiService: api, logger: logger),
|
||||
);
|
||||
}
|
||||
|
||||
test('fetchBookDetails() loads details for a real book', () async {
|
||||
await setUpDataSource();
|
||||
final book = await fetchFirstBook(api);
|
||||
|
||||
final details = await dataSource.fetchBookDetails(book, book.uuid);
|
||||
|
||||
expect(details, isNotNull);
|
||||
expect(details.title, isNotEmpty);
|
||||
});
|
||||
|
||||
test('toggleReadStatus() flips and restores the read flag', () async {
|
||||
await setUpDataSource();
|
||||
final book = await fetchFirstBook(api);
|
||||
|
||||
final first = await dataSource.toggleReadStatus(book.id);
|
||||
expect(first, isTrue);
|
||||
|
||||
final second = await dataSource.toggleReadStatus(book.id);
|
||||
expect(second, isTrue);
|
||||
});
|
||||
|
||||
test('toggleArchiveStatus() flips and restores the archive flag', () async {
|
||||
await setUpDataSource();
|
||||
final book = await fetchFirstBook(api);
|
||||
|
||||
final first = await dataSource.toggleArchiveStatus(book.id);
|
||||
expect(first, isTrue);
|
||||
|
||||
final second = await dataSource.toggleArchiveStatus(book.id);
|
||||
expect(second, isTrue);
|
||||
});
|
||||
|
||||
test('getDownloadStream() returns a 200 stream for a real book', () async {
|
||||
await setUpDataSource();
|
||||
final book = await fetchFirstBook(api);
|
||||
|
||||
final details = await dataSource.fetchBookDetails(book, book.uuid);
|
||||
if (details.formats.isEmpty) {
|
||||
markTestSkipped(
|
||||
'Book "${book.title}" has no downloadable formats -> cannot test',
|
||||
);
|
||||
return;
|
||||
}
|
||||
final format = details.formats.first;
|
||||
|
||||
try {
|
||||
final response = await dataSource.getDownloadStream(
|
||||
book.id.toString(),
|
||||
format,
|
||||
);
|
||||
expect(response.statusCode, 200);
|
||||
} catch (e) {
|
||||
final msg = e.toString();
|
||||
if (msg.contains('Server error')) {
|
||||
markTestSkipped('Server returned 5xx for /download/${book.id}/$format');
|
||||
return;
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
});
|
||||
|
||||
test('getMetadataProviders() returns the configured providers', () async {
|
||||
await setUpDataSource();
|
||||
|
||||
final providers = await dataSource.getMetadataProviders();
|
||||
|
||||
expect(providers, isA<List>());
|
||||
});
|
||||
|
||||
test('searchMetadata() returns results for a query', () async {
|
||||
await setUpDataSource();
|
||||
|
||||
try {
|
||||
final results = await dataSource.searchMetadata('Tolkien', const []);
|
||||
expect(results, isA<List>());
|
||||
} catch (e) {
|
||||
final msg = e.toString();
|
||||
if (msg.contains('Server error')) {
|
||||
markTestSkipped(
|
||||
'Server returned 5xx for /metadata/search -> likely no metadata',
|
||||
);
|
||||
return;
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
});
|
||||
|
||||
test('getSeriesPath() resolves without throwing', () async {
|
||||
await setUpDataSource();
|
||||
|
||||
final path = await dataSource.getSeriesPath('Harry Potter');
|
||||
|
||||
expect(path == null || path.isNotEmpty, isTrue);
|
||||
});
|
||||
|
||||
test(
|
||||
'deleteBook() — POST /delete/{id}',
|
||||
() async {},
|
||||
skip: 'Destructive: permanently deletes a book from the real library.',
|
||||
);
|
||||
|
||||
test(
|
||||
'updateBookMetadata() — POST /admin/book/{id}',
|
||||
() async {},
|
||||
skip: 'Destructive: overwrites metadata of a real book.',
|
||||
);
|
||||
|
||||
test(
|
||||
'sendBookViaEmail() — POST /send/{id}/{format}/{conversion}',
|
||||
() async {},
|
||||
skip: 'Destructive: triggers a real e-mail send from the server.',
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
@Tags(['integration'])
|
||||
library;
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
import 'package:calibre_web_companion/features/book_view/data/datasources/book_view_remote_datasource.dart';
|
||||
|
||||
import '../../helpers/test_setup.dart';
|
||||
|
||||
void main() {
|
||||
late BookViewRemoteDatasource dataSource;
|
||||
|
||||
Future<void> setUpDataSource() async {
|
||||
final api = await setupIntegrationTest();
|
||||
dataSource = BookViewRemoteDatasource(
|
||||
apiService: api,
|
||||
logger: Logger(level: Level.off),
|
||||
preferences: testPrefs(),
|
||||
);
|
||||
}
|
||||
|
||||
test('fetchBooks() returns books from the real library', () async {
|
||||
await setUpDataSource();
|
||||
|
||||
final books = await dataSource.fetchBooks(offset: 0, limit: 10);
|
||||
|
||||
expect(books, isNotEmpty);
|
||||
expect(books.first.id, greaterThan(0));
|
||||
expect(books.first.title, isNotEmpty);
|
||||
});
|
||||
|
||||
test('fetchBooks() respects the limit parameter', () async {
|
||||
await setUpDataSource();
|
||||
|
||||
final books = await dataSource.fetchBooks(offset: 0, limit: 3);
|
||||
|
||||
expect(books.length, lessThanOrEqualTo(3));
|
||||
});
|
||||
|
||||
test('fetchBooks() pagination via offset returns different pages', () async {
|
||||
await setUpDataSource();
|
||||
|
||||
final firstPage = await dataSource.fetchBooks(offset: 0, limit: 5);
|
||||
final secondPage = await dataSource.fetchBooks(offset: 5, limit: 5);
|
||||
|
||||
if (firstPage.isNotEmpty && secondPage.isNotEmpty) {
|
||||
expect(firstPage.first.id, isNot(equals(secondPage.first.id)));
|
||||
}
|
||||
});
|
||||
|
||||
test('fetchBooks() with sort parameters does not error', () async {
|
||||
await setUpDataSource();
|
||||
|
||||
final books = await dataSource.fetchBooks(
|
||||
offset: 0,
|
||||
limit: 5,
|
||||
sortBy: 'title',
|
||||
sortOrder: 'asc',
|
||||
);
|
||||
|
||||
expect(books, isA<List>());
|
||||
});
|
||||
|
||||
test('fetchBooks() with a search query returns matching books', () async {
|
||||
await setUpDataSource();
|
||||
|
||||
final sample = await dataSource.fetchBooks(offset: 0, limit: 1);
|
||||
expect(sample, isNotEmpty);
|
||||
final token = sample.first.title.split(' ').first;
|
||||
|
||||
final results = await dataSource.fetchBooks(
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
searchQuery: token,
|
||||
);
|
||||
|
||||
expect(results, isA<List>());
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
@Tags(['integration'])
|
||||
library;
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
import 'package:calibre_web_companion/features/discover/blocs/discover_event.dart';
|
||||
import 'package:calibre_web_companion/features/discover_details/data/datasources/discover_details_remote_datasource.dart';
|
||||
|
||||
import '../../helpers/test_setup.dart';
|
||||
|
||||
void main() {
|
||||
late DiscoverDetailsRemoteDatasource dataSource;
|
||||
|
||||
Future<void> setUpDataSource() async {
|
||||
final api = await setupIntegrationTest();
|
||||
dataSource = DiscoverDetailsRemoteDatasource(
|
||||
apiService: api,
|
||||
logger: Logger(level: Level.off),
|
||||
preferences: testPrefs(),
|
||||
);
|
||||
}
|
||||
|
||||
const optionalTypes = {
|
||||
DiscoverType.hot,
|
||||
DiscoverType.rated,
|
||||
DiscoverType.readbooks,
|
||||
DiscoverType.unreadbooks,
|
||||
};
|
||||
for (final type in const [
|
||||
DiscoverType.discover,
|
||||
DiscoverType.hot,
|
||||
DiscoverType.newlyAdded,
|
||||
DiscoverType.rated,
|
||||
DiscoverType.readbooks,
|
||||
DiscoverType.unreadbooks,
|
||||
]) {
|
||||
test('loadBooks(${type.name}) returns a feed', () async {
|
||||
await setUpDataSource();
|
||||
|
||||
try {
|
||||
final feed = await dataSource.loadBooks(type);
|
||||
expect(feed, isNotNull);
|
||||
expect(feed.books, isA<List>());
|
||||
} catch (e) {
|
||||
if (optionalTypes.contains(type) && e.toString().contains('404')) {
|
||||
return;
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (final type in const [
|
||||
CategoryType.author,
|
||||
CategoryType.category,
|
||||
CategoryType.series,
|
||||
CategoryType.publisher,
|
||||
CategoryType.language,
|
||||
CategoryType.formats,
|
||||
CategoryType.ratings,
|
||||
]) {
|
||||
test('loadCategories(${type.name}) returns a feed', () async {
|
||||
await setUpDataSource();
|
||||
|
||||
final feed = await dataSource.loadCategories(type);
|
||||
|
||||
expect(feed, isNotNull);
|
||||
expect(feed.categories, isA<List>());
|
||||
});
|
||||
}
|
||||
|
||||
test('loadCategories(libraries) hits /libraries', () async {
|
||||
await setUpDataSource();
|
||||
|
||||
try {
|
||||
final feed = await dataSource.loadCategories(CategoryType.libraries);
|
||||
expect(feed.categories, isA<List>());
|
||||
} catch (e) {
|
||||
expect(e, isA<Exception>());
|
||||
}
|
||||
});
|
||||
|
||||
test('loadBooksFromPath() loads books from an explicit OPDS path', () async {
|
||||
await setUpDataSource();
|
||||
|
||||
final feed = await dataSource.loadBooksFromPath('/opds/new');
|
||||
|
||||
expect(feed, isNotNull);
|
||||
expect(feed.books, isA<List>());
|
||||
});
|
||||
|
||||
test('loadCategoriesgeneric() parses a category path', () async {
|
||||
await setUpDataSource();
|
||||
|
||||
final feed = await dataSource.loadCategoriesgeneric('/opds/category');
|
||||
|
||||
expect(feed, isNotNull);
|
||||
expect(feed.categories, isA<List>());
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
@Tags(['integration'])
|
||||
library;
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:calibre_web_companion/core/services/api_service.dart';
|
||||
import 'package:calibre_web_companion/features/download_service/data/datasources/download_service_remote_datasource.dart';
|
||||
import 'package:calibre_web_companion/features/login_settings/data/datasources/login_settings_local_datasource.dart';
|
||||
import 'package:calibre_web_companion/features/login_settings/data/repositories/login_settings_repository.dart';
|
||||
|
||||
import '../../test_env.dart';
|
||||
|
||||
void main() {
|
||||
late DownloadServiceRemoteDataSource dataSource;
|
||||
|
||||
Future<void> setUpDataSource() async {
|
||||
SharedPreferences.setMockInitialValues({
|
||||
'downloader_url': TestEnv.downloaderUrl,
|
||||
'downloader_username': TestEnv.downloaderUsername,
|
||||
'downloader_password': TestEnv.downloaderPassword,
|
||||
});
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final logger = Logger(level: Level.off);
|
||||
|
||||
final loginSettingsRepository = LoginSettingsRepository(
|
||||
loginSettingsLocalDataSource: LoginSettingsLocalDataSource(
|
||||
preferences: prefs,
|
||||
logger: logger,
|
||||
apiService: ApiService(),
|
||||
),
|
||||
logger: logger,
|
||||
);
|
||||
|
||||
dataSource = DownloadServiceRemoteDataSource(
|
||||
client: http.Client(),
|
||||
sharedPreferences: prefs,
|
||||
logger: logger,
|
||||
loginSettingsRepository: loginSettingsRepository,
|
||||
);
|
||||
}
|
||||
|
||||
test(
|
||||
'searchBooks() returns results (GET /api/releases)',
|
||||
() async {
|
||||
await setUpDataSource();
|
||||
|
||||
final books = await dataSource.searchBooks('Tolkien');
|
||||
|
||||
expect(books, isA<List>());
|
||||
},
|
||||
skip: TestEnv.hasDownloader ? false : 'TestEnv.downloaderUrl not set',
|
||||
);
|
||||
|
||||
test(
|
||||
'getDownloadStatus() returns status (GET /api/status)',
|
||||
() async {
|
||||
await setUpDataSource();
|
||||
|
||||
final books = await dataSource.getDownloadStatus();
|
||||
|
||||
expect(books, isA<List>());
|
||||
},
|
||||
skip: TestEnv.hasDownloader ? false : 'TestEnv.downloaderUrl not set',
|
||||
);
|
||||
|
||||
test(
|
||||
'getConfig() returns the downloader config (GET /api/config)',
|
||||
() async {
|
||||
await setUpDataSource();
|
||||
|
||||
final config = await dataSource.getConfig();
|
||||
|
||||
expect(config, isNotNull);
|
||||
},
|
||||
skip: TestEnv.hasDownloader ? false : 'TestEnv.downloaderUrl not set',
|
||||
);
|
||||
|
||||
test(
|
||||
'downloadBook() — POST /api/releases/download',
|
||||
() async {},
|
||||
skip: 'Destructive: queues a real download on the downloader service.',
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
@Tags(['integration'])
|
||||
library;
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
import 'package:calibre_web_companion/features/login/data/datasources/login_remote_datasource.dart';
|
||||
|
||||
import '../../helpers/test_setup.dart';
|
||||
|
||||
void main() {
|
||||
test('login() succeeds with valid credentials (POST /login)', () async {
|
||||
final api = await setupIntegrationTest();
|
||||
expect(api, isNotNull);
|
||||
});
|
||||
|
||||
test('canAccessWebsite() returns true for a valid session', () async {
|
||||
final api = await setupIntegrationTest();
|
||||
final dataSource = LoginRemoteDataSource(
|
||||
apiService: api,
|
||||
logger: Logger(level: Level.off),
|
||||
);
|
||||
|
||||
final canAccess = await dataSource.canAccessWebsite();
|
||||
expect(canAccess, isTrue);
|
||||
});
|
||||
|
||||
test('getStoredServerType() reflects the stored server type', () async {
|
||||
final api = await setupIntegrationTest();
|
||||
final dataSource = LoginRemoteDataSource(
|
||||
apiService: api,
|
||||
logger: Logger(level: Level.off),
|
||||
);
|
||||
|
||||
final type = await dataSource.getStoredServerType();
|
||||
expect(type.name, 'calibreWeb');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
@Tags(['integration'])
|
||||
library;
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:calibre_web_companion/features/me/data/datasources/me_remote_datasource.dart';
|
||||
|
||||
import '../../helpers/test_setup.dart';
|
||||
|
||||
void main() {
|
||||
test('getStats() returns library statistics', () async {
|
||||
final api = await setupIntegrationTest();
|
||||
final dataSource = MeRemoteDataSource(
|
||||
apiService: api,
|
||||
preferences: testPrefs(),
|
||||
);
|
||||
|
||||
final stats = await dataSource.getStats();
|
||||
|
||||
expect(stats, isNotNull);
|
||||
expect(stats.books, greaterThanOrEqualTo(0));
|
||||
});
|
||||
|
||||
test('getIsOpds() is false for a Calibre-Web server', () async {
|
||||
final api = await setupIntegrationTest();
|
||||
final dataSource = MeRemoteDataSource(
|
||||
apiService: api,
|
||||
preferences: testPrefs(),
|
||||
);
|
||||
|
||||
expect(dataSource.getIsOpds(), isFalse);
|
||||
});
|
||||
|
||||
test(
|
||||
'logOut() — GET /logout',
|
||||
() async {},
|
||||
skip: 'Skipped: ends the session shared by all integration tests.',
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
@Tags(['integration'])
|
||||
library;
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
import 'package:calibre_web_companion/core/services/api_service.dart';
|
||||
import 'package:calibre_web_companion/features/shelf_details/data/datasources/shelf_details_remote_datasource.dart';
|
||||
import 'package:calibre_web_companion/features/shelf_view.dart/data/datasources/shelf_view_remote_datasource.dart';
|
||||
|
||||
import '../../helpers/test_setup.dart';
|
||||
|
||||
void main() {
|
||||
late ApiService api;
|
||||
late Logger logger;
|
||||
late ShelfDetailsRemoteDataSource dataSource;
|
||||
late ShelfViewRemoteDataSource shelfViewSource;
|
||||
|
||||
Future<void> setUpDataSources() async {
|
||||
api = await setupIntegrationTest();
|
||||
logger = Logger(level: Level.off);
|
||||
dataSource = ShelfDetailsRemoteDataSource(
|
||||
apiService: api,
|
||||
logger: logger,
|
||||
preferences: testPrefs(),
|
||||
);
|
||||
shelfViewSource = ShelfViewRemoteDataSource(
|
||||
apiService: api,
|
||||
logger: logger,
|
||||
preferences: testPrefs(),
|
||||
shelfDetailsRemoteDataSource: dataSource,
|
||||
);
|
||||
}
|
||||
|
||||
test('getShelfDetails() loads a freshly created shelf', () async {
|
||||
await setUpDataSources();
|
||||
final shelfName = 'cwc_itest_${DateTime.now().millisecondsSinceEpoch}';
|
||||
|
||||
String? shelfId;
|
||||
try {
|
||||
shelfId = await shelfViewSource.createShelf(shelfName);
|
||||
|
||||
// GET /opds/shelf/{id}
|
||||
final details = await dataSource.getShelfDetails(shelfId);
|
||||
expect(details, isNotNull);
|
||||
expect(details.books, isA<List>());
|
||||
} finally {
|
||||
if (shelfId != null) {
|
||||
await dataSource.deleteShelf(shelfId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('editShelf() renames a shelf (POST /shelf/edit/{id})', () async {
|
||||
await setUpDataSources();
|
||||
final shelfName = 'cwc_itest_${DateTime.now().millisecondsSinceEpoch}';
|
||||
|
||||
String? shelfId;
|
||||
try {
|
||||
shelfId = await shelfViewSource.createShelf(shelfName);
|
||||
|
||||
final renamed = await dataSource.editShelf(
|
||||
shelfId,
|
||||
'${shelfName}_edited',
|
||||
);
|
||||
expect(renamed, isTrue);
|
||||
} finally {
|
||||
if (shelfId != null) {
|
||||
await dataSource.deleteShelf(shelfId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('removeFromShelf() removes a book (POST /shelf/remove)', () async {
|
||||
await setUpDataSources();
|
||||
final book = await fetchFirstBook(api);
|
||||
final shelfName = 'cwc_itest_${DateTime.now().millisecondsSinceEpoch}';
|
||||
|
||||
String? shelfId;
|
||||
try {
|
||||
shelfId = await shelfViewSource.createShelf(shelfName);
|
||||
await shelfViewSource.addBookToShelf(
|
||||
bookId: book.id.toString(),
|
||||
shelfId: shelfId,
|
||||
);
|
||||
|
||||
final removed = await dataSource.removeFromShelf(
|
||||
shelfId,
|
||||
book.id.toString(),
|
||||
);
|
||||
expect(removed, isTrue);
|
||||
} finally {
|
||||
if (shelfId != null) {
|
||||
await dataSource.deleteShelf(shelfId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('deleteShelf() deletes a shelf (POST /shelf/delete/{id})', () async {
|
||||
await setUpDataSources();
|
||||
final shelfName = 'cwc_itest_${DateTime.now().millisecondsSinceEpoch}';
|
||||
|
||||
final shelfId = await shelfViewSource.createShelf(shelfName);
|
||||
final deleted = await dataSource.deleteShelf(shelfId);
|
||||
|
||||
expect(deleted, isTrue);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
@Tags(['integration'])
|
||||
library;
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
import 'package:calibre_web_companion/core/services/api_service.dart';
|
||||
import 'package:calibre_web_companion/features/shelf_details/data/datasources/shelf_details_remote_datasource.dart';
|
||||
import 'package:calibre_web_companion/features/shelf_view.dart/data/datasources/shelf_view_remote_datasource.dart';
|
||||
|
||||
import '../../helpers/test_setup.dart';
|
||||
|
||||
void main() {
|
||||
late ApiService api;
|
||||
late Logger logger;
|
||||
late ShelfDetailsRemoteDataSource detailsSource;
|
||||
late ShelfViewRemoteDataSource dataSource;
|
||||
|
||||
Future<void> setUpDataSources() async {
|
||||
api = await setupIntegrationTest();
|
||||
logger = Logger(level: Level.off);
|
||||
detailsSource = ShelfDetailsRemoteDataSource(
|
||||
apiService: api,
|
||||
logger: logger,
|
||||
preferences: testPrefs(),
|
||||
);
|
||||
dataSource = ShelfViewRemoteDataSource(
|
||||
apiService: api,
|
||||
logger: logger,
|
||||
preferences: testPrefs(),
|
||||
shelfDetailsRemoteDataSource: detailsSource,
|
||||
);
|
||||
}
|
||||
|
||||
test('loadShelves() returns the shelf list', () async {
|
||||
await setUpDataSources();
|
||||
|
||||
final shelves = await dataSource.loadShelves();
|
||||
|
||||
expect(shelves, isNotNull);
|
||||
expect(shelves.shelves, isA<List>());
|
||||
});
|
||||
|
||||
test(
|
||||
'shelf lifecycle: create -> add book -> remove book -> delete',
|
||||
() async {
|
||||
await setUpDataSources();
|
||||
final book = await fetchFirstBook(api);
|
||||
final shelfName = 'cwc_itest_${DateTime.now().millisecondsSinceEpoch}';
|
||||
|
||||
String? shelfId;
|
||||
try {
|
||||
// POST /shelf/create
|
||||
shelfId = await dataSource.createShelf(shelfName);
|
||||
expect(shelfId, isNotEmpty);
|
||||
|
||||
// POST /shelf/add/{shelf}/{book}
|
||||
await dataSource.addBookToShelf(
|
||||
bookId: book.id.toString(),
|
||||
shelfId: shelfId,
|
||||
);
|
||||
|
||||
// POST /shelf/remove/{shelf}/{book}
|
||||
await dataSource.removeBookFromShelf(
|
||||
bookId: book.id.toString(),
|
||||
shelfId: shelfId,
|
||||
);
|
||||
} finally {
|
||||
if (shelfId != null) {
|
||||
await detailsSource.deleteShelf(shelfId);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test('findShelvesContainingBook() runs without error', () async {
|
||||
await setUpDataSources();
|
||||
final book = await fetchFirstBook(api);
|
||||
|
||||
final shelves = await dataSource.findShelvesContainingBook(
|
||||
book.id.toString(),
|
||||
);
|
||||
|
||||
expect(shelves, isA<List>());
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
@Tags(['integration'])
|
||||
library;
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
import 'package:calibre_web_companion/features/shelf_details/data/datasources/shelf_details_remote_datasource.dart';
|
||||
import 'package:calibre_web_companion/features/shelf_view.dart/data/datasources/shelf_view_remote_datasource.dart';
|
||||
|
||||
import '../../helpers/test_setup.dart';
|
||||
|
||||
void main() {
|
||||
test('GET /opds/shelf/{id} returns a parseable OPDS feed', () async {
|
||||
final api = await setupIntegrationTest();
|
||||
final logger = Logger(level: Level.off);
|
||||
|
||||
final detailsSource = ShelfDetailsRemoteDataSource(
|
||||
apiService: api,
|
||||
logger: logger,
|
||||
preferences: testPrefs(),
|
||||
);
|
||||
final shelfViewSource = ShelfViewRemoteDataSource(
|
||||
apiService: api,
|
||||
logger: logger,
|
||||
preferences: testPrefs(),
|
||||
shelfDetailsRemoteDataSource: detailsSource,
|
||||
);
|
||||
final book = await fetchFirstBook(api);
|
||||
final shelfName = 'cwc_itest_${DateTime.now().millisecondsSinceEpoch}';
|
||||
|
||||
String? shelfId;
|
||||
try {
|
||||
shelfId = await shelfViewSource.createShelf(shelfName);
|
||||
await shelfViewSource.addBookToShelf(
|
||||
bookId: book.id.toString(),
|
||||
shelfId: shelfId,
|
||||
);
|
||||
|
||||
final response = await api.get(endpoint: '/opds/shelf/$shelfId');
|
||||
|
||||
expect(response.statusCode, 200);
|
||||
expect(response.body, contains('<entry>'));
|
||||
|
||||
final hasUuid = RegExp(
|
||||
r'<id>urn:uuid:([a-fA-F0-9-]+)</id>',
|
||||
).hasMatch(response.body);
|
||||
final hasDownloadId = RegExp(
|
||||
r'href="\/opds\/download\/(\d+)\/',
|
||||
).hasMatch(response.body);
|
||||
expect(hasUuid && hasDownloadId, isTrue);
|
||||
} finally {
|
||||
if (shelfId != null) {
|
||||
await detailsSource.deleteShelf(shelfId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import 'package:calibre_web_companion/core/services/api_service.dart';
|
||||
import 'package:calibre_web_companion/features/book_view/data/datasources/book_view_remote_datasource.dart';
|
||||
import 'package:calibre_web_companion/features/book_view/data/models/book_view_model.dart';
|
||||
import 'package:calibre_web_companion/features/login/bloc/login_state.dart';
|
||||
import 'package:calibre_web_companion/features/login/data/datasources/login_remote_datasource.dart';
|
||||
import 'package:calibre_web_companion/features/login/data/models/login_credentials.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../test_env.dart';
|
||||
|
||||
Future<ApiService> setupIntegrationTest() async {
|
||||
SharedPreferences.setMockInitialValues({
|
||||
'base_url': TestEnv.baseUrl,
|
||||
'username': TestEnv.username,
|
||||
'password': TestEnv.password,
|
||||
'server_type': 'calibreWeb',
|
||||
});
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (!GetIt.instance.isRegistered<SharedPreferences>()) {
|
||||
GetIt.instance.registerSingleton<SharedPreferences>(prefs);
|
||||
}
|
||||
|
||||
if (GetIt.instance.isRegistered<ApiService>()) {
|
||||
GetIt.instance.unregister<ApiService>();
|
||||
}
|
||||
final apiService = ApiService();
|
||||
GetIt.instance.registerSingleton<ApiService>(apiService);
|
||||
await apiService.initialize();
|
||||
|
||||
final logger = Logger(level: Level.off);
|
||||
final loginDataSource = LoginRemoteDataSource(
|
||||
apiService: apiService,
|
||||
logger: logger,
|
||||
);
|
||||
|
||||
final success = await loginDataSource.login(
|
||||
LoginCredentials(
|
||||
baseUrl: TestEnv.baseUrl,
|
||||
username: TestEnv.username,
|
||||
password: TestEnv.password,
|
||||
),
|
||||
ServerType.calibreWeb,
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
throw Exception('Login failed during integration test setup.');
|
||||
}
|
||||
|
||||
return apiService;
|
||||
}
|
||||
|
||||
SharedPreferences testPrefs() => GetIt.instance<SharedPreferences>();
|
||||
|
||||
Future<BookViewModel> fetchFirstBook(ApiService api) async {
|
||||
final ds = BookViewRemoteDatasource(
|
||||
apiService: api,
|
||||
logger: Logger(level: Level.off),
|
||||
preferences: testPrefs(),
|
||||
);
|
||||
final books = await ds.fetchBooks(offset: 0, limit: 1);
|
||||
if (books.isEmpty) {
|
||||
throw Exception('Library is empty!');
|
||||
}
|
||||
return books.first;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
@Tags(['integration'])
|
||||
library;
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'test_env.dart';
|
||||
|
||||
void main() {
|
||||
test('server is reachable', () async {
|
||||
final response = await http.get(Uri.parse(TestEnv.baseUrl));
|
||||
expect(response.statusCode, anyOf(200, 302, 401));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user