diff --git a/integration_test/ocr_provider_test.dart b/integration_test/ocr_provider_test.dart new file mode 100644 index 0000000..8d5d163 --- /dev/null +++ b/integration_test/ocr_provider_test.dart @@ -0,0 +1,43 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:snapfig/shared/services/ocr_core/ocr_core.dart'; +import 'package:flutter/services.dart' show rootBundle; +import 'package:path/path.dart' as path_lib; +import 'dart:io'; + +/// assetPath: 예) 'test/unit/assets/test.pdf' +/// 반환: 내부 저장소에 복사된 파일의 경로 +Future copyAssetToLocal(String assetPath) async { + final byteData = await rootBundle.load(assetPath); + final tempDir = await Directory.systemTemp.createTemp('test_pdf_'); + final fileName = path_lib.basename(assetPath); + final file = File(path_lib.join(tempDir.path, fileName)); + await file.writeAsBytes(byteData.buffer.asUint8List()); + return file.path; +} + +void main() { + late OCRProvider provider; + + setUp(() { + provider = OCRProviderImpl( + baseUrl: 'https://901b-39-115-116-188.ngrok-free.app', + ); + }); + + test('process()는 OCRResult를 반환해야 한다', () async { + final pdfPath = await copyAssetToLocal('integration_test/assets/test.pdf'); + final result = await provider.process(pdfPath); + expect(result, isA()); + expect(result.title, isNotEmpty); + expect(result.chapters, isNotEmpty); + expect(result.pages, isNotEmpty); + expect(result.metadata.totalPages, isNotEmpty); + expect(result.metadata.processingTime, isNotEmpty); + expect(result.metadata.totalFigures, isNotEmpty); + }, timeout: const Timeout(Duration(minutes: 15))); + + test('process()에 잘못된 경로를 넣어도 예외가 발생하지 않는다', () async { + final result = await provider.process('invalid/path.pdf'); + expect(result, isA()); + }); +} diff --git a/integration_test/pdf_provider_test.dart b/integration_test/pdf_provider_test.dart index 43328aa..a665929 100644 --- a/integration_test/pdf_provider_test.dart +++ b/integration_test/pdf_provider_test.dart @@ -20,7 +20,13 @@ Future copyAssetToLocal(String assetPath) async { class DummyOcrService implements OCRProvider { @override Future process(String pdfPath) async { - return OCRResult(); + return OCRResult( + title: 'test', + chapters: [], + pages: [], + figures: [], + metadata: Metadata(totalPages: 0, processingTime: 0, totalFigures: 0), + ); } } diff --git a/lib/features/pdf_viewer/models/pdf_viewer_view_model.dart b/lib/features/pdf_viewer/models/pdf_viewer_view_model.dart new file mode 100644 index 0000000..f94e6b6 --- /dev/null +++ b/lib/features/pdf_viewer/models/pdf_viewer_view_model.dart @@ -0,0 +1,231 @@ +import 'package:flutter/foundation.dart'; +import 'package:file_picker/file_picker.dart'; + +import '../../../shared/services/pdf_core/pdf_core.dart'; + +class PdfViewerViewModel extends ChangeNotifier { + // Private state + BasePdf? _pdfModel; + List _pages = []; + List _layouts = []; + Map> _layoutsByPage = {}; + + // UI state + double _zoomPercent = 100.0; + int _currentPage = 1; + String _pdfTitle = 'PDF Viewer'; + String _searchQuery = ''; + bool _sidebarVisible = true; + int _selectedTabIndex = 0; + bool _isLoading = false; + String? _errorMessage; + + // Dependencies + final PDFProvider _pdfProvider; + + PdfViewerViewModel({required PDFProvider pdfProvider}) + : _pdfProvider = pdfProvider; + + BasePdf? get pdfModel => _pdfModel; + List get pages => _pages; + List get layouts => _layouts; + + double get zoomPercent => _zoomPercent; + int get currentPage => _currentPage; + String get pdfTitle => _pdfTitle; + String get searchQuery => _searchQuery; + bool get sidebarVisible => _sidebarVisible; + int get selectedTabIndex => _selectedTabIndex; + bool get isLoading => _isLoading; + String? get errorMessage => _errorMessage; + + // Filtered data + List get filteredPages => + _pages + .where((p) => p.pageIndex.toString().contains(_searchQuery)) + .toList(); + + List get filteredLayouts => + _layouts + .where((l) => l.type == LayoutType.figure) + .where( + (l) => + l.figureId?.toLowerCase().contains( + _searchQuery.toLowerCase(), + ) ?? + false, + ) + .toList(); + + // Get layouts for a specific page + List getLayoutsForPage(int pageIndex) { + return _layoutsByPage[pageIndex] ?? []; + } + + // Get figure references for a specific page + List getFigureReferencesForPage(int pageIndex) { + return getLayoutsForPage( + pageIndex, + ).where((layout) => layout.type == LayoutType.figureReference).toList(); + } + + // Get all figure references + List get figureReferences => + _layouts + .where((layout) => layout.type == LayoutType.figureReference) + .toList(); + + // Get all figures + List get figures => + _layouts.where((layout) => layout.type == LayoutType.figure).toList(); + + // Find figure by ID + BaseLayout? findFigureById(String figureId) { + try { + return figures.firstWhere((figure) => figure.figureId == figureId); + } catch (e) { + return null; + } + } + + // Find figure by referenced figure ID + BaseLayout? findFigureByReference(BaseLayout reference) { + if (reference.type != LayoutType.figureReference) return null; + final referencedId = reference.referencedFigureId; + if (referencedId == null) return null; + return findFigureById(referencedId); + } + + // Get page containing a specific layout + BasePage? getPageForLayout(BaseLayout layout) { + for (final entry in _layoutsByPage.entries) { + if (entry.value.contains(layout)) { + return _pages.firstWhere((page) => page.pageIndex == entry.key); + } + } + return null; + } + + // Navigate to a specific figure by finding its page + int? getPageNumberForFigure(BaseLayout figure) { + if (figure.type != LayoutType.figure) return null; + + final page = getPageForLayout(figure); + if (page != null) { + return page.pageIndex + + 1; // Convert to 1-based page number for PDF controller + } + return null; + } + + // Methods + Future loadPdf({required String path, required bool isAsset}) async { + try { + _setLoading(true); + _clearError(); + + // 2. Get PDF from provider by path + final model = await _pdfProvider.getPDF(path); + + if (model == null) { + throw StateError('PDF not found in provider: $path'); + } + + // 3. Load pages and layouts + final pages = await model.getPages(); + final allLayouts = []; + final layoutsByPage = >{}; + + for (final page in pages) { + final layouts = await page.getLayouts(); + allLayouts.addAll(layouts); + layoutsByPage[page.pageIndex] = layouts; + } + + // 4. Update state + _pdfModel = model; + _pages = pages; + _layouts = allLayouts; + _layoutsByPage = layoutsByPage; + _currentPage = 1; + _zoomPercent = 100.0; + _pdfTitle = model.name; + + notifyListeners(); + } catch (e) { + _setError('Failed to load PDF: $e'); + } finally { + _setLoading(false); + } + } + + Future pickPdfFromDevice() async { + try { + final result = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['pdf'], + ); + + if (result != null && result.files.single.path != null) { + await loadPdf(path: result.files.single.path!, isAsset: false); + } + } catch (e) { + _setError('Failed to pick PDF: $e'); + } + } + + void updateZoom(double scale) { + _zoomPercent = (scale * 100).roundToDouble(); + notifyListeners(); + } + + void updateCurrentPage(int pageIndex) { + _currentPage = pageIndex; + notifyListeners(); + } + + void updateSearchQuery(String query) { + _searchQuery = query; + notifyListeners(); + } + + void toggleSidebar() { + _sidebarVisible = !_sidebarVisible; + notifyListeners(); + } + + void selectTab(int index) { + _selectedTabIndex = index; + notifyListeners(); + } + + void clearSearch() { + _searchQuery = ''; + notifyListeners(); + } + + void clearError() { + _clearError(); + } + + // Private helpers + void _setLoading(bool loading) { + _isLoading = loading; + notifyListeners(); + } + + void _setError(String error) { + _errorMessage = error; + notifyListeners(); + } + + void _clearError() { + _errorMessage = null; + notifyListeners(); + } + + @override + void dispose() { + super.dispose(); + } +} diff --git a/lib/features/pdf_viewer/screens/pdf_viewer.dart b/lib/features/pdf_viewer/screens/pdf_viewer.dart new file mode 100644 index 0000000..a507a36 --- /dev/null +++ b/lib/features/pdf_viewer/screens/pdf_viewer.dart @@ -0,0 +1,719 @@ +import 'package:flutter/material.dart'; +import 'package:pdfrx/pdfrx.dart'; + +import '../../../shared/services/pdf_core/pdf_core.dart'; +import '../models/pdf_viewer_view_model.dart'; +import '../widgets/pdf_sidebar.dart'; +import '../widgets/no_pdf_loaded_view.dart'; +import '../widgets/pdf_bottom_bar.dart'; + +// 매직 넘버 상수 선언 +const double kStatusBarHeight = 48; +const double kIconSize = 64; +const double kFontSize = 18; +const double kSpacingLarge = 16; +const double kSpacingSmall = 8; + +class PDFViewer extends StatefulWidget { + final String path; + final bool isAsset; + + const PDFViewer({super.key, required this.path, this.isAsset = true}); + + @override + State createState() => _PDFViewerState(); +} + +class _PDFViewerState extends State { + PdfViewerViewModel? _viewModel; + final PdfViewerController _pdfController = PdfViewerController(); + + @override + void initState() { + super.initState(); + + // Get PDFProvider from InheritedWidget after the first frame + WidgetsBinding.instance.addPostFrameCallback((_) { + final pdfProvider = InheritedPDFProviderWidget.of(context).provider; + setState(() { + _viewModel = PdfViewerViewModel(pdfProvider: pdfProvider); + }); + + // Load initial PDF + _viewModel!.loadPdf(path: widget.path, isAsset: widget.isAsset); + }); + + // Listen to PDF controller for page changes + _pdfController.addListener(_onPdfControllerChanged); + } + + void _onPdfControllerChanged() { + if (_pdfController.isReady) { + // Update current page in view model + final pageNumber = _pdfController.pageNumber ?? 1; + _viewModel?.updateCurrentPage(pageNumber); + + // Update zoom in view model + final zoomRatio = _pdfController.currentZoom; + _viewModel?.updateZoom(zoomRatio); + } + } + + void _onPageSelected(BasePage page) { + // Jump to the selected page (page.pageIndex is 0-based, but controller expects 1-based) + _pdfController.goToPage(pageNumber: page.pageIndex + 1); + } + + void _onLayoutSelected(BaseLayout layout) { + // Navigate to the page containing this figure + if (_viewModel != null && layout.type == LayoutType.figure) { + final pageNumber = _viewModel!.getPageNumberForFigure(layout); + if (pageNumber != null) { + _pdfController.goToPage(pageNumber: pageNumber); + } else { + // Show error message if figure page not found + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Unable to navigate to figure'), + duration: Duration(seconds: 2), + ), + ); + } + } + } + + @override + void dispose() { + _pdfController.removeListener(_onPdfControllerChanged); + _viewModel?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + // ViewModel null 체크 분리 + if (_viewModel == null) { + return const Scaffold(body: Center(child: CircularProgressIndicator())); + } + + return ListenableBuilder( + listenable: _viewModel!, + builder: (context, child) { + // 1. 로딩 상태 + if (_viewModel!.isLoading) { + return const Scaffold( + body: Center(child: CircularProgressIndicator()), + ); + } + // 2. PDF 미로딩 상태 + if (_viewModel!.errorMessage != null) { + return const Scaffold(body: NoPdfLoadedView()); + } + // 3. 정상 상태 + return Scaffold( + appBar: AppBar(title: Text(_viewModel!.pdfTitle), actions: []), + body: Row( + children: [ + Expanded( + child: Column( + children: [ + // Main PDF Viewer Area + Expanded( + child: PdfViewer.file( + widget.path, + controller: _pdfController, + params: PdfViewerParams( + backgroundColor: + Theme.of(context).colorScheme.surfaceContainer, + pageOverlaysBuilder: (context, pageRect, page) { + return _buildPageOverlays(context, pageRect, page); + }, + ), + ), + ), + // Bottom Status Bar + PDFBottomBar( + zoomPercent: _viewModel!.zoomPercent.toInt(), + currentPage: _viewModel!.currentPage, + totalPages: _viewModel!.pages.length, + sidebarVisible: _viewModel!.sidebarVisible, + onSidebarToggle: _viewModel!.toggleSidebar, + ), + ], + ), + ), + // Sidebar + AnimatedSwitcher( + duration: const Duration(milliseconds: 400), + transitionBuilder: (child, animation) { + final offsetAnimation = Tween( + begin: const Offset(1, 0), + end: Offset.zero, + ).animate( + CurvedAnimation(parent: animation, curve: Curves.easeOut), + ); + return SlideTransition( + position: offsetAnimation, + child: child, + ); + }, + child: + _viewModel!.sidebarVisible + ? PdfSidebar( + key: const ValueKey('sidebar'), + viewModel: _viewModel!, + onPageSelected: _onPageSelected, + onLayoutSelected: _onLayoutSelected, + ) + : const SizedBox.shrink(key: ValueKey('empty')), + ), + ], + ), + ); + }, + ); + } + + List _buildPageOverlays( + BuildContext context, + Rect pageRect, + PdfPage page, + ) { + // Get the current page index (PdfPage.pageNumber is 1-based) + final pageIndex = page.pageNumber - 1; + + // Check if we have view model + if (_viewModel == null) { + return []; + } + + // Get the corresponding PageModel from our model + if (pageIndex >= _viewModel!.pages.length) { + return []; + } + final pageModel = _viewModel!.pages.firstWhere( + (page) => page.pageIndex == pageIndex, + ); + + // Get figure references for this page + final figureReferences = _viewModel!.getFigureReferencesForPage(pageIndex); + + if (figureReferences.isEmpty) { + return []; + } + + // scaleX, scaleY 각각 적용 + final scaleX = pageRect.width / pageModel.width.toDouble(); + final scaleY = pageRect.height / pageModel.height.toDouble(); + + return figureReferences.map((reference) { + final rect = reference.rect; + + // Direct coordinate transformation without Y-axis flip + // The OCR coordinates appear to use top-left origin like Flutter + final left = (rect.left * scaleX); + final top = (rect.top * scaleY); + final width = rect.width * scaleX; + final height = rect.height * scaleY; + + return Positioned( + left: left, + top: top, + width: width, + height: height, + child: Builder( + builder: (innerContext) { + return GestureDetector( + onTap: () { + // Get the render box of this specific widget + final RenderBox renderBox = + innerContext.findRenderObject() as RenderBox; + // Calculate center position in local coordinates + final localCenter = Offset(width / 2, height / 2); + // Convert to global coordinates + final globalPosition = renderBox.localToGlobal(localCenter); + _showFigurePopup(context, reference, globalPosition); + }, + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: Theme.of( + context, + ).colorScheme.secondary.withOpacity(0.7), + width: 2, + ), + borderRadius: BorderRadius.circular(4), + color: Theme.of( + context, + ).colorScheme.secondary.withOpacity(0.1), + ), + ), + ); + }, + ), + ); + }).toList(); + } + + Future _showFigurePopup( + BuildContext context, + BaseLayout reference, + Offset globalPosition, + ) async { + // Find the corresponding figure + final figure = _viewModel?.findFigureByReference(reference); + if (figure == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Figure not found: ${reference.referenceText}'), + duration: const Duration(seconds: 2), + ), + ); + return; + } + + // Find which page contains the figure + final figurePage = _viewModel?.getPageForLayout(figure); + if (figurePage == null) return; + + // Show the figure in a custom overlay + final overlay = Overlay.of(context); + late OverlayEntry overlayEntry; + + overlayEntry = OverlayEntry( + builder: + (context) => _FigurePopupOverlay( + figure: figure, + figurePage: figurePage, + targetPosition: globalPosition, + onClose: () => overlayEntry.remove(), + renderFigure: _renderFigure, + ), + ); + + overlay.insert(overlayEntry); + } + + Future _renderFigure(int pageIndex, BaseLayout figure) async { + try { + // Load the PDF document + final document = await PdfDocument.openFile(widget.path); + + // Get the specific page (pageIndex is 0-based) + final page = document.pages[pageIndex]; + + // Get page dimensions from the PageModel (OCR coordinates) + final pageModel = _viewModel!.pages.firstWhere( + (p) => p.pageIndex == pageIndex, + ); + final ocrPageWidth = pageModel.width.toDouble(); + final ocrPageHeight = pageModel.height.toDouble(); + + // Get the figure's bounding box (in OCR coordinates) + final figureRect = figure.rect; + + // Calculate scale to render at a reasonable resolution + final targetWidth = 1000.0; // Target width for full page render + final scale = targetWidth / ocrPageWidth; + + // Calculate the scaled dimensions + final scaledPageWidth = (ocrPageWidth * scale); + final scaledPageHeight = (ocrPageHeight * scale); + + // Calculate the figure position and size in the scaled coordinates + final scaledX = (figureRect.left * scale).toInt(); + final scaledY = (figureRect.top * scale).toInt(); + final scaledWidth = (figureRect.width * scale).toInt(); + final scaledHeight = (figureRect.height * scale).toInt(); + + // Render the sub-area of the page + final renderResult = await page.render( + x: scaledX, + y: scaledY, + width: scaledWidth, + height: scaledHeight, + fullWidth: scaledPageWidth, + fullHeight: scaledPageHeight, + ); + + if (renderResult == null) { + await document.dispose(); + return const Text('Failed to render page'); + } + + final image = await renderResult.createImage(); + + // Clean up + renderResult.dispose(); + await document.dispose(); + + if (image != null) { + return InteractiveViewer( + panEnabled: true, + minScale: 0.5, + maxScale: 4.0, + child: RawImage(image: image, fit: BoxFit.contain), + ); + } + + return const Text('Failed to create image'); + } catch (e) { + return Text('Error: $e'); + } + } +} + +// Custom overlay widget for the figure popup with pin +class _FigurePopupOverlay extends StatefulWidget { + final BaseLayout figure; + final BasePage figurePage; + final Offset targetPosition; + final VoidCallback onClose; + final Future Function(int, BaseLayout) renderFigure; + + const _FigurePopupOverlay({ + required this.figure, + required this.figurePage, + required this.targetPosition, + required this.onClose, + required this.renderFigure, + }); + + @override + State<_FigurePopupOverlay> createState() => _FigurePopupOverlayState(); +} + +class _FigurePopupOverlayState extends State<_FigurePopupOverlay> + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _scaleAnimation; + late Animation _fadeAnimation; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 200), + vsync: this, + ); + + _scaleAnimation = Tween(begin: 0.7, end: 1.0).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeOutBack), + ); + + _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeOut), + ); + + _animationController.forward(); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + Size _calculatePopupSize(Size screenSize, BaseLayout figure) { + // Get figure dimensions from the rect + final figureRect = figure.rect; + final figureWidth = figureRect.width; + final figureHeight = figureRect.height; + final aspectRatio = figureWidth / figureHeight; + + // Define constraints + const minPopupWidth = 300.0; + const minPopupHeight = 200.0; + const headerHeight = 64.0; // Approximate header height + const padding = 32.0; // Total padding around content + const margin = 40.0; // Margin from screen edges + + // Calculate maximum available space + final maxPopupWidth = screenSize.width - (margin * 2); + final maxPopupHeight = screenSize.height - (margin * 2); + final maxContentHeight = maxPopupHeight - headerHeight - padding; + + // Calculate optimal size based on aspect ratio + double popupWidth; + double popupHeight; + + if (aspectRatio > 1) { + // Landscape figure - prioritize width + popupWidth = (figureWidth * 0.8).clamp(minPopupWidth, maxPopupWidth); + final contentHeight = (popupWidth - padding) / aspectRatio; + popupHeight = (contentHeight + headerHeight + padding).clamp( + minPopupHeight, + maxPopupHeight, + ); + + // Adjust width if height constraint is hit + if (popupHeight == maxPopupHeight) { + final adjustedContentHeight = maxContentHeight; + popupWidth = (adjustedContentHeight * aspectRatio + padding).clamp( + minPopupWidth, + maxPopupWidth, + ); + } + } else { + // Portrait or square figure - prioritize height + final contentHeight = (figureHeight * 0.8).clamp( + minPopupHeight - headerHeight - padding, + maxContentHeight, + ); + popupHeight = contentHeight + headerHeight + padding; + popupWidth = ((contentHeight * aspectRatio) + padding).clamp( + minPopupWidth, + maxPopupWidth, + ); + } + + return Size(popupWidth, popupHeight); + } + + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + final theme = Theme.of(context); + + // Calculate popup size based on figure dimensions + final popupSize = _calculatePopupSize(screenSize, widget.figure); + final popupWidth = popupSize.width; + final popupHeight = popupSize.height; + const pinHeight = 20.0; + const margin = 20.0; + + // Determine if popup should appear above or below the target + final showAbove = widget.targetPosition.dy > screenSize.height / 2; + + // Calculate popup position + double popupLeft = widget.targetPosition.dx - popupWidth / 2; + double popupTop = + showAbove + ? widget.targetPosition.dy - popupHeight - pinHeight - margin + : widget.targetPosition.dy + margin; + + // Ensure popup stays within screen bounds + popupLeft = popupLeft.clamp(margin, screenSize.width - popupWidth - margin); + + // For vertical positioning, ensure popup fits on screen + final totalPopupHeight = popupHeight + pinHeight; + if (showAbove) { + // Check if there's enough space above + final minTopForAbove = margin; + final maxTopForAbove = + widget.targetPosition.dy - totalPopupHeight - margin; + + if (maxTopForAbove < minTopForAbove) { + // Not enough space above, switch to below + popupTop = widget.targetPosition.dy + margin; + } else { + popupTop = maxTopForAbove; + } + } else { + // Check if there's enough space below + final maxTopForBelow = screenSize.height - totalPopupHeight - margin; + + if (widget.targetPosition.dy + margin > maxTopForBelow) { + // Not enough space below, switch to above or limit height + popupTop = widget.targetPosition.dy - totalPopupHeight - margin; + if (popupTop < margin) { + // Force to fit on screen by positioning at top margin + popupTop = margin; + } + } else { + popupTop = widget.targetPosition.dy + margin; + } + } + + // Final clamp to ensure popup stays within screen bounds + popupTop = popupTop.clamp( + margin, + screenSize.height - totalPopupHeight - margin, + ); + + // Calculate pin position relative to popup + final pinLeft = + widget.targetPosition.dx - popupLeft - 10; // 10 is half of pin width + + return AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return Stack( + children: [ + // Dimmed background + Positioned.fill( + child: GestureDetector( + onTap: () { + _animationController.reverse().then((_) => widget.onClose()); + }, + child: Container( + color: Colors.black.withOpacity(0.3 * _fadeAnimation.value), + ), + ), + ), + // Popup with pin + Positioned( + left: popupLeft, + top: popupTop, + child: Transform.scale( + scale: _scaleAnimation.value, + alignment: + showAbove ? Alignment.bottomCenter : Alignment.topCenter, + child: Opacity( + opacity: _fadeAnimation.value, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (!showAbove) _buildPin(theme, pinLeft, popupWidth), + Container( + width: popupWidth, + height: popupHeight, + decoration: BoxDecoration( + color: theme.colorScheme.surface, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 20, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + children: [ + // Header + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceVariant, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(12), + ), + ), + child: Row( + children: [ + Expanded( + child: Text( + widget.figure.caption ?? + 'Figure ${widget.figure.figureNumber}', + style: theme.textTheme.titleMedium, + overflow: TextOverflow.ellipsis, + ), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () { + _animationController.reverse().then( + (_) => widget.onClose(), + ); + }, + ), + ], + ), + ), + // Figure content + Expanded( + child: Padding( + padding: const EdgeInsets.all(16), + child: FutureBuilder( + future: widget.renderFigure( + widget.figurePage.pageIndex, + widget.figure, + ), + builder: (context, snapshot) { + if (snapshot.connectionState == + ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator(), + ); + } + if (snapshot.hasError) { + return Center( + child: Text('Error: ${snapshot.error}'), + ); + } + return snapshot.data ?? + const Text('Figure not available'); + }, + ), + ), + ), + ], + ), + ), + if (showAbove) _buildPin(theme, pinLeft, popupWidth), + ], + ), + ), + ), + ), + ], + ); + }, + ); + } + + Widget _buildPin(ThemeData theme, double pinLeft, double popupWidth) { + return SizedBox( + width: popupWidth, + height: 20, + child: CustomPaint( + painter: _PinPainter( + color: theme.colorScheme.surface, + shadowColor: Colors.black.withOpacity(0.2), + pinPosition: pinLeft, + ), + ), + ); + } +} + +// Custom painter for the pin/arrow +class _PinPainter extends CustomPainter { + final Color color; + final Color shadowColor; + final double pinPosition; + + _PinPainter({ + required this.color, + required this.shadowColor, + required this.pinPosition, + }); + + @override + void paint(Canvas canvas, Size size) { + final paint = + Paint() + ..color = color + ..style = PaintingStyle.fill; + + final shadowPaint = + Paint() + ..color = shadowColor + ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4); + + // Create pin path + final path = Path(); + const pinWidth = 20.0; + const pinHeight = 20.0; + + // Clamp pin position to ensure it stays within bounds + final clampedPosition = pinPosition.clamp( + 10.0, + size.width - pinWidth - 10.0, + ); + + path.moveTo(clampedPosition, 0); + path.lineTo(clampedPosition + pinWidth, 0); + path.lineTo(clampedPosition + pinWidth / 2, pinHeight); + path.close(); + + // Draw shadow + canvas.drawPath(path, shadowPaint); + // Draw pin + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} diff --git a/lib/features/pdf_viewer/widgets/document_card.dart b/lib/features/pdf_viewer/widgets/document_card.dart new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/lib/features/pdf_viewer/widgets/document_card.dart @@ -0,0 +1 @@ + diff --git a/lib/features/pdf_viewer/widgets/no_pdf_loaded_view.dart b/lib/features/pdf_viewer/widgets/no_pdf_loaded_view.dart new file mode 100644 index 0000000..e6359a0 --- /dev/null +++ b/lib/features/pdf_viewer/widgets/no_pdf_loaded_view.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +const double kIconSize = 64; +const double kFontSize = 18; +const double kSpacingLarge = 16; +const double kSpacingSmall = 8; + +class NoPdfLoadedView extends StatelessWidget { + const NoPdfLoadedView({super.key}); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.picture_as_pdf, size: kIconSize, color: Colors.grey), + const SizedBox(height: kSpacingLarge), + const Text( + 'No PDF loaded', + style: TextStyle(fontSize: kFontSize, color: Colors.grey), + ), + const SizedBox(height: kSpacingSmall), + const Text( + 'Open a PDF file to get started', + style: TextStyle(color: Colors.grey), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text('Close'), + ), + ], + ), + ); + } +} diff --git a/lib/features/pdf_viewer/widgets/pdf_bottom_bar.dart b/lib/features/pdf_viewer/widgets/pdf_bottom_bar.dart new file mode 100644 index 0000000..f1e0f32 --- /dev/null +++ b/lib/features/pdf_viewer/widgets/pdf_bottom_bar.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; + +const double kStatusBarHeight = 48; + +class PDFBottomBar extends StatelessWidget { + final int zoomPercent; + final int currentPage; + final int totalPages; + final bool sidebarVisible; + final VoidCallback onSidebarToggle; + const PDFBottomBar({ + super.key, + required this.zoomPercent, + required this.currentPage, + required this.totalPages, + required this.sidebarVisible, + required this.onSidebarToggle, + }); + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Container( + height: kStatusBarHeight, + color: Theme.of(context).colorScheme.surfaceContainer, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '$zoomPercent%', + style: Theme.of(context).textTheme.bodyMedium, + ), + Text( + '$currentPage / $totalPages', + style: Theme.of(context).textTheme.bodyLarge, + ), + IconButton( + icon: const Icon(Icons.search), + tooltip: sidebarVisible ? 'Hide sidebar' : 'Show sidebar', + onPressed: onSidebarToggle, + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/pdf_viewer/widgets/pdf_page_viewer.dart b/lib/features/pdf_viewer/widgets/pdf_page_viewer.dart index 6367bde..f0f182c 100644 --- a/lib/features/pdf_viewer/widgets/pdf_page_viewer.dart +++ b/lib/features/pdf_viewer/widgets/pdf_page_viewer.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:pdfrx/pdfrx.dart'; import 'package:vector_math/vector_math_64.dart' as vm; +import '../../../shared/services/pdf_core/pdf_core.dart'; /// PDF 문서를 화면에 렌더링하고 /// - 수직 스크롤 @@ -13,12 +14,14 @@ class PdfPageViewer extends StatefulWidget { final PdfDocument document; final ValueChanged? onScaleChanged; final ValueChanged? onPageChanged; + final List? pages; const PdfPageViewer({ super.key, required this.document, this.onScaleChanged, this.onPageChanged, + this.pages, }); static PdfPageViewerState? of(BuildContext context) => @@ -45,14 +48,14 @@ class PdfPageViewerState extends State { // 페이지 컨트롤러: 페이지 변화 콜백 등록 _pageController = PageController(initialPage: 1) // 초기 페이지 값 설정 - ..addListener(() { - final page = (_pageController.page ?? 1).round(); // 페이지 목록의 첫번째 페이지 값 설정 - widget.onPageChanged?.call(page); - }); + ..addListener(() { + final page = (_pageController.page ?? 1).round(); // 페이지 목록의 첫번째 페이지 값 설정 + widget.onPageChanged?.call(page); + }); // TransformationController 생성 후 리스너 등록 - _transformationController = TransformationController() - ..addListener(_onTransformChanged); + _transformationController = + TransformationController()..addListener(_onTransformChanged); _lastTranslate = vm.Vector3.zero(); } @@ -86,9 +89,10 @@ class PdfPageViewerState extends State { final newScale = (_currentScale + 0.5).clamp(_minScale, _maxScale); setState(() { _currentScale = newScale; - _transformationController.value = Matrix4.identity() - ..translate(_lastTranslate.x, _lastTranslate.y) - ..scale(_currentScale); + _transformationController.value = + Matrix4.identity() + ..translate(_lastTranslate.x, _lastTranslate.y) + ..scale(_currentScale); widget.onScaleChanged?.call(_currentScale); }); } @@ -98,9 +102,10 @@ class PdfPageViewerState extends State { final newScale = (_currentScale - 0.5).clamp(_minScale, _maxScale); setState(() { _currentScale = newScale; - _transformationController.value = Matrix4.identity() - ..translate(_lastTranslate.x, _lastTranslate.y) - ..scale(_currentScale); + _transformationController.value = + Matrix4.identity() + ..translate(_lastTranslate.x, _lastTranslate.y) + ..scale(_currentScale); widget.onScaleChanged?.call(_currentScale); }); } @@ -108,7 +113,7 @@ class PdfPageViewerState extends State { void _resetAlignment() { setState(() { _transformationController.value = - Matrix4.identity()..scale(_currentScale); + Matrix4.identity()..scale(_currentScale); widget.onScaleChanged?.call(_currentScale); }); } @@ -131,87 +136,40 @@ class PdfPageViewerState extends State { Widget build(BuildContext context) { final pageCount = widget.document.pages.length; - return Stack( - children: [ - PageView.builder( - controller: _pageController, - scrollDirection: Axis.vertical, - itemCount: pageCount, - itemBuilder: (context, index) { - final page = widget.document.pages[index]; - return LayoutBuilder( - builder: (context, constraints) { - return InteractiveViewer( - constrained: false, - transformationController: _transformationController, - panEnabled: true, - scaleEnabled: true, - minScale: _minScale, - maxScale: _maxScale, - child: SizedBox( - width: constraints.maxWidth, - height: constraints.maxHeight, - child: FittedBox( - fit: _fitWidth - ? BoxFit.fitWidth - : _fitHeight - ? BoxFit.fitHeight - : BoxFit.contain, - alignment: Alignment.center, - child: SizedBox( - width: page.width, - height: page.height, - child: PdfPageView( - document: widget.document, - pageNumber: index, - ), - ), - ), - ), - ); - }, + if (pageCount == 0) { + return const Center(child: Text('No pages found in document')); + } + + return PageView.builder( + controller: _pageController, + scrollDirection: Axis.vertical, + itemCount: pageCount, + itemBuilder: (context, index) { + if (index >= widget.document.pages.length) { + return const Center(child: Text('Page index out of bounds')); + } + return LayoutBuilder( + builder: (context, constraints) { + return InteractiveViewer( + transformationController: _transformationController, + panEnabled: true, + scaleEnabled: true, + minScale: _minScale, + maxScale: _maxScale, + child: SizedBox( + width: constraints.maxWidth, + height: constraints.maxHeight, + child: PdfPageView( + backgroundColor: + Theme.of(context).colorScheme.surfaceContainer, + document: widget.document, + pageNumber: index + 1, + ), + ), ); }, - ), - - Positioned( - top: 16, - right: 16, - child: Column( - children: [ - FloatingActionButton( - onPressed: _zoomIn, - mini: true, - child: const Icon(Icons.zoom_in), - ), - const SizedBox(height: 8), - FloatingActionButton( - onPressed: _zoomOut, - mini: true, - child: const Icon(Icons.zoom_out), - ), - const SizedBox(height: 8), - FloatingActionButton( - onPressed: _resetAlignment, - mini: true, - child: const Icon(Icons.center_focus_strong), - ), - const SizedBox(height: 8), - FloatingActionButton( - onPressed: _toggleFitWidth, - mini: true, - child: const Icon(Icons.swap_horiz), - ), - const SizedBox(height: 8), - FloatingActionButton( - onPressed: _toggleFitHeight, - mini: true, - child: const Icon(Icons.swap_vert), - ), - ], - ), - ), - ], + ); + }, ); } -} \ No newline at end of file +} diff --git a/lib/features/pdf_viewer/widgets/pdf_sidebar.dart b/lib/features/pdf_viewer/widgets/pdf_sidebar.dart new file mode 100644 index 0000000..e047969 --- /dev/null +++ b/lib/features/pdf_viewer/widgets/pdf_sidebar.dart @@ -0,0 +1,193 @@ +import 'package:flutter/material.dart'; +import '../../../shared/services/pdf_core/pdf_core.dart'; +import '../models/pdf_viewer_view_model.dart'; + +class PdfSidebar extends StatelessWidget { + final PdfViewerViewModel viewModel; + final Function(BasePage) onPageSelected; + final Function(BaseLayout) onLayoutSelected; + + const PdfSidebar({ + super.key, + required this.viewModel, + required this.onPageSelected, + required this.onLayoutSelected, + }); + + @override + Widget build(BuildContext context) { + return Container( + width: 300, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + border: Border(left: BorderSide(color: Theme.of(context).dividerColor)), + ), + child: Column( + children: [ + // Tab Toggle + _buildTabToggle(context), + + // Content List + Expanded( + child: + viewModel.selectedTabIndex == 0 + ? _PageListView( + pages: viewModel.filteredPages, + onPageSelected: onPageSelected, + ) + : _LayoutListView( + layouts: viewModel.filteredLayouts, + onLayoutSelected: onLayoutSelected, + ), + ), + + // Search Bar + _buildSearchBar(context), + ], + ), + ); + } + + Widget _buildTabToggle(BuildContext context) { + return ToggleButtons( + isSelected: [ + viewModel.selectedTabIndex == 0, + viewModel.selectedTabIndex == 1, + ], + onPressed: viewModel.selectTab, + borderRadius: BorderRadius.circular(20), + selectedBorderColor: Theme.of(context).colorScheme.onSecondaryContainer, + fillColor: Theme.of(context).colorScheme.primary.withValues(alpha: 0.1), + selectedColor: Theme.of(context).colorScheme.onSecondaryContainer, + color: Theme.of(context).colorScheme.onSurface, + constraints: const BoxConstraints(minWidth: 100, minHeight: 36), + children: [ + Text('Page', style: Theme.of(context).textTheme.bodyMedium), + Text('Figure', style: Theme.of(context).textTheme.bodyMedium), + ], + ); + } + + Widget _buildSearchBar(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + decoration: InputDecoration( + hintText: + viewModel.selectedTabIndex == 0 ? 'Search Page' : 'Search Figure', + prefixIcon: const Icon(Icons.search), + suffixIcon: + viewModel.searchQuery.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear), + onPressed: viewModel.clearSearch, + ) + : null, + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), + isDense: true, + contentPadding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 12, + ), + ), + onChanged: viewModel.updateSearchQuery, + ), + ); + } +} + +class _PageListView extends StatelessWidget { + final List pages; + final Function(BasePage) onPageSelected; + + const _PageListView({required this.pages, required this.onPageSelected}); + + @override + Widget build(BuildContext context) { + return ListView.builder( + itemCount: pages.length, + itemBuilder: (context, index) { + final page = pages[index]; + return ListTile( + leading: _buildPageThumbnail(page, context: context), + title: Text('Page ${page.pageIndex}'), + trailing: const Icon(Icons.chevron_right), + onTap: () => onPageSelected(page), + ); + }, + ); + } + + Widget _buildPageThumbnail(BasePage page, {required BuildContext context}) { + // For now, use a placeholder. In a real implementation, + // you would render the page thumbnail using pdfrx + return Container( + width: 45, + height: 45, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(15), + ), + child: const Icon(Icons.picture_as_pdf, color: Colors.grey), + ); + } +} + +class _LayoutListView extends StatelessWidget { + final List layouts; + final Function(BaseLayout) onLayoutSelected; + + const _LayoutListView({ + required this.layouts, + required this.onLayoutSelected, + }); + + @override + Widget build(BuildContext context) { + final figures = + layouts.where((layout) => layout.type == LayoutType.figure).toList(); + return ListView.builder( + itemCount: figures.length, + itemBuilder: (context, index) { + final layout = figures[index]; + return ListTile( + leading: _buildLayoutThumbnail(layout, context: context), + title: Text(layout.figureId ?? 'Figure ${index + 1}'), + trailing: const Icon(Icons.chevron_right), + onTap: () => onLayoutSelected(layout), + ); + }, + ); + } + + Widget _buildLayoutThumbnail( + BaseLayout layout, { + required BuildContext context, + }) { + return Container( + width: 45, + height: 45, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondaryContainer, + borderRadius: BorderRadius.circular(15), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.image, color: Colors.grey[700]), + if (layout.type == LayoutType.figure && layout.figureNumber != null) + Text( + 'Fig ${layout.figureNumber}', + style: const TextStyle(fontSize: 10, color: Colors.black54), + ), + if (layout.type == LayoutType.figureReference && + layout.figureNumber != null) + Text( + 'Ref ${layout.figureNumber}', + style: const TextStyle(fontSize: 10, color: Colors.black54), + ), + ], + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 61c10a6..a00d735 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,7 @@ // lib/main.dart +// lib/main.dart + import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'package:snapfig/features/home/screens/home_widget.dart'; @@ -8,18 +10,13 @@ import 'package:snapfig/shared/services/ocr_core/ocr_core.dart'; import 'package:snapfig/shared/services/pdf_core/pdf_core.dart'; import 'core/theme/theme.dart'; -class _DummyOCRProvider extends OCRProvider { - @override - Future process(String imagePath) async { - return OCRResult(); - } -} - void main() async { WidgetsFlutterBinding.ensureInitialized(); final dir = await getApplicationCacheDirectory(); final pdfProvider = await PDFProviderImpl.load( - ocrProvider: _DummyOCRProvider(), + ocrProvider: OCRProviderImpl( + baseUrl: 'https://901b-39-115-116-188.ngrok-free.app', + ), dbPath: dir.path, ); runApp(SnapfigApp(pdfProvider: pdfProvider)); @@ -34,6 +31,15 @@ class SnapfigApp extends StatelessWidget { @override Widget build(BuildContext context) { + return InheritedPDFProviderWidget( + provider: _pdfProvider, + child: MaterialApp( + title: 'Snapfig', + theme: lightTheme, + darkTheme: darkTheme, + navigatorKey: _navigationService.navigatorKey, + home: const HomeWidget(), + ), return InheritedPDFProviderWidget( provider: _pdfProvider, child: MaterialApp( diff --git a/lib/shared/services/navigation_service/navigation_view_model.dart b/lib/shared/services/navigation_service/navigation_view_model.dart index 0ce842f..a991ce3 100644 --- a/lib/shared/services/navigation_service/navigation_view_model.dart +++ b/lib/shared/services/navigation_service/navigation_view_model.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:snapfig/features/pdf_viewer/screens/pdf_viewer_screen.dart'; +import 'package:snapfig/features/pdf_viewer/screens/pdf_viewer.dart'; import 'package:snapfig/shared/services/navigation_service/navigation_service.dart'; /// ViewModel for handling navigation in the MVVM pattern. @@ -16,7 +16,7 @@ class NavigationViewModel extends ChangeNotifier { /// [isAsset] Whether the PDF is an asset file (default: false) void navigateToPdfViewer({required String path, bool isAsset = false}) { _navigationService.navigateToWidget( - PdfViewerScreen(path: path, isAsset: isAsset), + PDFViewer(path: path, isAsset: isAsset), ); } diff --git a/lib/shared/services/ocr_core/ocr_core.dart b/lib/shared/services/ocr_core/ocr_core.dart index c546d98..1801a5e 100644 --- a/lib/shared/services/ocr_core/ocr_core.dart +++ b/lib/shared/services/ocr_core/ocr_core.dart @@ -1,2 +1,3 @@ export 'ocr_provider.dart'; +export 'ocr_provider_impl.dart'; export 'ocr_result.dart'; diff --git a/lib/shared/services/ocr_core/ocr_provider_impl.dart b/lib/shared/services/ocr_core/ocr_provider_impl.dart new file mode 100644 index 0000000..8030000 --- /dev/null +++ b/lib/shared/services/ocr_core/ocr_provider_impl.dart @@ -0,0 +1,90 @@ +// lib/shared/services/ocr_core/ocr_provider_impl.dart + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter_client_sse/flutter_client_sse.dart'; +import 'package:flutter_client_sse/constants/sse_request_type_enum.dart'; +import 'package:http/http.dart' as http; +import 'package:snapfig/shared/services/ocr_core/ocr_provider.dart'; +import 'package:snapfig/shared/services/ocr_core/ocr_result.dart'; + +import 'package:path/path.dart' as path_lib; + +class OCRProviderImpl extends OCRProvider { + final String baseUrl; + final http.Client httpClient; + + OCRProviderImpl({required this.baseUrl, http.Client? httpClient}) + : httpClient = httpClient ?? http.Client(); + + @override + Future process(String pdfPath) async { + // 1) 분석 요청 + final analyzeUri = Uri.parse('$baseUrl/analyze/pdf?format=frontend'); + final pdfBytes = await File(pdfPath).readAsBytes(); + final request = http.MultipartRequest('POST', analyzeUri) + ..files.add( + http.MultipartFile.fromBytes( + 'file', + pdfBytes, + filename: path_lib.basename(pdfPath), + ), + ); + final response = await httpClient.send(request); + final analyzeRes = await http.Response.fromStream(response); + if (analyzeRes.statusCode != 202) { + throw Exception('OCR API 요청 실패: HTTP ${analyzeRes.statusCode}'); + } + + // 2) task_id, stream_url 파싱 + final bodyJson = json.decode(analyzeRes.body) as Map; + final taskId = bodyJson['task_id'] as String; + final streamUrl = '$baseUrl/analyze/stream/$taskId'; + + // 3) SSE 구독: analyze/stream/{taskId} + final completer = Completer(); + // 먼저 선언 + late StreamSubscription subscription; + // 나중에 assign + subscription = SSEClient.subscribeToSSE( + method: SSERequestType.GET, + url: streamUrl, + header: { + HttpHeaders.acceptHeader: 'text/event-stream', + 'Cache-Control': 'no-cache', + }, + ).listen( + (SSEModel event) { + try { + final data = json.decode(event.data ?? '{}') as Map; + if (data['status'] == 'completed') { + subscription.cancel(); + if (!completer.isCompleted) completer.complete(); + } + } catch (_) {} + }, + onError: (err) { + subscription.cancel(); + if (!completer.isCompleted) completer.completeError(err); + }, + ); + // 완료를 기다림 + await completer.future; + + // 4) 결과 조회: GET /result/{taskId} + final resultUri = Uri.parse('$baseUrl/result/$taskId'); + final resultReq = http.Request('GET', resultUri); + resultReq.headers[HttpHeaders.acceptHeader] = 'application/json'; + final streamedResult = await httpClient.send(resultReq); + final resultRes = await http.Response.fromStream(streamedResult); + if (resultRes.statusCode != 200) { + throw Exception('결과 조회 실패: HTTP ${resultRes.statusCode}'); + } + + // 5) JSON → 모델 매핑 + final resultJson = json.decode(resultRes.body) as Map; + return OCRResult.fromJson(resultJson); + } +} diff --git a/lib/shared/services/ocr_core/ocr_result.dart b/lib/shared/services/ocr_core/ocr_result.dart index 33fa233..8e95c50 100644 --- a/lib/shared/services/ocr_core/ocr_result.dart +++ b/lib/shared/services/ocr_core/ocr_result.dart @@ -1,4 +1,288 @@ -/// OCR 결과 모델 (책임 분리) +// lib/shared/services/ocr_core/ocr_result.dart + +/// OCR 분석 결과 모델 (프론트엔드용) class OCRResult { - // 실제 OCR 결과 구조에 맞게 구현 + /// 문서 제목 (null 이거나 누락되면 빈 문자열) + final String title; + + /// 챕터 리스트 + final List chapters; + + /// 페이지별 상세 + final List pages; + + /// 본문 중 Figure 객체 + final List
figures; + + /// 전체 메타데이터 + final Metadata metadata; + + OCRResult({ + required this.title, + required this.chapters, + required this.pages, + required this.figures, + required this.metadata, + }); + + factory OCRResult.fromJson(Map json) { + // 1) title + final rawTitle = json['title']; + final title = rawTitle is String ? rawTitle : ''; + + // 2) chapters + final chaptersJson = json['chapters'] as List? ?? []; + final chapters = + chaptersJson + .map((e) => Chapter.fromJson(e as Map)) + .toList(); + + // 3) pages + final pagesJson = json['pages'] as List? ?? []; + final pages = + pagesJson + .map((e) => PageDetail.fromJson(e as Map)) + .toList(); + + // 4) figures + final figsJson = json['figures'] as List? ?? []; + final figures = + figsJson + .map((e) => Figure.fromJson(e as Map)) + .toList(); + + // 5) metadata + final metaJson = json['metadata'] as Map? ?? {}; + final metadata = Metadata.fromJson(metaJson); + + return OCRResult( + title: title, + chapters: chapters, + pages: pages, + figures: figures, + metadata: metadata, + ); + } +} + +/// 챕터 정보 +class Chapter { + final int chapterNumber; + final String title; + final int startPage; + final int endPage; + final List sections; + + Chapter({ + required this.chapterNumber, + required this.title, + required this.startPage, + required this.endPage, + required this.sections, + }); + + factory Chapter.fromJson(Map json) { + return Chapter( + chapterNumber: (json['chapter_number'] as num).toInt(), + title: (json['title'] as String?) ?? '', + startPage: (json['start_page'] as num).toInt(), + endPage: (json['end_page'] as num).toInt(), + sections: + (json['sections'] as List?) + ?.map((e) => e as String) + .toList() ?? + [], + ); + } +} + +/// 페이지 상세 정보 +class PageDetail { + final int pageIndex; + final String? chapter; + final String? section; + final List layouts; + final String fullText; + final int width; + final int height; + + PageDetail({ + required this.pageIndex, + this.chapter, + this.section, + required this.layouts, + required this.fullText, + required this.width, + required this.height, + }); + + factory PageDetail.fromJson(Map json) { + // layouts + final layoutsJson = json['layouts'] as List? ?? []; + final layouts = + layoutsJson + .map((e) => LayoutItem.fromJson(e as Map)) + .toList(); + final sizeJson = json['size'] as Map? ?? {}; + + return PageDetail( + pageIndex: (json['page_index'] as num).toInt(), + chapter: json['chapter'] as String?, + section: json['section'] as String?, + layouts: layouts, + fullText: (json['full_text'] as String?) ?? '', + width: ((sizeJson['width'] ?? 0) as num).toInt(), + height: ((sizeJson['height'] ?? 0) as num).toInt(), + ); + } +} + +/// Figure 정보 (예시에선 빈 배열이지만 구조를 맞춰 파싱) +class Figure { + final String id; + final List boundingBox; + final String? caption; + final int? pageIndex; + + Figure({ + required this.id, + required this.boundingBox, + this.caption, + this.pageIndex, + }); + + factory Figure.fromJson(Map json) { + return Figure( + id: json['figure_id'] as String? ?? '', + boundingBox: + (json['bounding_box'] as List? ?? []) + .map((e) => (e as num).toDouble()) + .toList(), + caption: json['caption'] as String?, + pageIndex: + json['page_index'] != null + ? (json['page_index'] as num).toInt() + : null, + ); + } +} + +/// 레이아웃 추상 클래스 +abstract class LayoutItem { + final String type; + final List boundingBox; + + LayoutItem({required this.type, required this.boundingBox}); + + factory LayoutItem.fromJson(Map json) { + switch (json['type'] as String) { + case 'text': + return TextLayout.fromJson(json); + case 'figure': + return FigureLayoutItem.fromJson(json); + case 'figure_reference': + return FigureReferenceLayout.fromJson(json); + default: + throw UnimplementedError('Unknown layout type: ${json['type']}'); + } + } +} + +/// 텍스트 레이아웃 +class TextLayout extends LayoutItem { + final String text; + final double confidence; + + TextLayout({ + required super.boundingBox, + required this.text, + required this.confidence, + }) : super(type: 'text'); + + factory TextLayout.fromJson(Map json) { + return TextLayout( + boundingBox: + (json['bounding_box'] as List) + .map((e) => (e as num).toDouble()) + .toList(), + text: json['text'] as String? ?? '', + confidence: ((json['confidence'] ?? 0) as num).toDouble(), + ); + } +} + +/// Figure 레이아웃 (본문 내 그림 박스) +class FigureLayoutItem extends LayoutItem { + final String figureId; + final String caption; + final int figureNumber; + + FigureLayoutItem({ + required super.boundingBox, + required this.figureId, + required this.caption, + required this.figureNumber, + }) : super(type: 'figure'); + + factory FigureLayoutItem.fromJson(Map json) { + return FigureLayoutItem( + boundingBox: + (json['bounding_box'] as List) + .map((e) => (e as num).toDouble()) + .toList(), + figureId: (json['figure_id'] ?? '') as String, + caption: (json['caption'] ?? '') as String, + figureNumber: ((json['figure_number'] ?? 0) as num).toInt(), + ); + } +} + +/// Figure 참조 레이아웃 (본문 내 그림 언급) +class FigureReferenceLayout extends LayoutItem { + final String referencedFigureId; + final String referenceText; + final int figureNumber; + final double confidence; + + FigureReferenceLayout({ + required super.boundingBox, + required this.referencedFigureId, + required this.referenceText, + required this.figureNumber, + required this.confidence, + }) : super(type: 'figure_reference'); + + factory FigureReferenceLayout.fromJson(Map json) { + return FigureReferenceLayout( + boundingBox: + (json['bounding_box'] as List) + .map((e) => (e as num).toDouble()) + .toList(), + referencedFigureId: json['referenced_figure_id'] as String, + referenceText: json['reference_text'] as String, + figureNumber: (json['figure_number'] as num).toInt(), + confidence: ((json['confidence'] ?? 0) as num).toDouble(), + ); + } +} + +/// 메타데이터 +class Metadata { + final int totalPages; + final double processingTime; + final int totalFigures; + + Metadata({ + required this.totalPages, + required this.processingTime, + required this.totalFigures, + }); + + factory Metadata.fromJson(Map json) { + return Metadata( + totalPages: (json['total_pages'] as num).toInt(), + processingTime: (json['processing_time'] as num).toDouble(), + totalFigures: (json['total_figures'] as num).toInt(), + ); + } } diff --git a/lib/shared/services/pdf_core/models/implement/layout_impl.dart b/lib/shared/services/pdf_core/models/implement/layout_impl.dart index c44ac39..60a4daf 100644 --- a/lib/shared/services/pdf_core/models/implement/layout_impl.dart +++ b/lib/shared/services/pdf_core/models/implement/layout_impl.dart @@ -27,6 +27,18 @@ class LayoutModel extends BaseLayout { final double _width; final double _height; + // Figure 관련 필드들 + @override + final String? figureId; + @override + final int? figureNumber; + @override + final String? caption; + @override + final String? referencedFigureId; + @override + final String? referenceText; + // ====== 관계 ====== IsarLink page = IsarLink(); @@ -36,6 +48,11 @@ class LayoutModel extends BaseLayout { required this.content, this.text, this.latex, + this.figureId, + this.figureNumber, + this.caption, + this.referencedFigureId, + this.referenceText, required double top, required double left, required double width, @@ -50,6 +67,11 @@ class LayoutModel extends BaseLayout { required this.content, required this.text, required this.latex, + this.figureId, + this.figureNumber, + this.caption, + this.referencedFigureId, + this.referenceText, required Rect box, }) : _top = box.top, _left = box.left, diff --git a/lib/shared/services/pdf_core/models/implement/layout_impl.g.dart b/lib/shared/services/pdf_core/models/implement/layout_impl.g.dart index ca98cb9..451bb8a 100644 --- a/lib/shared/services/pdf_core/models/implement/layout_impl.g.dart +++ b/lib/shared/services/pdf_core/models/implement/layout_impl.g.dart @@ -17,47 +17,40 @@ const LayoutModelSchema = CollectionSchema( name: r'pdf_layout', id: 5438317236946775742, properties: { - r'content': PropertySchema( - id: 0, - name: r'content', - type: IsarType.string, - ), - r'height': PropertySchema( - id: 1, - name: r'height', - type: IsarType.double, - ), - r'latex': PropertySchema( + r'caption': PropertySchema(id: 0, name: r'caption', type: IsarType.string), + r'content': PropertySchema(id: 1, name: r'content', type: IsarType.string), + r'figureId': PropertySchema( id: 2, - name: r'latex', + name: r'figureId', type: IsarType.string, ), - r'left': PropertySchema( + r'figureNumber': PropertySchema( id: 3, - name: r'left', - type: IsarType.double, + name: r'figureNumber', + type: IsarType.long, ), - r'text': PropertySchema( - id: 4, - name: r'text', + r'height': PropertySchema(id: 4, name: r'height', type: IsarType.double), + r'latex': PropertySchema(id: 5, name: r'latex', type: IsarType.string), + r'left': PropertySchema(id: 6, name: r'left', type: IsarType.double), + r'referenceText': PropertySchema( + id: 7, + name: r'referenceText', type: IsarType.string, ), - r'top': PropertySchema( - id: 5, - name: r'top', - type: IsarType.double, + r'referencedFigureId': PropertySchema( + id: 8, + name: r'referencedFigureId', + type: IsarType.string, ), + r'text': PropertySchema(id: 9, name: r'text', type: IsarType.string), + r'top': PropertySchema(id: 10, name: r'top', type: IsarType.double), r'type': PropertySchema( - id: 6, + id: 11, name: r'type', type: IsarType.byte, enumMap: _LayoutModeltypeEnumValueMap, ), - r'width': PropertySchema( - id: 7, - name: r'width', - type: IsarType.double, - ) + r'width': PropertySchema(id: 12, name: r'width', type: IsarType.double), }, estimateSize: _layoutModelEstimateSize, serialize: _layoutModelSerialize, @@ -71,7 +64,7 @@ const LayoutModelSchema = CollectionSchema( name: r'page', target: r'pdf_page', single: true, - ) + ), }, embeddedSchemas: {}, getId: _layoutModelGetId, @@ -86,13 +79,37 @@ int _layoutModelEstimateSize( Map> allOffsets, ) { var bytesCount = offsets.last; + { + final value = object.caption; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } bytesCount += 3 + object.content.length * 3; + { + final value = object.figureId; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } { final value = object.latex; if (value != null) { bytesCount += 3 + value.length * 3; } } + { + final value = object.referenceText; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } + { + final value = object.referencedFigureId; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } { final value = object.text; if (value != null) { @@ -108,14 +125,19 @@ void _layoutModelSerialize( List offsets, Map> allOffsets, ) { - writer.writeString(offsets[0], object.content); - writer.writeDouble(offsets[1], object.height); - writer.writeString(offsets[2], object.latex); - writer.writeDouble(offsets[3], object.left); - writer.writeString(offsets[4], object.text); - writer.writeDouble(offsets[5], object.top); - writer.writeByte(offsets[6], object.type.index); - writer.writeDouble(offsets[7], object.width); + writer.writeString(offsets[0], object.caption); + writer.writeString(offsets[1], object.content); + writer.writeString(offsets[2], object.figureId); + writer.writeLong(offsets[3], object.figureNumber); + writer.writeDouble(offsets[4], object.height); + writer.writeString(offsets[5], object.latex); + writer.writeDouble(offsets[6], object.left); + writer.writeString(offsets[7], object.referenceText); + writer.writeString(offsets[8], object.referencedFigureId); + writer.writeString(offsets[9], object.text); + writer.writeDouble(offsets[10], object.top); + writer.writeByte(offsets[11], object.type.index); + writer.writeDouble(offsets[12], object.width); } LayoutModel _layoutModelDeserialize( @@ -125,15 +147,21 @@ LayoutModel _layoutModelDeserialize( Map> allOffsets, ) { final object = LayoutModel( - content: reader.readString(offsets[0]), - height: reader.readDouble(offsets[1]), - latex: reader.readStringOrNull(offsets[2]), - left: reader.readDouble(offsets[3]), - text: reader.readStringOrNull(offsets[4]), - top: reader.readDouble(offsets[5]), - type: _LayoutModeltypeValueEnumMap[reader.readByteOrNull(offsets[6])] ?? + caption: reader.readStringOrNull(offsets[0]), + content: reader.readString(offsets[1]), + figureId: reader.readStringOrNull(offsets[2]), + figureNumber: reader.readLongOrNull(offsets[3]), + height: reader.readDouble(offsets[4]), + latex: reader.readStringOrNull(offsets[5]), + left: reader.readDouble(offsets[6]), + referenceText: reader.readStringOrNull(offsets[7]), + referencedFigureId: reader.readStringOrNull(offsets[8]), + text: reader.readStringOrNull(offsets[9]), + top: reader.readDouble(offsets[10]), + type: + _LayoutModeltypeValueEnumMap[reader.readByteOrNull(offsets[11])] ?? LayoutType.formula, - width: reader.readDouble(offsets[7]), + width: reader.readDouble(offsets[12]), ); object.id = id; return object; @@ -147,21 +175,32 @@ P _layoutModelDeserializeProp

