diff --git a/lib/screens/reading/pdf/pdf_book_screen.dart b/lib/screens/reading/pdf/pdf_book_screen.dart index e3295227..7a592b37 100644 --- a/lib/screens/reading/pdf/pdf_book_screen.dart +++ b/lib/screens/reading/pdf/pdf_book_screen.dart @@ -13,6 +13,7 @@ import '../../../widgets/password_dialog.dart'; import 'pdf_thumbnails_screen.dart'; import 'package:otzaria/models/tabs.dart'; import 'package:printing/printing.dart'; +import 'package:otzaria/utils/page_converter.dart'; class PdfBookViewr extends StatefulWidget { final PdfBookTab tab; @@ -70,22 +71,20 @@ class _PdfBookViewrState extends State } return IconButton( onPressed: () async { - //find the latest outline - final outlines = await widget - .tab.pdfViewerController.document - .loadOutline(); - final outline = outlines[0].children.firstWhere( - (element) => - element.dest?.pageNumber == - (widget.tab.pdfViewerController.pageNumber ?? - 0), - ); + final appModel = context.read(); + final textBook = await appModel.library.then((library) => + library.findBookByTitle(widget.tab.title, TextBook)); - openTextBookFromRef( - widget.tab.title, - outline.title, - context, - ); + if (textBook != null) { + final currentPage = + widget.tab.pdfViewerController.pageNumber ?? 0; + final textIndex = await pdfToTextPage( + widget.tab.title, currentPage, context); + + if (textIndex != null) { + appModel.openBook(textBook, textIndex); + } + } }, icon: const Icon( Icons.text_snippet, diff --git a/lib/screens/reading/text/text_book_screen.dart b/lib/screens/reading/text/text_book_screen.dart index 09143ee7..9ba8cb17 100644 --- a/lib/screens/reading/text/text_book_screen.dart +++ b/lib/screens/reading/text/text_book_screen.dart @@ -6,6 +6,7 @@ import 'package:otzaria/screens/reading/text/combined_book_screen.dart'; import 'package:otzaria/screens/printing_screen.dart'; import 'package:otzaria/screens/reading/text/splited_view_screen.dart'; import 'package:otzaria/utils/daf_yomi_helper.dart'; +import 'package:otzaria/utils/page_converter.dart'; import 'package:provider/provider.dart'; import 'package:otzaria/screens/reading/text/text_book_search_screen.dart'; import 'dart:io'; @@ -104,16 +105,19 @@ class _TextBookViewerState extends State icon: const Icon(Icons.picture_as_pdf), tooltip: 'פתח ספר במהדורה מודפסת ', onPressed: () async { - openPdfBookFromRef( - widget.tab.book.title, - await utils.refFromIndex( - widget.tab.positionsListener.itemPositions - .value.isNotEmpty - ? widget.tab.positionsListener - .itemPositions.value.first.index - : 0, - widget.tab.book.tableOfContents), + final appModel = context.read(); + final book = await appModel.library.then((library) => + library.findBookByTitle( + widget.tab.title, PdfBook)); + final index = await textToPdfPage( + widget.tab.title, + widget.tab.positionsListener.itemPositions.value + .isNotEmpty + ? widget.tab.positionsListener.itemPositions + .value.first.index + : 0, context); + appModel.openBook(book!, index ?? 0); }) : SizedBox.shrink()), diff --git a/lib/utils/file_sync_service.dart b/lib/utils/file_sync_service.dart index 2ed28dfc..94fd9f12 100644 --- a/lib/utils/file_sync_service.dart +++ b/lib/utils/file_sync_service.dart @@ -8,6 +8,8 @@ class FileSyncService { final String repositoryName; final String branch; bool isSyncing = false; + int _currentProgress = 0; + int _totalFiles = 0; FileSyncService({ required this.githubOwner, @@ -15,6 +17,9 @@ class FileSyncService { this.branch = 'main', }); + int get currentProgress => _currentProgress; + int get totalFiles => _totalFiles; + Future get _localManifestPath async { final directory = _localDirectory; return '${await directory}${Platform.pathSeparator}files_manifest.json'; @@ -30,7 +35,7 @@ class FileSyncService { if (!await file.exists()) { return {}; } - final content = await file.readAsString(); + final content = await file.readAsString(encoding: utf8); return json.decode(content); } catch (e) { print('Error reading local manifest: $e'); @@ -42,9 +47,16 @@ class FileSyncService { final url = 'https://raw.githubusercontent.com/$githubOwner/$repositoryName/$branch/files_manifest.json'; try { - final response = await http.get(Uri.parse(url)); + final response = await http.get( + Uri.parse(url), + headers: { + 'Accept': 'application/json', + 'Accept-Charset': 'utf-8', + }, + ); if (response.statusCode == 200) { - return json.decode(response.body); + // Explicitly decode as UTF-8 + return json.decode(utf8.decode(response.bodyBytes)); } throw Exception('Failed to fetch remote manifest'); } catch (e) { @@ -57,14 +69,29 @@ class FileSyncService { final url = 'https://raw.githubusercontent.com/$githubOwner/$repositoryName/$branch/$filePath'; try { - final response = await http.get(Uri.parse(url)); + final response = await http.get( + Uri.parse(url), + headers: { + 'Accept-Charset': 'utf-8', + }, + ); if (response.statusCode == 200) { final directory = await _localDirectory; final file = File('$directory/$filePath'); // Create directories if they don't exist await file.parent.create(recursive: true); - await file.writeAsBytes(response.bodyBytes); + + // For text files, handle UTF-8 encoding explicitly + if (filePath.endsWith('.txt') || + filePath.endsWith('.json') || + filePath.endsWith('.csv')) { + await file.writeAsString(utf8.decode(response.bodyBytes), + encoding: utf8); + } else { + // For binary files, write bytes directly + await file.writeAsBytes(response.bodyBytes); + } } } catch (e) { print('Error downloading file $filePath: $e'); @@ -80,8 +107,11 @@ class FileSyncService { // Update the manifest for this specific file localManifest[filePath] = fileInfo; - // Write the updated manifest back to disk - await manifestFile.writeAsString(json.encode(localManifest)); + // Write the updated manifest back to disk with UTF-8 encoding + await manifestFile.writeAsString( + json.encode(localManifest), + encoding: utf8, + ); } catch (e) { print('Error updating local manifest for file $filePath: $e'); } @@ -89,21 +119,24 @@ class FileSyncService { Future _removeFromLocalManifest(String filePath) async { try { + // Try to remove the actual file if it exists + final directory = await _localDirectory; + final file = File('$directory/$filePath'); + if (await file.exists()) { + await file.delete(); + } + //if successful, remove from manifest final manifestFile = File(await _localManifestPath); Map localManifest = await _getLocalManifest(); // Remove the file from the manifest localManifest.remove(filePath); - // Write the updated manifest back to disk - await manifestFile.writeAsString(json.encode(localManifest)); - - // Also remove the actual file if it exists - final directory = await _localDirectory; - final file = File('$directory/$filePath'); - if (await file.exists()) { - await file.delete(); - } + // Write the updated manifest back to disk with UTF-8 encoding + await manifestFile.writeAsString( + json.encode(localManifest), + encoding: utf8, + ); } catch (e) { print('Error removing file $filePath from local manifest: $e'); } @@ -117,7 +150,7 @@ class FileSyncService { remoteManifest.forEach((filePath, remoteInfo) { if (!localManifest.containsKey(filePath) || - localManifest[filePath]['modified'] != remoteInfo['modified']) { + localManifest[filePath]['hash'] != remoteInfo['hash']) { filesToUpdate.add(filePath); } }); @@ -131,12 +164,15 @@ class FileSyncService { } isSyncing = true; int count = 0; + _currentProgress = 0; + try { final remoteManifest = await _getRemoteManifest(); final localManifest = await _getLocalManifest(); // Find files to update or add final filesToUpdate = await checkForUpdates(); + _totalFiles = filesToUpdate.length; // Download and update manifest for each file individually for (final filePath in filesToUpdate) { @@ -146,6 +182,7 @@ class FileSyncService { await downloadFile(filePath); await _updateLocalManifestForFile(filePath, remoteManifest[filePath]); count++; + _currentProgress = count; } // Remove files that exist locally but not in remote @@ -156,6 +193,7 @@ class FileSyncService { if (!remoteManifest.containsKey(localFilePath)) { await _removeFromLocalManifest(localFilePath); count++; + _currentProgress = count; } } } catch (e) { diff --git a/lib/utils/page_converter.dart b/lib/utils/page_converter.dart new file mode 100644 index 00000000..4536fe1c --- /dev/null +++ b/lib/utils/page_converter.dart @@ -0,0 +1,227 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:otzaria/models/app_model.dart'; +import 'package:otzaria/models/books.dart'; +import 'package:pdfrx/pdfrx.dart'; +import 'package:provider/provider.dart'; + +/// Converts a text book page index to the corresponding PDF page number +/// +/// [bookTitle] is the title of the book +/// [textIndex] is the index in the text version +/// Returns the corresponding page number in the PDF version, or null if not found +Future textToPdfPage( + String bookTitle, int textIndex, BuildContext context) async { + final appModel = Provider.of(context, listen: false); + + // Get both text and PDF versions of the book + final textBook = (await appModel.library).findBookByTitle(bookTitle, TextBook) + as TextBook?; + final pdfBook = + (await appModel.library).findBookByTitle(bookTitle, PdfBook) as PdfBook?; + + if (textBook == null || pdfBook == null) { + return null; + } + + // Get the TOC entry for the text index + final toc = await textBook.tableOfContents; + final tocEntry = _findLastEntryBeforeIndex(toc, textIndex); + if (tocEntry == null) { + return null; + } + + // Find matching outline entry in PDF + final outlines = + await PdfDocument.openFile(pdfBook.path).then((doc) => doc.loadOutline()); + final outlineEntry = _findMatchingOutline(outlines, tocEntry); + + return outlineEntry?.dest?.pageNumber; +} + +/// Converts a PDF page number to the corresponding text book index +/// +/// [bookTitle] is the title of the book +/// [pdfPage] is the page number in the PDF version +/// Returns the corresponding index in the text version, or null if not found +Future pdfToTextPage( + String bookTitle, int pdfPage, BuildContext context) async { + final appModel = Provider.of(context, listen: false); + + // Get both text and PDF versions of the book + final textBook = (await appModel.library).findBookByTitle(bookTitle, TextBook) + as TextBook?; + final pdfBook = + (await appModel.library).findBookByTitle(bookTitle, PdfBook) as PdfBook?; + + if (textBook == null || pdfBook == null) { + return null; + } + + // Get the outline entry for the PDF page + final outlines = + await PdfDocument.openFile(pdfBook.path).then((doc) => doc.loadOutline()); + final outlineEntry = _findOutlineByPage(outlines, pdfPage); + if (outlineEntry == null) { + return null; + } + + // Find matching TOC entry in text book + final toc = await textBook.tableOfContents; + final tocEntry = _findMatchingTocEntry(toc, outlineEntry); + + return tocEntry?.index; +} + +TocEntry? _findLastEntryBeforeIndex(List entries, int targetIndex) { + TocEntry? lastBefore; + + for (var entry in entries) { + // Check if this entry is before target and later than current lastBefore + if (entry.index <= targetIndex && + (lastBefore == null || entry.index > lastBefore.index)) { + lastBefore = entry; + } + + // Recursively search children + final childResult = _findLastEntryBeforeIndex(entry.children, targetIndex); + if (childResult != null && + (lastBefore == null || childResult.index > lastBefore.index)) { + lastBefore = childResult; + } + } + + return lastBefore; +} + +List _getHierarchy(PdfOutlineNode node, List outlines) { + List hierarchy = [node.title]; + PdfOutlineNode? current = node; + + while (current != null) { + PdfOutlineNode? parent = _findParentNode(current, outlines); + if (parent != null) { + hierarchy.insert(0, parent.title); + } + current = parent; + } + + return hierarchy; +} + +List _getTocHierarchy(TocEntry entry, List entries) { + List hierarchy = [entry.text]; + TocEntry? current = entry; + + while (current != null) { + TocEntry? parent = _findTocParent(current, entries); + if (parent != null) { + hierarchy.insert(0, parent.text); + } + current = parent; + } + + return hierarchy; +} + +PdfOutlineNode? _findParentNode( + PdfOutlineNode child, List nodes) { + for (var node in nodes) { + if (node.children.contains(child)) { + return node; + } + final result = _findParentNode(child, node.children); + if (result != null) { + return result; + } + } + return null; +} + +TocEntry? _findTocParent(TocEntry child, List entries) { + for (var entry in entries) { + if (entry.children.contains(child)) { + return entry; + } + final result = _findTocParent(child, entry.children); + if (result != null) { + return result; + } + } + return null; +} + +PdfOutlineNode? _findMatchingOutline( + List outlines, TocEntry tocEntry) { + final tocHierarchy = _getTocHierarchy(tocEntry, []); + + for (var outline in outlines) { + final outlineHierarchy = _getHierarchy(outline, outlines); + + if (_compareHierarchies(tocHierarchy, outlineHierarchy)) { + return outline; + } + + // Recursively search children + final result = _findMatchingOutline(outline.children, tocEntry); + if (result != null) { + return result; + } + } + return null; +} + +PdfOutlineNode? _findOutlineByPage( + List outlines, int targetPage) { + for (var outline in outlines) { + if (outline.dest?.pageNumber == targetPage) { + return outline; + } + // Recursively search children + final result = _findOutlineByPage(outline.children, targetPage); + if (result != null) { + return result; + } + } + return null; +} + +TocEntry? _findMatchingTocEntry( + List entries, PdfOutlineNode outlineNode) { + final outlineHierarchy = _getHierarchy(outlineNode, []); + + for (var entry in entries) { + final tocHierarchy = _getTocHierarchy(entry, entries); + + if (_compareHierarchies(tocHierarchy, outlineHierarchy)) { + return entry; + } + + // Recursively search children + final result = _findMatchingTocEntry(entry.children, outlineNode); + if (result != null) { + return result; + } + } + return null; +} + +bool _compareHierarchies(List hierarchy1, List hierarchy2) { + if (hierarchy1.length != hierarchy2.length) { + return false; + } + + for (int i = 0; i < hierarchy1.length; i++) { + // Normalize strings by trimming whitespace and control characters + final str1 = hierarchy1[i].trim(); + final str2 = hierarchy2[i].trim(); + + // Compare normalized strings + if (str1 != str2) { + return false; + } + } + + return true; +} diff --git a/lib/widgets/file_sync_widget.dart b/lib/widgets/file_sync_widget.dart index aa028363..3e776d1a 100644 --- a/lib/widgets/file_sync_widget.dart +++ b/lib/widgets/file_sync_widget.dart @@ -77,6 +77,15 @@ class _SyncIconButtonState extends State _startSync(); } + void _updateStatus() { + if (widget.fileSync.isSyncing && widget.fileSync.totalFiles > 0) { + setState(() { + _status = + 'מסנכרן קבצים... ${widget.fileSync.currentProgress}/${widget.fileSync.totalFiles}'; + }); + } + } + // פונקציה שמבצעת את הסנכרון בפועל Future _startSync() async { setState(() { @@ -85,7 +94,15 @@ class _SyncIconButtonState extends State _rotationController.repeat(); try { + // Set up a timer to update the status periodically + final statusTimer = + Stream.periodic(const Duration(milliseconds: 100)).listen((_) { + _updateStatus(); + }); + final results = await widget.fileSync.syncFiles(); + statusTimer.cancel(); + int successCount = results; setState(() { diff --git a/pubspec.yaml b/pubspec.yaml index d4c7833c..bfa41d37 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,7 +8,7 @@ msix_config: display_name: אוצריא publisher_display_name: sivan22 identity_name: sivan22.Otzaria - msix_version: 0.2.2.0 + msix_version: 0.2.3.0 logo_path: assets/icon/icon.png publisher: CN=sivan22, O=sivan22, C=IL certificate_path: sivan22.pfx @@ -33,7 +33,7 @@ msix_config: # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 0.2.2-alpha+1 +version: 0.2.3-alpha+1 environment: sdk: ">=3.2.6 <4.0.0"