( ) { switch (propertyId) { case 0: - return (reader.readString(offset)) as P; + return (reader.readStringOrNull(offset)) as P; case 1: - return (reader.readDouble(offset)) as P; + return (reader.readString(offset)) as P; case 2: return (reader.readStringOrNull(offset)) as P; case 3: - return (reader.readDouble(offset)) as P; + return (reader.readLongOrNull(offset)) as P; case 4: - return (reader.readStringOrNull(offset)) as P; - case 5: return (reader.readDouble(offset)) as P; + case 5: + return (reader.readStringOrNull(offset)) as P; case 6: - return (_LayoutModeltypeValueEnumMap[reader.readByteOrNull(offset)] ?? - LayoutType.formula) as P; + return (reader.readDouble(offset)) as P; case 7: + return (reader.readStringOrNull(offset)) as P; + case 8: + return (reader.readStringOrNull(offset)) as P; + case 9: + return (reader.readStringOrNull(offset)) as P; + case 10: + return (reader.readDouble(offset)) as P; + case 11: + return (_LayoutModeltypeValueEnumMap[reader.readByteOrNull(offset)] ?? + LayoutType.formula) + as P; + case 12: return (reader.readDouble(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); @@ -174,6 +213,8 @@ const _LayoutModeltypeEnumValueMap = { 'number': 2, 'header': 3, 'algorithm': 4, + 'figure': 5, + 'figureReference': 6, }; const _LayoutModeltypeValueEnumMap = { 0: LayoutType.formula, @@ -181,6 +222,8 @@ const _LayoutModeltypeValueEnumMap = { 2: LayoutType.number, 3: LayoutType.header, 4: LayoutType.algorithm, + 5: LayoutType.figure, + 6: LayoutType.figureReference, }; Id _layoutModelGetId(LayoutModel object) { @@ -192,7 +235,10 @@ List> _layoutModelGetLinks(LayoutModel object) { } void _layoutModelAttach( - IsarCollection col, Id id, LayoutModel object) { + IsarCollection col, + Id id, + LayoutModel object, +) { object.id = id; object.page.attach(col, col.isar.collection(), r'page', id); } @@ -210,15 +256,13 @@ extension LayoutModelQueryWhere on QueryBuilder { QueryBuilder idEqualTo(Id id) { return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between( - lower: id, - upper: id, - )); + return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); }); } QueryBuilder idNotEqualTo( - Id id) { + Id id, + ) { return QueryBuilder.apply(this, (query) { if (query.whereSort == Sort.asc) { return query @@ -240,8 +284,10 @@ extension LayoutModelQueryWhere }); } - QueryBuilder idGreaterThan(Id id, - {bool include = false}) { + QueryBuilder idGreaterThan( + Id id, { + bool include = false, + }) { return QueryBuilder.apply(this, (query) { return query.addWhereClause( IdWhereClause.greaterThan(lower: id, includeLower: include), @@ -249,8 +295,10 @@ extension LayoutModelQueryWhere }); } - QueryBuilder idLessThan(Id id, - {bool include = false}) { + QueryBuilder idLessThan( + Id id, { + bool include = false, + }) { return QueryBuilder.apply(this, (query) { return query.addWhereClause( IdWhereClause.lessThan(upper: id, includeUpper: include), @@ -265,44 +313,215 @@ extension LayoutModelQueryWhere bool includeUpper = true, }) { return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - )); + return query.addWhereClause( + IdWhereClause.between( + lower: lowerId, + includeLower: includeLower, + upper: upperId, + includeUpper: includeUpper, + ), + ); }); } } extension LayoutModelQueryFilter on QueryBuilder { + QueryBuilder + captionIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const FilterCondition.isNull(property: r'caption'), + ); + }); + } + + QueryBuilder + captionIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const FilterCondition.isNotNull(property: r'caption'), + ); + }); + } + + QueryBuilder captionEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo( + property: r'caption', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + captionGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'caption', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder captionLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'caption', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder captionBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.between( + property: r'caption', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + captionStartsWith(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.startsWith( + property: r'caption', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder captionEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.endsWith( + property: r'caption', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder captionContains( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.contains( + property: r'caption', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder captionMatches( + String pattern, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.matches( + property: r'caption', + wildcard: pattern, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + captionIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo(property: r'caption', value: ''), + ); + }); + } + + QueryBuilder + captionIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan(property: r'caption', value: ''), + ); + }); + } + QueryBuilder contentEqualTo( String value, { bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'content', - value: value, - caseSensitive: caseSensitive, - )); + return query.addFilterCondition( + FilterCondition.equalTo( + property: r'content', + value: value, + caseSensitive: caseSensitive, + ), + ); }); } QueryBuilder - contentGreaterThan( + contentGreaterThan( String value, { bool include = false, bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'content', - value: value, - caseSensitive: caseSensitive, - )); + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'content', + value: value, + caseSensitive: caseSensitive, + ), + ); }); } @@ -312,12 +531,14 @@ extension LayoutModelQueryFilter bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'content', - value: value, - caseSensitive: caseSensitive, - )); + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'content', + value: value, + caseSensitive: caseSensitive, + ), + ); }); } @@ -329,266 +550,923 @@ extension LayoutModelQueryFilter bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'content', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - )); + return query.addFilterCondition( + FilterCondition.between( + property: r'content', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + ), + ); }); } QueryBuilder - contentStartsWith( + contentStartsWith(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.startsWith( + property: r'content', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder contentEndsWith( String value, { bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.startsWith( - property: r'content', - value: value, - caseSensitive: caseSensitive, - )); + return query.addFilterCondition( + FilterCondition.endsWith( + property: r'content', + value: value, + caseSensitive: caseSensitive, + ), + ); }); } - QueryBuilder contentEndsWith( + QueryBuilder contentContains( String value, { bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.endsWith( - property: r'content', - value: value, - caseSensitive: caseSensitive, - )); + return query.addFilterCondition( + FilterCondition.contains( + property: r'content', + value: value, + caseSensitive: caseSensitive, + ), + ); }); } - QueryBuilder contentContains( - String value, - {bool caseSensitive = true}) { + QueryBuilder contentMatches( + String pattern, { + bool caseSensitive = true, + }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.contains( - property: r'content', - value: value, - caseSensitive: caseSensitive, - )); + return query.addFilterCondition( + FilterCondition.matches( + property: r'content', + wildcard: pattern, + caseSensitive: caseSensitive, + ), + ); }); } - QueryBuilder contentMatches( - String pattern, - {bool caseSensitive = true}) { + QueryBuilder + contentIsEmpty() { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.matches( - property: r'content', - wildcard: pattern, - caseSensitive: caseSensitive, - )); + return query.addFilterCondition( + FilterCondition.equalTo(property: r'content', value: ''), + ); }); } QueryBuilder - contentIsEmpty() { + contentIsNotEmpty() { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'content', - value: '', - )); + return query.addFilterCondition( + FilterCondition.greaterThan(property: r'content', value: ''), + ); }); } QueryBuilder - contentIsNotEmpty() { + figureIdIsNull() { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - property: r'content', - value: '', - )); + return query.addFilterCondition( + const FilterCondition.isNull(property: r'figureId'), + ); }); } - QueryBuilder heightEqualTo( - double value, { - double epsilon = Query.epsilon, + QueryBuilder + figureIdIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const FilterCondition.isNotNull(property: r'figureId'), + ); + }); + } + + QueryBuilder figureIdEqualTo( + String? value, { + bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'height', - value: value, - epsilon: epsilon, - )); + return query.addFilterCondition( + FilterCondition.equalTo( + property: r'figureId', + value: value, + caseSensitive: caseSensitive, + ), + ); }); } QueryBuilder - heightGreaterThan( - double value, { + figureIdGreaterThan( + String? value, { bool include = false, - double epsilon = Query.epsilon, + bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'height', - value: value, - epsilon: epsilon, - )); + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'figureId', + value: value, + caseSensitive: caseSensitive, + ), + ); }); } - QueryBuilder heightLessThan( - double value, { + QueryBuilder + figureIdLessThan( + String? value, { bool include = false, - double epsilon = Query.epsilon, + bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'height', - value: value, - epsilon: epsilon, - )); + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'figureId', + value: value, + caseSensitive: caseSensitive, + ), + ); }); } - QueryBuilder heightBetween( - double lower, - double upper, { + QueryBuilder figureIdBetween( + String? lower, + String? upper, { bool includeLower = true, bool includeUpper = true, - double epsilon = Query.epsilon, + bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'height', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - epsilon: epsilon, - )); + return query.addFilterCondition( + FilterCondition.between( + property: r'figureId', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + ), + ); }); } - QueryBuilder idEqualTo( - Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'id', - value: value, - )); + QueryBuilder + figureIdStartsWith(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.startsWith( + property: r'figureId', + value: value, + caseSensitive: caseSensitive, + ), + ); }); } - QueryBuilder idGreaterThan( - Id value, { - bool include = false, + QueryBuilder + figureIdEndsWith(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.endsWith( + property: r'figureId', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + figureIdContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.contains( + property: r'figureId', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder figureIdMatches( + String pattern, { + bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - )); + return query.addFilterCondition( + FilterCondition.matches( + property: r'figureId', + wildcard: pattern, + caseSensitive: caseSensitive, + ), + ); }); } - QueryBuilder idLessThan( - Id value, { - bool include = false, - }) { + QueryBuilder + figureIdIsEmpty() { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - )); + return query.addFilterCondition( + FilterCondition.equalTo(property: r'figureId', value: ''), + ); }); } - QueryBuilder idBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { + QueryBuilder + figureIdIsNotEmpty() { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); + return query.addFilterCondition( + FilterCondition.greaterThan(property: r'figureId', value: ''), + ); }); } - QueryBuilder latexIsNull() { + QueryBuilder + figureNumberIsNull() { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNull( - property: r'latex', - )); + return query.addFilterCondition( + const FilterCondition.isNull(property: r'figureNumber'), + ); }); } QueryBuilder - latexIsNotNull() { + figureNumberIsNotNull() { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNotNull( - property: r'latex', - )); + return query.addFilterCondition( + const FilterCondition.isNotNull(property: r'figureNumber'), + ); }); } - QueryBuilder latexEqualTo( - String? value, { - bool caseSensitive = true, - }) { + QueryBuilder + figureNumberEqualTo(int? value) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'latex', - value: value, - caseSensitive: caseSensitive, - )); + return query.addFilterCondition( + FilterCondition.equalTo(property: r'figureNumber', value: value), + ); + }); + } + + QueryBuilder + figureNumberGreaterThan(int? value, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'figureNumber', + value: value, + ), + ); + }); + } + + QueryBuilder + figureNumberLessThan(int? value, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'figureNumber', + value: value, + ), + ); + }); + } + + QueryBuilder + figureNumberBetween( + int? lower, + int? upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.between( + property: r'figureNumber', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + ), + ); + }); + } + + QueryBuilder heightEqualTo( + double value, { + double epsilon = Query.epsilon, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo( + property: r'height', + value: value, + epsilon: epsilon, + ), + ); + }); + } + + QueryBuilder + heightGreaterThan( + double value, { + bool include = false, + double epsilon = Query.epsilon, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'height', + value: value, + epsilon: epsilon, + ), + ); + }); + } + + QueryBuilder heightLessThan( + double value, { + bool include = false, + double epsilon = Query.epsilon, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'height', + value: value, + epsilon: epsilon, + ), + ); + }); + } + + QueryBuilder heightBetween( + double lower, + double upper, { + bool includeLower = true, + bool includeUpper = true, + double epsilon = Query.epsilon, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.between( + property: r'height', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + epsilon: epsilon, + ), + ); + }); + } + + QueryBuilder idEqualTo( + Id value, + ) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo(property: r'id', value: value), + ); + }); + } + + QueryBuilder idGreaterThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'id', + value: value, + ), + ); + }); + } + + QueryBuilder idLessThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'id', + value: value, + ), + ); + }); + } + + QueryBuilder idBetween( + Id lower, + Id upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.between( + property: r'id', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + ), + ); + }); + } + + QueryBuilder latexIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const FilterCondition.isNull(property: r'latex'), + ); + }); + } + + QueryBuilder + latexIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const FilterCondition.isNotNull(property: r'latex'), + ); + }); + } + + QueryBuilder latexEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo( + property: r'latex', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + latexGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'latex', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder latexLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'latex', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder latexBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.between( + property: r'latex', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder latexStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.startsWith( + property: r'latex', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder latexEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.endsWith( + property: r'latex', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder latexContains( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.contains( + property: r'latex', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder latexMatches( + String pattern, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.matches( + property: r'latex', + wildcard: pattern, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder latexIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo(property: r'latex', value: ''), + ); + }); + } + + QueryBuilder + latexIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan(property: r'latex', value: ''), + ); + }); + } + + QueryBuilder leftEqualTo( + double value, { + double epsilon = Query.epsilon, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo( + property: r'left', + value: value, + epsilon: epsilon, + ), + ); + }); + } + + QueryBuilder leftGreaterThan( + double value, { + bool include = false, + double epsilon = Query.epsilon, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'left', + value: value, + epsilon: epsilon, + ), + ); + }); + } + + QueryBuilder leftLessThan( + double value, { + bool include = false, + double epsilon = Query.epsilon, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'left', + value: value, + epsilon: epsilon, + ), + ); + }); + } + + QueryBuilder leftBetween( + double lower, + double upper, { + bool includeLower = true, + bool includeUpper = true, + double epsilon = Query.epsilon, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.between( + property: r'left', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + epsilon: epsilon, + ), + ); + }); + } + + QueryBuilder + referenceTextIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const FilterCondition.isNull(property: r'referenceText'), + ); + }); + } + + QueryBuilder + referenceTextIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const FilterCondition.isNotNull(property: r'referenceText'), + ); + }); + } + + QueryBuilder + referenceTextEqualTo(String? value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo( + property: r'referenceText', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + referenceTextGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'referenceText', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + referenceTextLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'referenceText', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + referenceTextBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.between( + property: r'referenceText', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + referenceTextStartsWith(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.startsWith( + property: r'referenceText', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + referenceTextEndsWith(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.endsWith( + property: r'referenceText', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + referenceTextContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.contains( + property: r'referenceText', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + referenceTextMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.matches( + property: r'referenceText', + wildcard: pattern, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + referenceTextIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo(property: r'referenceText', value: ''), + ); + }); + } + + QueryBuilder + referenceTextIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan(property: r'referenceText', value: ''), + ); + }); + } + + QueryBuilder + referencedFigureIdIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const FilterCondition.isNull(property: r'referencedFigureId'), + ); + }); + } + + QueryBuilder + referencedFigureIdIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const FilterCondition.isNotNull(property: r'referencedFigureId'), + ); + }); + } + + QueryBuilder + referencedFigureIdEqualTo(String? value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo( + property: r'referencedFigureId', + value: value, + caseSensitive: caseSensitive, + ), + ); }); } QueryBuilder - latexGreaterThan( + referencedFigureIdGreaterThan( String? value, { bool include = false, bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'latex', - value: value, - caseSensitive: caseSensitive, - )); + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'referencedFigureId', + value: value, + caseSensitive: caseSensitive, + ), + ); }); } - QueryBuilder latexLessThan( + QueryBuilder + referencedFigureIdLessThan( String? value, { bool include = false, bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'latex', - value: value, - caseSensitive: caseSensitive, - )); + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'referencedFigureId', + value: value, + caseSensitive: caseSensitive, + ), + ); }); } - QueryBuilder latexBetween( + QueryBuilder + referencedFigureIdBetween( String? lower, String? upper, { bool includeLower = true, @@ -596,162 +1474,103 @@ extension LayoutModelQueryFilter bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'latex', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder latexStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.startsWith( - property: r'latex', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder latexEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.endsWith( - property: r'latex', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder latexContains( - String value, - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.contains( - property: r'latex', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder latexMatches( - String pattern, - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.matches( - property: r'latex', - wildcard: pattern, - caseSensitive: caseSensitive, - )); + return query.addFilterCondition( + FilterCondition.between( + property: r'referencedFigureId', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + ), + ); }); } - QueryBuilder latexIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'latex', - value: '', - )); + QueryBuilder + referencedFigureIdStartsWith(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.startsWith( + property: r'referencedFigureId', + value: value, + caseSensitive: caseSensitive, + ), + ); }); } QueryBuilder - latexIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - property: r'latex', - value: '', - )); + referencedFigureIdEndsWith(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.endsWith( + property: r'referencedFigureId', + value: value, + caseSensitive: caseSensitive, + ), + ); }); } - QueryBuilder leftEqualTo( - double value, { - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'left', - value: value, - epsilon: epsilon, - )); + QueryBuilder + referencedFigureIdContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.contains( + property: r'referencedFigureId', + value: value, + caseSensitive: caseSensitive, + ), + ); }); } - QueryBuilder leftGreaterThan( - double value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'left', - value: value, - epsilon: epsilon, - )); + QueryBuilder + referencedFigureIdMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.matches( + property: r'referencedFigureId', + wildcard: pattern, + caseSensitive: caseSensitive, + ), + ); }); } - QueryBuilder leftLessThan( - double value, { - bool include = false, - double epsilon = Query.epsilon, - }) { + QueryBuilder + referencedFigureIdIsEmpty() { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'left', - value: value, - epsilon: epsilon, - )); + return query.addFilterCondition( + FilterCondition.equalTo(property: r'referencedFigureId', value: ''), + ); }); } - QueryBuilder leftBetween( - double lower, - double upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { + QueryBuilder + referencedFigureIdIsNotEmpty() { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'left', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - epsilon: epsilon, - )); + return query.addFilterCondition( + FilterCondition.greaterThan(property: r'referencedFigureId', value: ''), + ); }); } QueryBuilder textIsNull() { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNull( - property: r'text', - )); + return query.addFilterCondition( + const FilterCondition.isNull(property: r'text'), + ); }); } QueryBuilder - textIsNotNull() { + textIsNotNull() { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNotNull( - property: r'text', - )); + return query.addFilterCondition( + const FilterCondition.isNotNull(property: r'text'), + ); }); } @@ -760,11 +1579,13 @@ extension LayoutModelQueryFilter bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'text', - value: value, - caseSensitive: caseSensitive, - )); + return query.addFilterCondition( + FilterCondition.equalTo( + property: r'text', + value: value, + caseSensitive: caseSensitive, + ), + ); }); } @@ -774,12 +1595,14 @@ extension LayoutModelQueryFilter bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'text', - value: value, - caseSensitive: caseSensitive, - )); + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'text', + value: value, + caseSensitive: caseSensitive, + ), + ); }); } @@ -789,12 +1612,14 @@ extension LayoutModelQueryFilter bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'text', - value: value, - caseSensitive: caseSensitive, - )); + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'text', + value: value, + caseSensitive: caseSensitive, + ), + ); }); } @@ -806,14 +1631,16 @@ extension LayoutModelQueryFilter bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'text', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - )); + return query.addFilterCondition( + FilterCondition.between( + property: r'text', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + ), + ); }); } @@ -822,11 +1649,13 @@ extension LayoutModelQueryFilter bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.startsWith( - property: r'text', - value: value, - caseSensitive: caseSensitive, - )); + return query.addFilterCondition( + FilterCondition.startsWith( + property: r'text', + value: value, + caseSensitive: caseSensitive, + ), + ); }); } @@ -835,54 +1664,60 @@ extension LayoutModelQueryFilter bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.endsWith( - property: r'text', - value: value, - caseSensitive: caseSensitive, - )); + return query.addFilterCondition( + FilterCondition.endsWith( + property: r'text', + value: value, + caseSensitive: caseSensitive, + ), + ); }); } QueryBuilder textContains( - String value, - {bool caseSensitive = true}) { + String value, { + bool caseSensitive = true, + }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.contains( - property: r'text', - value: value, - caseSensitive: caseSensitive, - )); + return query.addFilterCondition( + FilterCondition.contains( + property: r'text', + value: value, + caseSensitive: caseSensitive, + ), + ); }); } QueryBuilder textMatches( - String pattern, - {bool caseSensitive = true}) { + String pattern, { + bool caseSensitive = true, + }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.matches( - property: r'text', - wildcard: pattern, - caseSensitive: caseSensitive, - )); + return query.addFilterCondition( + FilterCondition.matches( + property: r'text', + wildcard: pattern, + caseSensitive: caseSensitive, + ), + ); }); } QueryBuilder textIsEmpty() { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'text', - value: '', - )); + return query.addFilterCondition( + FilterCondition.equalTo(property: r'text', value: ''), + ); }); } QueryBuilder - textIsNotEmpty() { + textIsNotEmpty() { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - property: r'text', - value: '', - )); + return query.addFilterCondition( + FilterCondition.greaterThan(property: r'text', value: ''), + ); }); } @@ -891,11 +1726,13 @@ extension LayoutModelQueryFilter double epsilon = Query.epsilon, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'top', - value: value, - epsilon: epsilon, - )); + return query.addFilterCondition( + FilterCondition.equalTo( + property: r'top', + value: value, + epsilon: epsilon, + ), + ); }); } @@ -905,12 +1742,14 @@ extension LayoutModelQueryFilter double epsilon = Query.epsilon, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'top', - value: value, - epsilon: epsilon, - )); + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'top', + value: value, + epsilon: epsilon, + ), + ); }); } @@ -920,12 +1759,14 @@ extension LayoutModelQueryFilter double epsilon = Query.epsilon, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'top', - value: value, - epsilon: epsilon, - )); + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'top', + value: value, + epsilon: epsilon, + ), + ); }); } @@ -937,24 +1778,26 @@ extension LayoutModelQueryFilter double epsilon = Query.epsilon, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'top', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - epsilon: epsilon, - )); + return query.addFilterCondition( + FilterCondition.between( + property: r'top', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + epsilon: epsilon, + ), + ); }); } QueryBuilder typeEqualTo( - LayoutType value) { + LayoutType value, + ) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'type', - value: value, - )); + return query.addFilterCondition( + FilterCondition.equalTo(property: r'type', value: value), + ); }); } @@ -963,11 +1806,13 @@ extension LayoutModelQueryFilter bool include = false, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'type', - value: value, - )); + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'type', + value: value, + ), + ); }); } @@ -976,11 +1821,13 @@ extension LayoutModelQueryFilter bool include = false, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'type', - value: value, - )); + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'type', + value: value, + ), + ); }); } @@ -991,13 +1838,15 @@ extension LayoutModelQueryFilter bool includeUpper = true, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'type', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); + return query.addFilterCondition( + FilterCondition.between( + property: r'type', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + ), + ); }); } @@ -1006,27 +1855,31 @@ extension LayoutModelQueryFilter double epsilon = Query.epsilon, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'width', - value: value, - epsilon: epsilon, - )); + return query.addFilterCondition( + FilterCondition.equalTo( + property: r'width', + value: value, + epsilon: epsilon, + ), + ); }); } QueryBuilder - widthGreaterThan( + widthGreaterThan( double value, { bool include = false, double epsilon = Query.epsilon, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'width', - value: value, - epsilon: epsilon, - )); + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'width', + value: value, + epsilon: epsilon, + ), + ); }); } @@ -1036,12 +1889,14 @@ extension LayoutModelQueryFilter double epsilon = Query.epsilon, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'width', - value: value, - epsilon: epsilon, - )); + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'width', + value: value, + epsilon: epsilon, + ), + ); }); } @@ -1053,14 +1908,16 @@ extension LayoutModelQueryFilter double epsilon = Query.epsilon, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'width', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - epsilon: epsilon, - )); + return query.addFilterCondition( + FilterCondition.between( + property: r'width', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + epsilon: epsilon, + ), + ); }); } } @@ -1071,7 +1928,8 @@ extension LayoutModelQueryObject extension LayoutModelQueryLinks on QueryBuilder { QueryBuilder page( - FilterQuery q) { + FilterQuery q, + ) { return QueryBuilder.apply(this, (query) { return query.link(q, r'page'); }); @@ -1086,6 +1944,18 @@ extension LayoutModelQueryLinks extension LayoutModelQuerySortBy on QueryBuilder { + QueryBuilder sortByCaption() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'caption', Sort.asc); + }); + } + + QueryBuilder sortByCaptionDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'caption', Sort.desc); + }); + } + QueryBuilder sortByContent() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'content', Sort.asc); @@ -1098,6 +1968,31 @@ extension LayoutModelQuerySortBy }); } + QueryBuilder sortByFigureId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'figureId', Sort.asc); + }); + } + + QueryBuilder sortByFigureIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'figureId', Sort.desc); + }); + } + + QueryBuilder sortByFigureNumber() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'figureNumber', Sort.asc); + }); + } + + QueryBuilder + sortByFigureNumberDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'figureNumber', Sort.desc); + }); + } + QueryBuilder sortByHeight() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'height', Sort.asc); @@ -1134,6 +2029,33 @@ extension LayoutModelQuerySortBy }); } + QueryBuilder sortByReferenceText() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'referenceText', Sort.asc); + }); + } + + QueryBuilder + sortByReferenceTextDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'referenceText', Sort.desc); + }); + } + + QueryBuilder + sortByReferencedFigureId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'referencedFigureId', Sort.asc); + }); + } + + QueryBuilder + sortByReferencedFigureIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'referencedFigureId', Sort.desc); + }); + } + QueryBuilder sortByText() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'text', Sort.asc); @@ -1185,6 +2107,18 @@ extension LayoutModelQuerySortBy extension LayoutModelQuerySortThenBy on QueryBuilder { + QueryBuilder thenByCaption() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'caption', Sort.asc); + }); + } + + QueryBuilder thenByCaptionDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'caption', Sort.desc); + }); + } + QueryBuilder thenByContent() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'content', Sort.asc); @@ -1197,6 +2131,31 @@ extension LayoutModelQuerySortThenBy }); } + QueryBuilder thenByFigureId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'figureId', Sort.asc); + }); + } + + QueryBuilder thenByFigureIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'figureId', Sort.desc); + }); + } + + QueryBuilder thenByFigureNumber() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'figureNumber', Sort.asc); + }); + } + + QueryBuilder + thenByFigureNumberDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'figureNumber', Sort.desc); + }); + } + QueryBuilder thenByHeight() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'height', Sort.asc); @@ -1245,6 +2204,33 @@ extension LayoutModelQuerySortThenBy }); } + QueryBuilder thenByReferenceText() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'referenceText', Sort.asc); + }); + } + + QueryBuilder + thenByReferenceTextDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'referenceText', Sort.desc); + }); + } + + QueryBuilder + thenByReferencedFigureId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'referencedFigureId', Sort.asc); + }); + } + + QueryBuilder + thenByReferencedFigureIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'referencedFigureId', Sort.desc); + }); + } + QueryBuilder thenByText() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'text', Sort.asc); @@ -1296,21 +2282,45 @@ extension LayoutModelQuerySortThenBy extension LayoutModelQueryWhereDistinct on QueryBuilder { - QueryBuilder distinctByContent( - {bool caseSensitive = true}) { + QueryBuilder distinctByCaption({ + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'caption', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByContent({ + bool caseSensitive = true, + }) { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'content', caseSensitive: caseSensitive); }); } + QueryBuilder distinctByFigureId({ + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'figureId', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByFigureNumber() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'figureNumber'); + }); + } + QueryBuilder distinctByHeight() { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'height'); }); } - QueryBuilder distinctByLatex( - {bool caseSensitive = true}) { + QueryBuilder distinctByLatex({ + bool caseSensitive = true, + }) { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'latex', caseSensitive: caseSensitive); }); @@ -1322,8 +2332,30 @@ extension LayoutModelQueryWhereDistinct }); } - QueryBuilder distinctByText( - {bool caseSensitive = true}) { + QueryBuilder distinctByReferenceText({ + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy( + r'referenceText', + caseSensitive: caseSensitive, + ); + }); + } + + QueryBuilder + distinctByReferencedFigureId({bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy( + r'referencedFigureId', + caseSensitive: caseSensitive, + ); + }); + } + + QueryBuilder distinctByText({ + bool caseSensitive = true, + }) { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'text', caseSensitive: caseSensitive); }); @@ -1356,12 +2388,30 @@ extension LayoutModelQueryProperty }); } + QueryBuilder captionProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'caption'); + }); + } + QueryBuilder contentProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'content'); }); } + QueryBuilder figureIdProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'figureId'); + }); + } + + QueryBuilder figureNumberProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'figureNumber'); + }); + } + QueryBuilder heightProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'height'); @@ -1380,6 +2430,19 @@ extension LayoutModelQueryProperty }); } + QueryBuilder referenceTextProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'referenceText'); + }); + } + + QueryBuilder + referencedFigureIdProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'referencedFigureId'); + }); + } + QueryBuilder textProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'text'); diff --git a/lib/shared/services/pdf_core/models/implement/page_impl.dart b/lib/shared/services/pdf_core/models/implement/page_impl.dart index 7824c81..e386218 100644 --- a/lib/shared/services/pdf_core/models/implement/page_impl.dart +++ b/lib/shared/services/pdf_core/models/implement/page_impl.dart @@ -21,8 +21,8 @@ class PageModel extends BasePage { @override final String fullText; - final double _width; - final double _height; + final int _width; + final int _height; // ====== 관계 ====== @Backlink(to: 'page') @@ -34,8 +34,8 @@ class PageModel extends BasePage { PageModel({ required this.pageIndex, required this.fullText, - required double width, - required double height, + required int width, + required int height, }) : _width = width, _height = height; @@ -43,19 +43,24 @@ class PageModel extends BasePage { required this.pageIndex, required this.fullText, required Size size, - }) : _width = size.width, - _height = size.height; + }) : _width = size.width.toInt(), + _height = size.height.toInt(); // ====== 게터 ====== @override @ignore - Size get size => Size(_width, _height); - + Size get size => Size(_width.toDouble(), _height.toDouble()); + @override @Name('width') - double get width => _width; + int get width => _width; + @override @Name('height') - double get height => _height; + int get height => _height; @override - Future> getLayouts() async => []; + Future> getLayouts() async { + // Isar의 백링크를 통해 레이아웃들을 로드 + await layouts.load(); + return layouts.toList(); + } } diff --git a/lib/shared/services/pdf_core/models/implement/page_impl.g.dart b/lib/shared/services/pdf_core/models/implement/page_impl.g.dart index d7ae72e..c54b049 100644 --- a/lib/shared/services/pdf_core/models/implement/page_impl.g.dart +++ b/lib/shared/services/pdf_core/models/implement/page_impl.g.dart @@ -22,21 +22,13 @@ const PageModelSchema = CollectionSchema( name: r'full_text', type: IsarType.string, ), - r'height': PropertySchema( - id: 1, - name: r'height', - type: IsarType.double, - ), + r'height': PropertySchema(id: 1, name: r'height', type: IsarType.long), r'page_index': PropertySchema( id: 2, name: r'page_index', type: IsarType.long, ), - r'width': PropertySchema( - id: 3, - name: r'width', - type: IsarType.double, - ) + r'width': PropertySchema(id: 3, name: r'width', type: IsarType.long), }, estimateSize: _pageModelEstimateSize, serialize: _pageModelSerialize, @@ -57,7 +49,7 @@ const PageModelSchema = CollectionSchema( name: r'pdf', target: r'pdf', single: true, - ) + ), }, embeddedSchemas: {}, getId: _pageModelGetId, @@ -83,9 +75,9 @@ void _pageModelSerialize( Map> allOffsets, ) { writer.writeString(offsets[0], object.fullText); - writer.writeDouble(offsets[1], object.height); + writer.writeLong(offsets[1], object.height); writer.writeLong(offsets[2], object.pageIndex); - writer.writeDouble(offsets[3], object.width); + writer.writeLong(offsets[3], object.width); } PageModel _pageModelDeserialize( @@ -96,9 +88,9 @@ PageModel _pageModelDeserialize( ) { final object = PageModel( fullText: reader.readString(offsets[0]), - height: reader.readDouble(offsets[1]), + height: reader.readLong(offsets[1]), pageIndex: reader.readLong(offsets[2]), - width: reader.readDouble(offsets[3]), + width: reader.readLong(offsets[3]), ); object.id = id; return object; @@ -114,11 +106,11 @@ P _pageModelDeserializeProp

( case 0: return (reader.readString(offset)) as P; case 1: - return (reader.readDouble(offset)) as P; + return (reader.readLong(offset)) as P; case 2: return (reader.readLong(offset)) as P; case 3: - return (reader.readDouble(offset)) as P; + return (reader.readLong(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); } @@ -134,8 +126,12 @@ List> _pageModelGetLinks(PageModel object) { void _pageModelAttach(IsarCollection col, Id id, PageModel object) { object.id = id; - object.layouts - .attach(col, col.isar.collection(), r'layouts', id); + object.layouts.attach( + col, + col.isar.collection(), + r'layouts', + id, + ); object.pdf.attach(col, col.isar.collection(), r'pdf', id); } @@ -152,10 +148,7 @@ extension PageModelQueryWhere on QueryBuilder { QueryBuilder idEqualTo(Id id) { return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between( - lower: id, - upper: id, - )); + return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); }); } @@ -181,8 +174,10 @@ extension PageModelQueryWhere }); } - QueryBuilder idGreaterThan(Id id, - {bool include = false}) { + QueryBuilder idGreaterThan( + Id id, { + bool include = false, + }) { return QueryBuilder.apply(this, (query) { return query.addWhereClause( IdWhereClause.greaterThan(lower: id, includeLower: include), @@ -190,8 +185,10 @@ extension PageModelQueryWhere }); } - QueryBuilder idLessThan(Id id, - {bool include = false}) { + QueryBuilder idLessThan( + Id id, { + bool include = false, + }) { return QueryBuilder.apply(this, (query) { return query.addWhereClause( IdWhereClause.lessThan(upper: id, includeUpper: include), @@ -206,12 +203,14 @@ extension PageModelQueryWhere bool includeUpper = true, }) { return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - )); + return query.addWhereClause( + IdWhereClause.between( + lower: lowerId, + includeLower: includeLower, + upper: upperId, + includeUpper: includeUpper, + ), + ); }); } } @@ -223,11 +222,13 @@ extension PageModelQueryFilter bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'full_text', - value: value, - caseSensitive: caseSensitive, - )); + return query.addFilterCondition( + FilterCondition.equalTo( + property: r'full_text', + value: value, + caseSensitive: caseSensitive, + ), + ); }); } @@ -237,12 +238,14 @@ extension PageModelQueryFilter bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'full_text', - value: value, - caseSensitive: caseSensitive, - )); + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'full_text', + value: value, + caseSensitive: caseSensitive, + ), + ); }); } @@ -252,12 +255,14 @@ extension PageModelQueryFilter bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'full_text', - value: value, - caseSensitive: caseSensitive, - )); + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'full_text', + value: value, + caseSensitive: caseSensitive, + ), + ); }); } @@ -269,14 +274,16 @@ extension PageModelQueryFilter bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'full_text', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - )); + return query.addFilterCondition( + FilterCondition.between( + property: r'full_text', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + ), + ); }); } @@ -285,11 +292,13 @@ extension PageModelQueryFilter bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.startsWith( - property: r'full_text', - value: value, - caseSensitive: caseSensitive, - )); + return query.addFilterCondition( + FilterCondition.startsWith( + property: r'full_text', + value: value, + caseSensitive: caseSensitive, + ), + ); }); } @@ -298,126 +307,129 @@ extension PageModelQueryFilter bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.endsWith( - property: r'full_text', - value: value, - caseSensitive: caseSensitive, - )); + return query.addFilterCondition( + FilterCondition.endsWith( + property: r'full_text', + value: value, + caseSensitive: caseSensitive, + ), + ); }); } QueryBuilder fullTextContains( - String value, - {bool caseSensitive = true}) { + String value, { + bool caseSensitive = true, + }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.contains( - property: r'full_text', - value: value, - caseSensitive: caseSensitive, - )); + return query.addFilterCondition( + FilterCondition.contains( + property: r'full_text', + value: value, + caseSensitive: caseSensitive, + ), + ); }); } QueryBuilder fullTextMatches( - String pattern, - {bool caseSensitive = true}) { + String pattern, { + bool caseSensitive = true, + }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.matches( - property: r'full_text', - wildcard: pattern, - caseSensitive: caseSensitive, - )); + return query.addFilterCondition( + FilterCondition.matches( + property: r'full_text', + wildcard: pattern, + caseSensitive: caseSensitive, + ), + ); }); } QueryBuilder fullTextIsEmpty() { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'full_text', - value: '', - )); + return query.addFilterCondition( + FilterCondition.equalTo(property: r'full_text', value: ''), + ); }); } QueryBuilder - fullTextIsNotEmpty() { + fullTextIsNotEmpty() { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - property: r'full_text', - value: '', - )); + return query.addFilterCondition( + FilterCondition.greaterThan(property: r'full_text', value: ''), + ); }); } QueryBuilder heightEqualTo( - double value, { - double epsilon = Query.epsilon, - }) { + int value, + ) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'height', - value: value, - epsilon: epsilon, - )); + return query.addFilterCondition( + FilterCondition.equalTo(property: r'height', value: value), + ); }); } QueryBuilder heightGreaterThan( - double value, { + int value, { bool include = false, - double epsilon = Query.epsilon, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'height', - value: value, - epsilon: epsilon, - )); + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'height', + value: value, + ), + ); }); } QueryBuilder heightLessThan( - double value, { + int value, { bool include = false, - double epsilon = Query.epsilon, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'height', - value: value, - epsilon: epsilon, - )); + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'height', + value: value, + ), + ); }); } QueryBuilder heightBetween( - double lower, - double upper, { + int lower, + int upper, { bool includeLower = true, bool includeUpper = true, - double epsilon = Query.epsilon, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'height', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - epsilon: epsilon, - )); + return query.addFilterCondition( + FilterCondition.between( + property: r'height', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + ), + ); }); } QueryBuilder idEqualTo( - Id value) { + Id value, + ) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'id', - value: value, - )); + return query.addFilterCondition( + FilterCondition.equalTo(property: r'id', value: value), + ); }); } @@ -426,11 +438,13 @@ extension PageModelQueryFilter bool include = false, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - )); + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'id', + value: value, + ), + ); }); } @@ -439,11 +453,13 @@ extension PageModelQueryFilter bool include = false, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - )); + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'id', + value: value, + ), + ); }); } @@ -454,37 +470,38 @@ extension PageModelQueryFilter bool includeUpper = true, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); + return query.addFilterCondition( + FilterCondition.between( + property: r'id', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + ), + ); }); } QueryBuilder pageIndexEqualTo( - int value) { + int value, + ) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'page_index', - value: value, - )); + return query.addFilterCondition( + FilterCondition.equalTo(property: r'page_index', value: value), + ); }); } QueryBuilder - pageIndexGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'page_index', - value: value, - )); + pageIndexGreaterThan(int value, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'page_index', + value: value, + ), + ); }); } @@ -493,11 +510,13 @@ extension PageModelQueryFilter bool include = false, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'page_index', - value: value, - )); + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'page_index', + value: value, + ), + ); }); } @@ -508,75 +527,74 @@ extension PageModelQueryFilter bool includeUpper = true, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'page_index', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); + return query.addFilterCondition( + FilterCondition.between( + property: r'page_index', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + ), + ); }); } QueryBuilder widthEqualTo( - double value, { - double epsilon = Query.epsilon, - }) { + int value, + ) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'width', - value: value, - epsilon: epsilon, - )); + return query.addFilterCondition( + FilterCondition.equalTo(property: r'width', value: value), + ); }); } QueryBuilder widthGreaterThan( - double value, { + int value, { bool include = false, - double epsilon = Query.epsilon, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'width', - value: value, - epsilon: epsilon, - )); + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'width', + value: value, + ), + ); }); } QueryBuilder widthLessThan( - double value, { + int value, { bool include = false, - double epsilon = Query.epsilon, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'width', - value: value, - epsilon: epsilon, - )); + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'width', + value: value, + ), + ); }); } QueryBuilder widthBetween( - double lower, - double upper, { + int lower, + int upper, { bool includeLower = true, bool includeUpper = true, - double epsilon = Query.epsilon, }) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'width', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - epsilon: epsilon, - )); + return query.addFilterCondition( + FilterCondition.between( + property: r'width', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + ), + ); }); } } @@ -587,14 +605,15 @@ extension PageModelQueryObject extension PageModelQueryLinks on QueryBuilder { QueryBuilder layouts( - FilterQuery q) { + FilterQuery q, + ) { return QueryBuilder.apply(this, (query) { return query.link(q, r'layouts'); }); } QueryBuilder - layoutsLengthEqualTo(int length) { + layoutsLengthEqualTo(int length) { return QueryBuilder.apply(this, (query) { return query.linkLength(r'layouts', length, true, length, true); }); @@ -607,34 +626,28 @@ extension PageModelQueryLinks } QueryBuilder - layoutsIsNotEmpty() { + layoutsIsNotEmpty() { return QueryBuilder.apply(this, (query) { return query.linkLength(r'layouts', 0, false, 999999, true); }); } QueryBuilder - layoutsLengthLessThan( - int length, { - bool include = false, - }) { + layoutsLengthLessThan(int length, {bool include = false}) { return QueryBuilder.apply(this, (query) { return query.linkLength(r'layouts', 0, true, length, include); }); } QueryBuilder - layoutsLengthGreaterThan( - int length, { - bool include = false, - }) { + layoutsLengthGreaterThan(int length, {bool include = false}) { return QueryBuilder.apply(this, (query) { return query.linkLength(r'layouts', length, include, 999999, true); }); } QueryBuilder - layoutsLengthBetween( + layoutsLengthBetween( int lower, int upper, { bool includeLower = true, @@ -642,12 +655,18 @@ extension PageModelQueryLinks }) { return QueryBuilder.apply(this, (query) { return query.linkLength( - r'layouts', lower, includeLower, upper, includeUpper); + r'layouts', + lower, + includeLower, + upper, + includeUpper, + ); }); } QueryBuilder pdf( - FilterQuery q) { + FilterQuery q, + ) { return QueryBuilder.apply(this, (query) { return query.link(q, r'pdf'); }); @@ -775,8 +794,9 @@ extension PageModelQuerySortThenBy extension PageModelQueryWhereDistinct on QueryBuilder { - QueryBuilder distinctByFullText( - {bool caseSensitive = true}) { + QueryBuilder distinctByFullText({ + bool caseSensitive = true, + }) { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'full_text', caseSensitive: caseSensitive); }); @@ -815,7 +835,7 @@ extension PageModelQueryProperty }); } - QueryBuilder heightProperty() { + QueryBuilder heightProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'height'); }); @@ -827,7 +847,7 @@ extension PageModelQueryProperty }); } - QueryBuilder widthProperty() { + QueryBuilder widthProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'width'); }); diff --git a/lib/shared/services/pdf_core/models/implement/pdf_impl.dart b/lib/shared/services/pdf_core/models/implement/pdf_impl.dart index f950b6b..76de52f 100644 --- a/lib/shared/services/pdf_core/models/implement/pdf_impl.dart +++ b/lib/shared/services/pdf_core/models/implement/pdf_impl.dart @@ -80,7 +80,14 @@ class PDFModel extends BasePdf { int get currentPage => _currentPage; @override - Future> getPages() async => []; + Future> getPages() async { + // Isar의 백링크를 통해 페이지들을 로드 + await pages.load(); + // 페이지 인덱스 순으로 정렬하여 반환 + final pageList = pages.toList(); + pageList.sort((a, b) => a.pageIndex.compareTo(b.pageIndex)); + return pageList; + } void update({ String? name, diff --git a/lib/shared/services/pdf_core/models/interface/base_layout.dart b/lib/shared/services/pdf_core/models/interface/base_layout.dart index a2ee7e4..ce7a88c 100644 --- a/lib/shared/services/pdf_core/models/interface/base_layout.dart +++ b/lib/shared/services/pdf_core/models/interface/base_layout.dart @@ -3,7 +3,15 @@ import 'package:flutter/services.dart'; import 'package:isar/isar.dart'; -enum LayoutType { formula, text, number, header, algorithm } +enum LayoutType { + formula, + text, + number, + header, + algorithm, + figure, + figureReference, +} // PDF 레이아웃 데이터 모델 인터페이스 abstract class BaseLayout { @@ -25,4 +33,20 @@ abstract class BaseLayout { // 레이아웃 위치 @ignore Rect get rect; + + // Figure 관련 속성들 + // figure 타입일 경우 figure ID를 가집니다. + String? get figureId; + + // figure 타입일 경우 figure 번호를 가집니다. + int? get figureNumber; + + // figure 타입일 경우 캡션을 가집니다. + String? get caption; + + // figureReference 타입일 경우 참조하는 figure ID를 가집니다. + String? get referencedFigureId; + + // figureReference 타입일 경우 참조 텍스트를 가집니다. + String? get referenceText; } diff --git a/lib/shared/services/pdf_core/models/interface/base_page.dart b/lib/shared/services/pdf_core/models/interface/base_page.dart index 6370e93..1fc7493 100644 --- a/lib/shared/services/pdf_core/models/interface/base_page.dart +++ b/lib/shared/services/pdf_core/models/interface/base_page.dart @@ -19,6 +19,9 @@ abstract class BasePage { // 페이지 전체 텍스트 String get fullText; + int get width; + int get height; + // 레이아웃 목록 Future> getLayouts(); } diff --git a/lib/shared/services/pdf_core/provider/pdf_provider.dart b/lib/shared/services/pdf_core/provider/pdf_provider.dart index 10c93a1..aa7f78a 100644 --- a/lib/shared/services/pdf_core/provider/pdf_provider.dart +++ b/lib/shared/services/pdf_core/provider/pdf_provider.dart @@ -27,4 +27,7 @@ abstract class PDFProvider extends ChangeNotifier { PDFStatus? status, int? limit, }); + + /// 경로로 PDF 조회 (Isar DB에서 조회, 없으면 null 반환) + Future getPDF(String path); } diff --git a/lib/shared/services/pdf_core/provider/pdf_provider_impl.dart b/lib/shared/services/pdf_core/provider/pdf_provider_impl.dart index 8ce5398..fbb2084 100644 --- a/lib/shared/services/pdf_core/provider/pdf_provider_impl.dart +++ b/lib/shared/services/pdf_core/provider/pdf_provider_impl.dart @@ -63,14 +63,6 @@ class PDFProviderImpl extends PDFProvider { _pdfs = await _getAllPdfs(); _logger.info('PDFs are changed.'); notifyListeners(); - // OCR 처리 대기 목록 처리 - final pendingPdfs = await _getPendingPdfs(); - for (final pdf in pendingPdfs) { - if (!_processing.contains(pdf.id)) { - _processing.add(pdf.id); - _processPdfWithOcr(pdf); - } - } }); } @@ -88,6 +80,7 @@ class PDFProviderImpl extends PDFProvider { // 주어진 PDF에 대한 OCR을 진행합니다. 이때 독립된 isolate로 변경이 되고, // 처리 결과는 주 스레드에서 처리됩니다. Future _processPdfWithOcr(PDFModel pdf) async { + _logger.severe('process OCR'); await updatePDF(id: pdf.id, status: PDFStatus.processing); final receivePort = ReceivePort(); await Isolate.spawn(_ocrProcess, [ @@ -127,7 +120,116 @@ class PDFProviderImpl extends PDFProvider { OCRResult? ocrResult, ) async { if (ocrResult == null) throw Exception('OCR 결과 없음'); - throw UnimplementedError(); + + try { + await _isar.writeTxn(() async { + // 2. 페이지별 데이터 저장 + for (final pageDetail in ocrResult.pages) { + // 페이지 모델 생성 + final pageModel = PageModel( + pageIndex: pageDetail.pageIndex, + fullText: pageDetail.fullText, + width: pageDetail.width, + height: pageDetail.height, + ); + + await _isar.pageModels.put(pageModel); + + // 페이지와 PDF 연결 + pageModel.pdf.value = pdf; + await pageModel.pdf.save(); + pdf.pages.add(pageModel); + + // 3. 레이아웃 데이터 저장 + for (final layout in pageDetail.layouts) { + LayoutModel layoutModel; + + if (layout is TextLayout) { + layoutModel = LayoutModel( + type: LayoutType.text, + content: layout.text, + text: layout.text, + latex: null, + figureId: null, + figureNumber: null, + caption: null, + referencedFigureId: null, + referenceText: null, + top: layout.boundingBox[1], + left: layout.boundingBox[0], + width: layout.boundingBox[2] - layout.boundingBox[0], + height: layout.boundingBox[3] - layout.boundingBox[1], + ); + } else if (layout is FigureLayoutItem) { + layoutModel = LayoutModel( + type: LayoutType.figure, + content: layout.caption, + text: null, + latex: null, + figureId: layout.figureId, + figureNumber: layout.figureNumber, + caption: layout.caption, + referencedFigureId: null, + referenceText: null, + top: layout.boundingBox[1], + left: layout.boundingBox[0], + width: layout.boundingBox[2] - layout.boundingBox[0], + height: layout.boundingBox[3] - layout.boundingBox[1], + ); + } else if (layout is FigureReferenceLayout) { + layoutModel = LayoutModel( + type: LayoutType.figureReference, + content: layout.referenceText, + text: layout.referenceText, + latex: null, + figureId: null, + figureNumber: layout.figureNumber, + caption: null, + referencedFigureId: layout.referencedFigureId, + referenceText: layout.referenceText, + top: layout.boundingBox[1], + left: layout.boundingBox[0], + width: layout.boundingBox[2] - layout.boundingBox[0], + height: layout.boundingBox[3] - layout.boundingBox[1], + ); + } else { + continue; // 알 수 없는 레이아웃 타입은 건너뜀 + } + + await _isar.layoutModels.put(layoutModel); + + // 레이아웃과 페이지 연결 + layoutModel.page.value = pageModel; + await layoutModel.page.save(); + pageModel.layouts.add(layoutModel); + } + + await pageModel.layouts.save(); + } + + // 4. PDF와 페이지 관계 저장 + await pdf.pages.save(); + await _isar.pDFModels.put(pdf); + }); + + // 5. PDF 상태를 완료로 업데이트 (updatePDF 메서드 사용) + await updatePDF( + id: pdf.id, + status: PDFStatus.completed, + updatedAt: DateTime.now(), + ); + + _logger.info('PDF OCR 결과 저장 완료: ${pdf.name}'); + } catch (e) { + _logger.severe('OCR 결과 저장 중 오류 발생: $e'); + // 오류 발생 시 상태를 failed로 업데이트 + await updatePDF( + id: pdf.id, + status: PDFStatus.failed, + updatedAt: DateTime.now(), + ); + rethrow; + } } Future<_PDFInfo> _getPDFInfo(String filePath) async { @@ -170,11 +272,13 @@ class PDFProviderImpl extends PDFProvider { await _isar.pDFModels.put(pdf); }); final pdfInfo = await _getPDFInfo(filePath); + pdf.thumbnail = pdfInfo.thumbnail; await updatePDF( id: pdf.id, thumbnail: pdfInfo.thumbnail, totalPages: pdfInfo.totalPages, ); + _processPdfWithOcr(pdf); } catch (e) { _logger.severe('Failed to add PDF: $e'); } @@ -232,6 +336,11 @@ class PDFProviderImpl extends PDFProvider { return await query.findAll(); } + @override + Future getPDF(String path) async { + return await _isar.pDFModels.filter().pathEqualTo(path).findFirst(); + } + @override void dispose() { _databaseSubscription.cancel(); diff --git a/pubspec.lock b/pubspec.lock index d1fc323..b5a1698 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,30 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + url: "https://pub.dev" + source: hosted + version: "61.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + url: "https://pub.dev" + source: hosted + version: "5.13.0" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" _fe_analyzer_shared: dependency: transitive description: @@ -105,6 +129,70 @@ packages: url: "https://pub.dev" source: hosted version: "8.9.5" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" + url: "https://pub.dev" + source: hosted + version: "4.0.4" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + url: "https://pub.dev" + source: hosted + version: "2.4.13" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 + url: "https://pub.dev" + source: hosted + version: "7.3.2" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4 + url: "https://pub.dev" + source: hosted + version: "8.9.5" characters: dependency: transitive description: @@ -121,6 +209,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" clock: dependency: transitive description: @@ -137,6 +233,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.10.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" + url: "https://pub.dev" + source: hosted + version: "4.10.1" collection: dependency: transitive description: @@ -169,6 +273,30 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.6" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + url: "https://pub.dev" + source: hosted + version: "0.3.4+2" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" cupertino_icons: dependency: "direct main" description: @@ -193,6 +321,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + dartx: + dependency: transitive + description: + name: dartx + sha256: "8b25435617027257d43e6508b5fe061012880ddfdaa75a71d607c3de2a13d244" + url: "https://pub.dev" + source: hosted + version: "1.2.0" fake_async: dependency: transitive description: @@ -233,11 +377,51 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: "77f8e81d22d2a07d0dee2c62e1dda71dc1da73bf43bb2d45af09727406167964" + url: "https://pub.dev" + source: hosted + version: "10.1.9" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_client_sse: + dependency: "direct main" + description: + name: flutter_client_sse + sha256: "4ce0297206473dfc064b255fe086713240002e149f52519bd48c21423e4aa5d2" + url: "https://pub.dev" + source: hosted + version: "2.0.3" flutter_driver: dependency: transitive description: flutter @@ -259,6 +443,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.28" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e + url: "https://pub.dev" + source: hosted + version: "2.0.28" flutter_test: dependency: "direct dev" description: flutter @@ -299,7 +491,7 @@ packages: source: hosted version: "2.3.2" http: - dependency: transitive + dependency: "direct main" description: name: http sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" @@ -423,6 +615,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + logging: + dependency: "direct main" + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" matcher: dependency: transitive description: @@ -455,6 +655,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + mockito: + dependency: "direct dev" + description: + name: mockito + sha256: "6841eed20a7befac0ce07df8116c8b8233ed1f4486a7647c7fc5a02ae6163917" + url: "https://pub.dev" + source: hosted + version: "5.4.4" nested: dependency: transitive description: @@ -471,6 +679,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + path: + dependency: "direct main" + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" path: dependency: "direct main" description: @@ -671,48 +887,256 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - source_gen: - dependency: transitive + path_provider: + dependency: "direct main" description: - name: source_gen - sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "1.5.0" - source_span: + version: "2.1.5" + path_provider_android: dependency: transitive description: - name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + name: path_provider_android + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 url: "https://pub.dev" source: hosted - version: "1.10.1" - stack_trace: + version: "2.2.17" + path_provider_foundation: dependency: transitive description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "1.12.1" - stream_channel: + version: "2.4.1" + path_provider_linux: dependency: transitive description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 url: "https://pub.dev" source: hosted - version: "2.1.4" - stream_transform: + version: "2.2.1" + path_provider_platform_interface: dependency: transitive description: - name: stream_transform - sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + pdfrx: + dependency: "direct main" + description: + name: pdfrx + sha256: "90747e916a64366b8beb69e9ac175e9134df520110543ab284b839102bf4b7e1" + url: "https://pub.dev" + source: hosted + version: "1.1.29" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + process: + dependency: transitive + description: + name: process + sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d" + url: "https://pub.dev" + source: hosted + version: "5.0.3" + provider: + dependency: "direct main" + description: + name: provider + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" + url: "https://pub.dev" + source: hosted + version: "6.1.5" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" + url: "https://pub.dev" + source: hosted + version: "2.4.10" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + 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" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 url: "https://pub.dev" source: hosted version: "2.1.1" @@ -740,6 +1164,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.3.1" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6" + url: "https://pub.dev" + source: hosted + version: "3.3.1" term_glyph: dependency: transitive description: @@ -837,7 +1277,96 @@ packages: source: hosted version: "2.4.1" url_launcher_windows: + time: + dependency: transitive + description: + name: time + sha256: "370572cf5d1e58adcb3e354c47515da3f7469dac3a95b447117e728e7be6f461" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + timing: + dependency: transitive + description: + name: timing + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + url_launcher: + dependency: transitive + description: + name: url_launcher + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" + url: "https://pub.dev" + source: hosted + version: "6.3.1" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" + url: "https://pub.dev" + source: hosted + version: "6.3.16" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" + url: "https://pub.dev" + source: hosted + version: "6.3.3" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_platform_interface: dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + vector_math: + dependency: "direct main" description: name: url_launcher_windows sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" @@ -932,6 +1461,79 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.3" + watcher: + dependency: transitive + description: + name: watcher + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8" + url: "https://pub.dev" + source: hosted + version: "3.0.4" + win32: + dependency: transitive + description: + name: win32 + sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba" + url: "https://pub.dev" + source: hosted + version: "5.13.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xxh3: + dependency: transitive + description: + name: xxh3 + sha256: "399a0438f5d426785723c99da6b16e136f4953fb1e9db0bf270bd41dd4619916" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" sdks: dart: ">=3.7.2 <4.0.0" flutter: ">=3.29.0" + flutter: ">=3.29.0" diff --git a/pubspec.yaml b/pubspec.yaml index f8d7e6b..d1c5ae6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,6 +39,14 @@ dependencies: shared_preferences: ^2.5.3 path: ^1.9.1 + pdfrx: ^1.1.28 + path_provider: ^2.1.5 + file_picker: ^10.1.9 + provider: ^6.1.5 + vector_math: ^2.1.4 + shared_preferences: ^2.5.3 + path: ^1.9.1 + # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 @@ -47,6 +55,9 @@ dependencies: intl: ^0.20.2 logging: ^1.2.0 + http: ^1.4.0 + flutter_client_sse: ^2.0.3 + dev_dependencies: flutter_test: sdk: flutter @@ -54,6 +65,9 @@ dev_dependencies: integration_test: sdk: flutter + integration_test: + sdk: flutter + # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is # activated in the `analysis_options.yaml` file located at the root of your @@ -62,6 +76,7 @@ dev_dependencies: flutter_lints: ^5.0.0 isar_generator: ^3.1.0+1 build_runner: ^2.4.13 + mockito: ^5.4.4 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec @@ -82,6 +97,13 @@ flutter: - assets/sample_ocr_test.pdf - assets/ocr_result.json + assets: + - assets/icons/ + - integration_test/assets/ + - assets/sample.pdf + - assets/sample_ocr_test.pdf + - assets/ocr_result.json + # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images @@ -94,6 +116,13 @@ flutter: # "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: Pretendard Variable + fonts: + - asset: assets/fonts/Pretendard-Medium.ttf + weight: 500 + - asset: assets/fonts/Pretendard-SemiBold.ttf + weight: 600 fonts: - family: Pretendard Variable fonts: