shoko 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.bundle/config +4 -0
- data/.bundle/config.bak +3 -0
- data/.rspec_status +42 -0
- data/.rubocop.yml +124 -0
- data/Gemfile +19 -0
- data/LICENSE +21 -0
- data/README.md +82 -0
- data/Rakefile +29 -0
- data/bin/start +15 -0
- data/lib/shoko/adapters/book_sources/document_service.rb +201 -0
- data/lib/shoko/adapters/book_sources/download_service.rb +95 -0
- data/lib/shoko/adapters/book_sources/epub/epub_resource_loader.rb +137 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/html_processor.rb +151 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/metadata_extractor.rb +53 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/entry_reader.rb +77 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/metadata_extractor.rb +67 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_context.rb +86 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_document_index.rb +75 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_document_scanner.rb +47 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_extractor.rb +46 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_label_resolver.rb +83 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_list_item.rb +55 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_result.rb +8 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_selector.rb +100 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_source_locator.rb +93 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_traversal.rb +103 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_walker.rb +56 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf_processor.rb +102 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/xhtml_content_parser.rb +661 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/xml_text_normalizer.rb +41 -0
- data/lib/shoko/adapters/book_sources/epub_document.rb +253 -0
- data/lib/shoko/adapters/book_sources/epub_finder/directory_scanner.rb +134 -0
- data/lib/shoko/adapters/book_sources/epub_finder/scanner_context.rb +28 -0
- data/lib/shoko/adapters/book_sources/epub_finder.rb +161 -0
- data/lib/shoko/adapters/book_sources/epub_importer.rb +268 -0
- data/lib/shoko/adapters/book_sources/gutendex_client.rb +150 -0
- data/lib/shoko/adapters/book_sources/library_scanner.rb +93 -0
- data/lib/shoko/adapters/book_sources/source_fingerprint.rb +57 -0
- data/lib/shoko/adapters/input/annotations/mouse_handler.rb +84 -0
- data/lib/shoko/adapters/input/command_bridge.rb +148 -0
- data/lib/shoko/adapters/input/command_factory.rb +255 -0
- data/lib/shoko/adapters/input/commands.rb +60 -0
- data/lib/shoko/adapters/input/dispatcher.rb +69 -0
- data/lib/shoko/adapters/input/input_controller.rb +250 -0
- data/lib/shoko/adapters/input/key_definitions.rb +108 -0
- data/lib/shoko/adapters/input/validators/file_path_validator.rb +81 -0
- data/lib/shoko/adapters/input/validators/terminal_size_validator.rb +76 -0
- data/lib/shoko/adapters/monitoring/logger.rb +150 -0
- data/lib/shoko/adapters/monitoring/perf_tracer.rb +183 -0
- data/lib/shoko/adapters/monitoring/performance_monitor.rb +110 -0
- data/lib/shoko/adapters/output/clipboard/clipboard_service.rb +125 -0
- data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler/image_builder.rb +149 -0
- data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler/text_wrapper.rb +149 -0
- data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler/tokenizer.rb +91 -0
- data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler.rb +144 -0
- data/lib/shoko/adapters/output/formatting/formatting_service/plain_lines_builder.rb +54 -0
- data/lib/shoko/adapters/output/formatting/formatting_service.rb +247 -0
- data/lib/shoko/adapters/output/formatting/wrapping_service.rb +228 -0
- data/lib/shoko/adapters/output/instrumentation_service.rb +52 -0
- data/lib/shoko/adapters/output/kitty/image_transcoder.rb +71 -0
- data/lib/shoko/adapters/output/kitty/kitty_graphics.rb +114 -0
- data/lib/shoko/adapters/output/kitty/kitty_image_renderer.rb +239 -0
- data/lib/shoko/adapters/output/kitty/kitty_unicode_placeholders.rb +139 -0
- data/lib/shoko/adapters/output/kitty/kitty_unicode_placeholders_diacritic_codepoints.txt +26 -0
- data/lib/shoko/adapters/output/notification_service.rb +58 -0
- data/lib/shoko/adapters/output/render_registry.rb +45 -0
- data/lib/shoko/adapters/output/rendering/models/line_geometry.rb +60 -0
- data/lib/shoko/adapters/output/rendering/models/page_rendering_context.rb +22 -0
- data/lib/shoko/adapters/output/rendering/models/render_params.rb +28 -0
- data/lib/shoko/adapters/output/rendering/models/rendering_context.rb +58 -0
- data/lib/shoko/adapters/output/terminal/buffer.rb +275 -0
- data/lib/shoko/adapters/output/terminal/constants/terminal_defaults.rb +11 -0
- data/lib/shoko/adapters/output/terminal/input/decoder.rb +347 -0
- data/lib/shoko/adapters/output/terminal/input.rb +161 -0
- data/lib/shoko/adapters/output/terminal/output.rb +105 -0
- data/lib/shoko/adapters/output/terminal/terminal.rb +167 -0
- data/lib/shoko/adapters/output/terminal/terminal_sanitizer.rb +243 -0
- data/lib/shoko/adapters/output/terminal/terminal_service.rb +138 -0
- data/lib/shoko/adapters/output/terminal/text_metrics.rb +273 -0
- data/lib/shoko/adapters/output/ui/builders/page_setup_builder.rb +47 -0
- data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay/footer_renderer.rb +80 -0
- data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay/geometry.rb +61 -0
- data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay/note_renderer.rb +86 -0
- data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay_component.rb +234 -0
- data/lib/shoko/adapters/output/ui/components/annotations_overlay/list_renderer.rb +142 -0
- data/lib/shoko/adapters/output/ui/components/annotations_overlay_component.rb +185 -0
- data/lib/shoko/adapters/output/ui/components/base_component.rb +110 -0
- data/lib/shoko/adapters/output/ui/components/component_interface.rb +80 -0
- data/lib/shoko/adapters/output/ui/components/content_component.rb +61 -0
- data/lib/shoko/adapters/output/ui/components/enhanced_popup_menu.rb +191 -0
- data/lib/shoko/adapters/output/ui/components/footer_component.rb +120 -0
- data/lib/shoko/adapters/output/ui/components/header_component.rb +46 -0
- data/lib/shoko/adapters/output/ui/components/layouts/horizontal.rb +63 -0
- data/lib/shoko/adapters/output/ui/components/layouts/vertical.rb +73 -0
- data/lib/shoko/adapters/output/ui/components/main_menu_component.rb +103 -0
- data/lib/shoko/adapters/output/ui/components/reading/base_view_renderer.rb +199 -0
- data/lib/shoko/adapters/output/ui/components/reading/config_helpers.rb +42 -0
- data/lib/shoko/adapters/output/ui/components/reading/help_renderer.rb +62 -0
- data/lib/shoko/adapters/output/ui/components/reading/inline_segment_highlighter.rb +144 -0
- data/lib/shoko/adapters/output/ui/components/reading/kitty_image_line_renderer.rb +262 -0
- data/lib/shoko/adapters/output/ui/components/reading/line_content_composer.rb +114 -0
- data/lib/shoko/adapters/output/ui/components/reading/line_drawer.rb +87 -0
- data/lib/shoko/adapters/output/ui/components/reading/line_geometry_builder.rb +41 -0
- data/lib/shoko/adapters/output/ui/components/reading/rendered_lines_recorder.rb +64 -0
- data/lib/shoko/adapters/output/ui/components/reading/single_view_renderer.rb +156 -0
- data/lib/shoko/adapters/output/ui/components/reading/split_view_renderer.rb +221 -0
- data/lib/shoko/adapters/output/ui/components/reading/view_renderer_factory.rb +20 -0
- data/lib/shoko/adapters/output/ui/components/reading/wrapped_lines_fetcher.rb +139 -0
- data/lib/shoko/adapters/output/ui/components/rect.rb +15 -0
- data/lib/shoko/adapters/output/ui/components/render_style.rb +84 -0
- data/lib/shoko/adapters/output/ui/components/screen_component.rb +24 -0
- data/lib/shoko/adapters/output/ui/components/screens/annotation_detail_screen_component.rb +175 -0
- data/lib/shoko/adapters/output/ui/components/screens/annotation_edit_screen_component.rb +221 -0
- data/lib/shoko/adapters/output/ui/components/screens/annotation_editor_screen_component.rb +205 -0
- data/lib/shoko/adapters/output/ui/components/screens/annotation_rendering_helpers.rb +190 -0
- data/lib/shoko/adapters/output/ui/components/screens/annotations_screen_component.rb +266 -0
- data/lib/shoko/adapters/output/ui/components/screens/base_screen_component.rb +49 -0
- data/lib/shoko/adapters/output/ui/components/screens/browse_screen_component.rb +319 -0
- data/lib/shoko/adapters/output/ui/components/screens/download_books_screen_component.rb +340 -0
- data/lib/shoko/adapters/output/ui/components/screens/library_screen_component.rb +205 -0
- data/lib/shoko/adapters/output/ui/components/screens/loading_overlay_component.rb +49 -0
- data/lib/shoko/adapters/output/ui/components/screens/menu_screen_component.rb +107 -0
- data/lib/shoko/adapters/output/ui/components/screens/settings_screen_component.rb +238 -0
- data/lib/shoko/adapters/output/ui/components/sidebar/annotations_tab_renderer.rb +159 -0
- data/lib/shoko/adapters/output/ui/components/sidebar/bookmarks_tab_renderer.rb +139 -0
- data/lib/shoko/adapters/output/ui/components/sidebar/tab_header_component.rb +157 -0
- data/lib/shoko/adapters/output/ui/components/sidebar/toc_tab_renderer.rb +111 -0
- data/lib/shoko/adapters/output/ui/components/sidebar/toc_tab_support.rb +1606 -0
- data/lib/shoko/adapters/output/ui/components/sidebar_panel_component.rb +217 -0
- data/lib/shoko/adapters/output/ui/components/surface.rb +88 -0
- data/lib/shoko/adapters/output/ui/components/tooltip_overlay_component.rb +224 -0
- data/lib/shoko/adapters/output/ui/components/ui/box_drawer.rb +32 -0
- data/lib/shoko/adapters/output/ui/components/ui/list_helpers.rb +33 -0
- data/lib/shoko/adapters/output/ui/components/ui/overlay_layout.rb +79 -0
- data/lib/shoko/adapters/output/ui/components/ui/text_utils.rb +46 -0
- data/lib/shoko/adapters/output/ui/constants/highlighting.rb +21 -0
- data/lib/shoko/adapters/output/ui/constants/messages.rb +12 -0
- data/lib/shoko/adapters/output/ui/constants/themes.rb +79 -0
- data/lib/shoko/adapters/output/ui/constants/ui_constants.rb +85 -0
- data/lib/shoko/adapters/output/ui/rendering/frame_coordinator.rb +42 -0
- data/lib/shoko/adapters/output/ui/rendering/reader_render_coordinator.rb +169 -0
- data/lib/shoko/adapters/output/ui/rendering/render_pipeline.rb +55 -0
- data/lib/shoko/adapters/storage/atomic_file_writer.rb +43 -0
- data/lib/shoko/adapters/storage/background_worker.rb +66 -0
- data/lib/shoko/adapters/storage/book_cache_pipeline.rb +653 -0
- data/lib/shoko/adapters/storage/cache/epub/memory_cache.rb +99 -0
- data/lib/shoko/adapters/storage/cache/epub/persistence.rb +131 -0
- data/lib/shoko/adapters/storage/cache/epub/serializer/deserialize.rb +225 -0
- data/lib/shoko/adapters/storage/cache/epub/serializer/helpers.rb +63 -0
- data/lib/shoko/adapters/storage/cache/epub/serializer/serialize.rb +83 -0
- data/lib/shoko/adapters/storage/cache/epub/serializer.rb +5 -0
- data/lib/shoko/adapters/storage/cache/epub/source_reference.rb +58 -0
- data/lib/shoko/adapters/storage/cache_paths.rb +21 -0
- data/lib/shoko/adapters/storage/cache_pointer_manager.rb +60 -0
- data/lib/shoko/adapters/storage/config_paths.rb +30 -0
- data/lib/shoko/adapters/storage/epub_cache.rb +195 -0
- data/lib/shoko/adapters/storage/file_writer_service.rb +47 -0
- data/lib/shoko/adapters/storage/json_cache_store/chapters.rb +141 -0
- data/lib/shoko/adapters/storage/json_cache_store/layouts.rb +67 -0
- data/lib/shoko/adapters/storage/json_cache_store/manifest.rb +42 -0
- data/lib/shoko/adapters/storage/json_cache_store/payload_helpers.rb +113 -0
- data/lib/shoko/adapters/storage/json_cache_store/resources.rb +84 -0
- data/lib/shoko/adapters/storage/json_cache_store.rb +167 -0
- data/lib/shoko/adapters/storage/lazy_file_string.rb +65 -0
- data/lib/shoko/adapters/storage/pagination_cache.rb +127 -0
- data/lib/shoko/adapters/storage/recent_files.rb +78 -0
- data/lib/shoko/adapters/storage/repositories/annotation_repository.rb +182 -0
- data/lib/shoko/adapters/storage/repositories/base_repository.rb +81 -0
- data/lib/shoko/adapters/storage/repositories/bookmark_repository.rb +132 -0
- data/lib/shoko/adapters/storage/repositories/cached_library_repository.rb +129 -0
- data/lib/shoko/adapters/storage/repositories/config_repository.rb +262 -0
- data/lib/shoko/adapters/storage/repositories/progress_repository.rb +166 -0
- data/lib/shoko/adapters/storage/repositories/storage/annotation_file_store.rb +128 -0
- data/lib/shoko/adapters/storage/repositories/storage/bookmark_file_store.rb +109 -0
- data/lib/shoko/adapters/storage/repositories/storage/file_store_utils.rb +20 -0
- data/lib/shoko/adapters/storage/repositories/storage/progress_file_store.rb +59 -0
- data/lib/shoko/application/annotation_editor_overlay_session.rb +138 -0
- data/lib/shoko/application/cli.rb +134 -0
- data/lib/shoko/application/controllers/menu/input_controller.rb +189 -0
- data/lib/shoko/application/controllers/menu/state_controller.rb +642 -0
- data/lib/shoko/application/controllers/menu_controller.rb +469 -0
- data/lib/shoko/application/controllers/mouseable_reader.rb +377 -0
- data/lib/shoko/application/controllers/reader_controller.rb +449 -0
- data/lib/shoko/application/controllers/state_controller.rb +410 -0
- data/lib/shoko/application/controllers/ui_controller.rb +782 -0
- data/lib/shoko/application/dependency_container.rb +301 -0
- data/lib/shoko/application/infrastructure/event_bus.rb +80 -0
- data/lib/shoko/application/infrastructure/observer_state_store.rb +136 -0
- data/lib/shoko/application/infrastructure/state_store.rb +413 -0
- data/lib/shoko/application/main_menu/menu_progress_presenter.rb +83 -0
- data/lib/shoko/application/pending_jump_handler.rb +122 -0
- data/lib/shoko/application/reader_lifecycle.rb +65 -0
- data/lib/shoko/application/reader_startup_orchestrator.rb +113 -0
- data/lib/shoko/application/selectors/config_selectors.rb +62 -0
- data/lib/shoko/application/selectors/menu_selectors.rb +62 -0
- data/lib/shoko/application/selectors/reader_selectors.rb +186 -0
- data/lib/shoko/application/state/actions/base_action.rb +24 -0
- data/lib/shoko/application/state/actions/quit_to_menu_action.rb +16 -0
- data/lib/shoko/application/state/actions/switch_reader_mode_action.rb +22 -0
- data/lib/shoko/application/state/actions/toggle_view_mode_action.rb +31 -0
- data/lib/shoko/application/state/actions/update_annotation_editor_overlay_action.rb +27 -0
- data/lib/shoko/application/state/actions/update_annotations_action.rb +20 -0
- data/lib/shoko/application/state/actions/update_annotations_overlay_action.rb +27 -0
- data/lib/shoko/application/state/actions/update_bookmarks_action.rb +20 -0
- data/lib/shoko/application/state/actions/update_chapter_action.rb +24 -0
- data/lib/shoko/application/state/actions/update_config_action.rb +22 -0
- data/lib/shoko/application/state/actions/update_field_helpers.rb +26 -0
- data/lib/shoko/application/state/actions/update_menu_action.rb +21 -0
- data/lib/shoko/application/state/actions/update_message_action.rb +35 -0
- data/lib/shoko/application/state/actions/update_page_action.rb +21 -0
- data/lib/shoko/application/state/actions/update_pagination_state_action.rb +21 -0
- data/lib/shoko/application/state/actions/update_popup_menu_action.rb +27 -0
- data/lib/shoko/application/state/actions/update_reader_meta_action.rb +21 -0
- data/lib/shoko/application/state/actions/update_reader_mode_action.rb +20 -0
- data/lib/shoko/application/state/actions/update_rendered_lines_action.rb +40 -0
- data/lib/shoko/application/state/actions/update_selection_action.rb +27 -0
- data/lib/shoko/application/state/actions/update_selections_action.rb +21 -0
- data/lib/shoko/application/state/actions/update_sidebar_action.rb +34 -0
- data/lib/shoko/application/state/actions/update_ui_loading_action.rb +23 -0
- data/lib/shoko/application/ui/reader_view_model_builder.rb +74 -0
- data/lib/shoko/application/ui/view_models/reader_view_model.rb +177 -0
- data/lib/shoko/application/unified_application.rb +48 -0
- data/lib/shoko/application/use_cases/catalog_service.rb +117 -0
- data/lib/shoko/application/use_cases/commands/annotation_editor_commands.rb +105 -0
- data/lib/shoko/application/use_cases/commands/application_commands.rb +208 -0
- data/lib/shoko/application/use_cases/commands/base_command.rb +166 -0
- data/lib/shoko/application/use_cases/commands/bookmark_commands.rb +114 -0
- data/lib/shoko/application/use_cases/commands/conditional_navigation_commands.rb +57 -0
- data/lib/shoko/application/use_cases/commands/menu_commands.rb +170 -0
- data/lib/shoko/application/use_cases/commands/navigation_commands.rb +183 -0
- data/lib/shoko/application/use_cases/commands/reader_commands.rb +46 -0
- data/lib/shoko/application/use_cases/commands/sidebar_commands.rb +55 -0
- data/lib/shoko/application/use_cases/settings_service.rb +123 -0
- data/lib/shoko/core/events/annotation_events.rb +94 -0
- data/lib/shoko/core/events/base_domain_event.rb +169 -0
- data/lib/shoko/core/events/bookmark_events.rb +41 -0
- data/lib/shoko/core/events/domain_event_bus.rb +163 -0
- data/lib/shoko/core/events/progress_events.rb +108 -0
- data/lib/shoko/core/models/bookmark.rb +36 -0
- data/lib/shoko/core/models/bookmark_data.rb +10 -0
- data/lib/shoko/core/models/chapter.rb +25 -0
- data/lib/shoko/core/models/content_block.rb +44 -0
- data/lib/shoko/core/models/reader_settings.rb +20 -0
- data/lib/shoko/core/models/selection_anchor.rb +73 -0
- data/lib/shoko/core/models/toc_entry.rb +14 -0
- data/lib/shoko/core/ports/annotation_repository.rb +0 -0
- data/lib/shoko/core/ports/book_repository.rb +0 -0
- data/lib/shoko/core/ports/book_source.rb +0 -0
- data/lib/shoko/core/ports/bookmark_repository.rb +0 -0
- data/lib/shoko/core/ports/cache.rb +0 -0
- data/lib/shoko/core/ports/input_handler.rb +0 -0
- data/lib/shoko/core/ports/renderer.rb +0 -0
- data/lib/shoko/core/ports/storage.rb +0 -0
- data/lib/shoko/core/services/annotation_service.rb +102 -0
- data/lib/shoko/core/services/base_service.rb +60 -0
- data/lib/shoko/core/services/bookmark_service.rb +267 -0
- data/lib/shoko/core/services/coordinate_service.rb +265 -0
- data/lib/shoko/core/services/layout_service.rb +95 -0
- data/lib/shoko/core/services/navigation/absolute_change_applier.rb +96 -0
- data/lib/shoko/core/services/navigation/absolute_layout.rb +101 -0
- data/lib/shoko/core/services/navigation/absolute_strategy.rb +179 -0
- data/lib/shoko/core/services/navigation/context_builder.rb +52 -0
- data/lib/shoko/core/services/navigation/context_helpers.rb +63 -0
- data/lib/shoko/core/services/navigation/dynamic_change_applier.rb +50 -0
- data/lib/shoko/core/services/navigation/dynamic_strategy.rb +51 -0
- data/lib/shoko/core/services/navigation/image_offset_snapper.rb +150 -0
- data/lib/shoko/core/services/navigation/nav_context.rb +27 -0
- data/lib/shoko/core/services/navigation/state_updater.rb +29 -0
- data/lib/shoko/core/services/navigation/strategy_factory.rb +20 -0
- data/lib/shoko/core/services/navigation_service.rb +150 -0
- data/lib/shoko/core/services/page_calculator_service.rb +242 -0
- data/lib/shoko/core/services/pagination/internal/absolute_page_map_builder.rb +28 -0
- data/lib/shoko/core/services/pagination/internal/chapter_cache.rb +60 -0
- data/lib/shoko/core/services/pagination/internal/dynamic_page_map_builder.rb +157 -0
- data/lib/shoko/core/services/pagination/internal/layout_metrics_calculator.rb +73 -0
- data/lib/shoko/core/services/pagination/internal/page_hydrator.rb +145 -0
- data/lib/shoko/core/services/pagination/internal/pagination_workflow.rb +152 -0
- data/lib/shoko/core/services/pagination/page_info_calculator.rb +247 -0
- data/lib/shoko/core/services/pagination/pagination_cache_preloader.rb +173 -0
- data/lib/shoko/core/services/pagination/pagination_coordinator.rb +202 -0
- data/lib/shoko/core/services/pagination/pagination_orchestrator.rb +291 -0
- data/lib/shoko/core/services/pagination.rb +10 -0
- data/lib/shoko/core/services/progress_helper.rb +22 -0
- data/lib/shoko/core/services/selection_service.rb +126 -0
- data/lib/shoko/core/validator.rb +76 -0
- data/lib/shoko/shared/errors.rb +97 -0
- data/lib/shoko/shared/version.rb +5 -0
- data/lib/shoko/test_support/terminal_double.rb +175 -0
- data/lib/shoko/test_support/test_mode.rb +78 -0
- data/lib/shoko.rb +279 -0
- data/lib/zip.rb +732 -0
- data/zip.rb +5 -0
- metadata +370 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 4da504e314b4ccf14caada8bcdb128ea1e128c3eb391e51ee85ef41529003fb5
|
|
4
|
+
data.tar.gz: 356baf7e71baf08181a92eb4d582c3e21e248ce121414c6ea1313c881f54ca9c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c31d6dac31624b683b5bb672174f5d47c1943d63bd8f2677806e42cd48943f9bd2ac83c73d72f9d4b4ace1416423d4361b00d688b23649b9a80edd7c4c839a27
|
|
7
|
+
data.tar.gz: 52f71139fc47843e8d078ad5fc23a874d8b0807002275781f6bbb99ac01f58b73ca216f2af3218f8905a3070ee5d6726108355d902cc276b760e141e7d4644a7
|
data/.bundle/config
ADDED
data/.bundle/config.bak
ADDED
data/.rspec_status
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
example_id | status | run_time |
|
|
2
|
+
----------------------------------------------- | ------ | --------------- |
|
|
3
|
+
./spec/comprehensive_coverage_spec.rb[1:1:1:1] | passed | 0.0089 seconds |
|
|
4
|
+
./spec/comprehensive_coverage_spec.rb[1:1:1:2] | passed | 0.00391 seconds |
|
|
5
|
+
./spec/comprehensive_coverage_spec.rb[1:1:1:3] | passed | 0.00181 seconds |
|
|
6
|
+
./spec/comprehensive_coverage_spec.rb[1:1:1:4] | passed | 0.00271 seconds |
|
|
7
|
+
./spec/comprehensive_coverage_spec.rb[1:1:1:5] | failed | 0.00894 seconds |
|
|
8
|
+
./spec/comprehensive_coverage_spec.rb[1:1:1:6] | passed | 0.00246 seconds |
|
|
9
|
+
./spec/comprehensive_coverage_spec.rb[1:1:1:7] | failed | 0.00194 seconds |
|
|
10
|
+
./spec/comprehensive_coverage_spec.rb[1:1:1:8] | passed | 0.00167 seconds |
|
|
11
|
+
./spec/comprehensive_coverage_spec.rb[1:1:1:9] | passed | 0.00327 seconds |
|
|
12
|
+
./spec/comprehensive_coverage_spec.rb[1:1:1:10] | passed | 0.00172 seconds |
|
|
13
|
+
./spec/comprehensive_coverage_spec.rb[1:1:1:11] | passed | 0.22362 seconds |
|
|
14
|
+
./spec/comprehensive_coverage_spec.rb[1:1:2:1] | failed | 0.00262 seconds |
|
|
15
|
+
./spec/comprehensive_coverage_spec.rb[1:1:2:2] | passed | 0.00312 seconds |
|
|
16
|
+
./spec/comprehensive_coverage_spec.rb[1:1:2:3] | passed | 0.0047 seconds |
|
|
17
|
+
./spec/comprehensive_coverage_spec.rb[1:1:3:1] | passed | 0.0022 seconds |
|
|
18
|
+
./spec/comprehensive_coverage_spec.rb[1:1:3:2] | failed | 0.00193 seconds |
|
|
19
|
+
./spec/comprehensive_coverage_spec.rb[1:1:3:3] | passed | 0.00264 seconds |
|
|
20
|
+
./spec/comprehensive_coverage_spec.rb[1:2:1:1] | passed | 0.00124 seconds |
|
|
21
|
+
./spec/comprehensive_coverage_spec.rb[1:2:1:2] | passed | 0.00094 seconds |
|
|
22
|
+
./spec/comprehensive_coverage_spec.rb[1:2:1:3] | passed | 0.00128 seconds |
|
|
23
|
+
./spec/comprehensive_coverage_spec.rb[1:2:1:4] | passed | 0.0018 seconds |
|
|
24
|
+
./spec/comprehensive_coverage_spec.rb[1:2:2:1] | passed | 0.00142 seconds |
|
|
25
|
+
./spec/comprehensive_coverage_spec.rb[1:2:2:2] | passed | 0.00105 seconds |
|
|
26
|
+
./spec/comprehensive_coverage_spec.rb[1:2:3:1] | passed | 0.00189 seconds |
|
|
27
|
+
./spec/comprehensive_coverage_spec.rb[1:2:3:2] | passed | 0.00288 seconds |
|
|
28
|
+
./spec/comprehensive_coverage_spec.rb[1:2:4:1] | passed | 0.00163 seconds |
|
|
29
|
+
./spec/comprehensive_coverage_spec.rb[1:2:4:2] | passed | 0.00181 seconds |
|
|
30
|
+
./spec/comprehensive_coverage_spec.rb[1:3:1:1] | passed | 0.0228 seconds |
|
|
31
|
+
./spec/comprehensive_coverage_spec.rb[1:3:1:2] | passed | 0.00391 seconds |
|
|
32
|
+
./spec/comprehensive_coverage_spec.rb[1:4:1:1] | passed | 0.00306 seconds |
|
|
33
|
+
./spec/comprehensive_coverage_spec.rb[1:4:1:2] | passed | 0.00191 seconds |
|
|
34
|
+
./spec/comprehensive_coverage_spec.rb[1:4:1:3] | passed | 0.00109 seconds |
|
|
35
|
+
./spec/comprehensive_coverage_spec.rb[1:5:1:1] | passed | 0.00094 seconds |
|
|
36
|
+
./spec/comprehensive_coverage_spec.rb[1:5:1:2] | passed | 0.00254 seconds |
|
|
37
|
+
./spec/comprehensive_coverage_spec.rb[1:6:1:1] | passed | 0.00089 seconds |
|
|
38
|
+
./spec/comprehensive_coverage_spec.rb[1:6:1:2] | passed | 0.00071 seconds |
|
|
39
|
+
./spec/comprehensive_coverage_spec.rb[1:6:1:3] | passed | 0.0591 seconds |
|
|
40
|
+
./spec/infrastructure_validator_spec.rb[1:1] | passed | 0.00546 seconds |
|
|
41
|
+
./spec/infrastructure_validator_spec.rb[1:2] | passed | 0.00201 seconds |
|
|
42
|
+
./spec/infrastructure_validator_spec.rb[1:3] | passed | 0.00107 seconds |
|
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
plugins:
|
|
2
|
+
- rubocop-performance
|
|
3
|
+
- rubocop-rails
|
|
4
|
+
|
|
5
|
+
AllCops:
|
|
6
|
+
TargetRubyVersion: 3.3
|
|
7
|
+
NewCops: enable
|
|
8
|
+
Exclude:
|
|
9
|
+
- 'vendor/**/*'
|
|
10
|
+
- 'tmp/**/*'
|
|
11
|
+
|
|
12
|
+
# Layout
|
|
13
|
+
Layout/LineLength:
|
|
14
|
+
Max: 120
|
|
15
|
+
Exclude:
|
|
16
|
+
- 'spec/**/*'
|
|
17
|
+
- 'test/**/*'
|
|
18
|
+
|
|
19
|
+
# Metrics
|
|
20
|
+
Metrics/ClassLength:
|
|
21
|
+
Max: 200
|
|
22
|
+
CountComments: false
|
|
23
|
+
Exclude:
|
|
24
|
+
- 'spec/**/*'
|
|
25
|
+
- 'test/**/*'
|
|
26
|
+
|
|
27
|
+
Metrics/ModuleLength:
|
|
28
|
+
Max: 200
|
|
29
|
+
CountComments: false
|
|
30
|
+
Exclude:
|
|
31
|
+
- 'spec/**/*'
|
|
32
|
+
- 'test/**/*'
|
|
33
|
+
|
|
34
|
+
Metrics/MethodLength:
|
|
35
|
+
Max: 15
|
|
36
|
+
CountComments: false
|
|
37
|
+
AllowedMethods:
|
|
38
|
+
- 'initialize'
|
|
39
|
+
Exclude:
|
|
40
|
+
- 'spec/**/*'
|
|
41
|
+
- 'test/**/*'
|
|
42
|
+
|
|
43
|
+
Metrics/AbcSize:
|
|
44
|
+
Max: 20
|
|
45
|
+
Exclude:
|
|
46
|
+
- 'spec/**/*'
|
|
47
|
+
- 'test/**/*'
|
|
48
|
+
|
|
49
|
+
Metrics/CyclomaticComplexity:
|
|
50
|
+
Max: 8
|
|
51
|
+
Exclude:
|
|
52
|
+
- 'spec/**/*'
|
|
53
|
+
- 'test/**/*'
|
|
54
|
+
|
|
55
|
+
Metrics/PerceivedComplexity:
|
|
56
|
+
Max: 9
|
|
57
|
+
Exclude:
|
|
58
|
+
- 'spec/**/*'
|
|
59
|
+
- 'test/**/*'
|
|
60
|
+
|
|
61
|
+
Metrics/ParameterLists:
|
|
62
|
+
Max: 4
|
|
63
|
+
CountKeywordArgs: false
|
|
64
|
+
Exclude:
|
|
65
|
+
- 'spec/**/*'
|
|
66
|
+
- 'test/**/*'
|
|
67
|
+
|
|
68
|
+
Metrics/BlockLength:
|
|
69
|
+
Max: 35
|
|
70
|
+
Exclude:
|
|
71
|
+
- 'spec/**/*'
|
|
72
|
+
- 'test/**/*'
|
|
73
|
+
|
|
74
|
+
# Style
|
|
75
|
+
Style/Documentation:
|
|
76
|
+
Enabled: true
|
|
77
|
+
Exclude:
|
|
78
|
+
- 'spec/**/*'
|
|
79
|
+
|
|
80
|
+
Style/StringLiterals:
|
|
81
|
+
EnforcedStyle: single_quotes
|
|
82
|
+
ConsistentQuotesInMultiline: true
|
|
83
|
+
|
|
84
|
+
Style/FrozenStringLiteralComment:
|
|
85
|
+
Enabled: true
|
|
86
|
+
EnforcedStyle: always
|
|
87
|
+
|
|
88
|
+
Style/GuardClause:
|
|
89
|
+
Enabled: true
|
|
90
|
+
MinBodyLength: 1
|
|
91
|
+
|
|
92
|
+
Style/ModuleFunction:
|
|
93
|
+
Enabled: true
|
|
94
|
+
EnforcedStyle: module_function
|
|
95
|
+
|
|
96
|
+
Style/TrailingCommaInArrayLiteral:
|
|
97
|
+
EnforcedStyleForMultiline: comma
|
|
98
|
+
|
|
99
|
+
Style/TrailingCommaInHashLiteral:
|
|
100
|
+
EnforcedStyleForMultiline: comma
|
|
101
|
+
|
|
102
|
+
# Security
|
|
103
|
+
Security/Eval:
|
|
104
|
+
Enabled: true
|
|
105
|
+
|
|
106
|
+
Security/Open:
|
|
107
|
+
Enabled: true
|
|
108
|
+
|
|
109
|
+
# Performance
|
|
110
|
+
Performance/Count:
|
|
111
|
+
Enabled: true
|
|
112
|
+
|
|
113
|
+
Performance/Detect:
|
|
114
|
+
Enabled: true
|
|
115
|
+
|
|
116
|
+
# Additional cops
|
|
117
|
+
Lint/UselessAssignment:
|
|
118
|
+
Enabled: true
|
|
119
|
+
|
|
120
|
+
Lint/DuplicateMethods:
|
|
121
|
+
Enabled: true
|
|
122
|
+
|
|
123
|
+
Rails:
|
|
124
|
+
Enabled: false
|
data/Gemfile
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
source 'https://rubygems.org'
|
|
4
|
+
|
|
5
|
+
gemspec
|
|
6
|
+
|
|
7
|
+
group :development do
|
|
8
|
+
gem 'reek'
|
|
9
|
+
gem 'rubocop', require: false
|
|
10
|
+
gem 'rubocop-performance', require: false
|
|
11
|
+
gem 'rubocop-rails', require: false
|
|
12
|
+
gem 'rubocop-rspec', require: false
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
group :test do
|
|
16
|
+
gem 'fakefs', require: 'fakefs/spec_helpers'
|
|
17
|
+
gem 'rspec'
|
|
18
|
+
gem 'simplecov', require: false
|
|
19
|
+
end
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Shayan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Shoko
|
|
2
|
+
|
|
3
|
+
Terminal ebook reader for EPUB files.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
- Scans common folders for EPUB files and shows them in a menu.
|
|
8
|
+
- Opens a specific file directly when a path is provided.
|
|
9
|
+
- Reads in split or single view with adjustable line spacing and themes.
|
|
10
|
+
- Provides a TOC sidebar, bookmarks, and annotations.
|
|
11
|
+
- Supports mouse selection for highlighting and annotation editing.
|
|
12
|
+
- Can download public-domain EPUBs from Gutendex.
|
|
13
|
+
- Optional Kitty inline image rendering (when supported).
|
|
14
|
+
|
|
15
|
+
## How it works
|
|
16
|
+
|
|
17
|
+
- `bin/start` runs the CLI and enters menu mode or opens a file directly.
|
|
18
|
+
- State lives in a single store; actions update state and selectors read it.
|
|
19
|
+
- Rendering is component-based and drawn through a terminal buffer with diff updates.
|
|
20
|
+
- Selection/highlighting uses recorded line geometry from the render pass.
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
From source:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
bundle install
|
|
28
|
+
bin/start
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Open a file directly:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
bin/start /path/to/book.epub
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Options:
|
|
38
|
+
|
|
39
|
+
- `-d`, `--debug` Enable debug logging.
|
|
40
|
+
- `--log PATH` Write JSON logs to PATH.
|
|
41
|
+
- `--log-level LEVEL` Set log level (`debug`, `info`, `warn`, `error`, `fatal`).
|
|
42
|
+
- `--profile PATH` Write a concise performance profile to PATH.
|
|
43
|
+
- `-h`, `--help` Show help.
|
|
44
|
+
|
|
45
|
+
## Controls (basics)
|
|
46
|
+
|
|
47
|
+
Menu:
|
|
48
|
+
|
|
49
|
+
- `j`/`k` or arrow keys to move
|
|
50
|
+
- `Enter` to select
|
|
51
|
+
- `Esc` to go back
|
|
52
|
+
- `/` to search in browse mode
|
|
53
|
+
- `q` to quit
|
|
54
|
+
|
|
55
|
+
Reader:
|
|
56
|
+
|
|
57
|
+
- `h`/`l` or arrow keys to change pages
|
|
58
|
+
- `j`/`k` to scroll
|
|
59
|
+
- `Space` for next page
|
|
60
|
+
- `t` for TOC
|
|
61
|
+
- `b` to add bookmark, `B` to open bookmarks
|
|
62
|
+
- `A` to open annotations
|
|
63
|
+
- `?` for help
|
|
64
|
+
- `q` to return to menu, `Q` to quit
|
|
65
|
+
|
|
66
|
+
## Data locations
|
|
67
|
+
|
|
68
|
+
- Config and data: `~/.config/shoko/`
|
|
69
|
+
- `config.json`
|
|
70
|
+
- `annotations.json`, `bookmarks.json`, `progress.json`, `recent.json`
|
|
71
|
+
- `downloads/` (Gutendex downloads)
|
|
72
|
+
- `epub_cache.json` (scan cache)
|
|
73
|
+
- Cache: `~/.cache/shoko/`
|
|
74
|
+
|
|
75
|
+
## Logging and profiling
|
|
76
|
+
|
|
77
|
+
You can also configure logging with environment variables:
|
|
78
|
+
|
|
79
|
+
- `DEBUG=1` Enable debug logging.
|
|
80
|
+
- `SHOKO_LOG_PATH=/path/to/log` Write JSON logs to a file.
|
|
81
|
+
- `SHOKO_LOG_LEVEL=info` Set log level.
|
|
82
|
+
- `SHOKO_PROFILE_PATH=/path/to/profile` Write a performance profile.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/gem_tasks'
|
|
4
|
+
require 'rspec/core/rake_task'
|
|
5
|
+
require 'rubocop/rake_task'
|
|
6
|
+
|
|
7
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
8
|
+
RuboCop::RakeTask.new
|
|
9
|
+
|
|
10
|
+
desc 'Run all quality checks'
|
|
11
|
+
task quality: %i[spec rubocop]
|
|
12
|
+
|
|
13
|
+
task default: :quality
|
|
14
|
+
|
|
15
|
+
namespace :test do
|
|
16
|
+
desc 'Run tests with coverage'
|
|
17
|
+
task :coverage do
|
|
18
|
+
ENV['COVERAGE'] = 'true'
|
|
19
|
+
Rake::Task['spec'].invoke
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
desc 'Console with library loaded'
|
|
24
|
+
task :console do
|
|
25
|
+
require 'irb'
|
|
26
|
+
require 'shoko'
|
|
27
|
+
ARGV.clear
|
|
28
|
+
IRB.start
|
|
29
|
+
end
|
data/bin/start
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
|
|
5
|
+
|
|
6
|
+
begin
|
|
7
|
+
require "bundler/setup"
|
|
8
|
+
rescue LoadError
|
|
9
|
+
# Bundler isn't required when running from a packaged gem.
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
13
|
+
require "shoko"
|
|
14
|
+
|
|
15
|
+
Shoko::CLI.run(ARGV)
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../monitoring/performance_monitor.rb'
|
|
4
|
+
require_relative 'epub_document'
|
|
5
|
+
|
|
6
|
+
module Shoko
|
|
7
|
+
module Adapters::BookSources
|
|
8
|
+
# Document service for loading and accessing EPUB content.
|
|
9
|
+
# Provides clean interface to document operations without coupling to controllers.
|
|
10
|
+
class DocumentService
|
|
11
|
+
def initialize(epub_path, wrapping_service = nil, formatting_service: nil, background_worker: nil,
|
|
12
|
+
progress_reporter: nil)
|
|
13
|
+
@epub_path = epub_path
|
|
14
|
+
@document = nil
|
|
15
|
+
@content_cache = {}
|
|
16
|
+
@wrapping_service = wrapping_service
|
|
17
|
+
@formatting_service = formatting_service
|
|
18
|
+
@background_worker = background_worker
|
|
19
|
+
@progress_reporter = progress_reporter
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Load the EPUB document
|
|
23
|
+
#
|
|
24
|
+
# @return [EPUBDocument] Loaded document
|
|
25
|
+
def load_document
|
|
26
|
+
@document ||= Adapters::Monitoring::PerformanceMonitor.time('import.document.load') do
|
|
27
|
+
EPUBDocument.new(@epub_path,
|
|
28
|
+
formatting_service: @formatting_service,
|
|
29
|
+
background_worker: @background_worker,
|
|
30
|
+
progress_reporter: @progress_reporter)
|
|
31
|
+
end
|
|
32
|
+
rescue StandardError => e
|
|
33
|
+
Adapters::Monitoring::Logger.error('Failed to load document', path: @epub_path, error: e.message)
|
|
34
|
+
create_error_document(e.message)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Get chapter by index
|
|
38
|
+
#
|
|
39
|
+
# @param index [Integer] Chapter index
|
|
40
|
+
# @return [Chapter] Chapter object or nil
|
|
41
|
+
def chapter_at(index)
|
|
42
|
+
return nil unless @document
|
|
43
|
+
|
|
44
|
+
@document.get_chapter(index)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Get table of contents
|
|
48
|
+
#
|
|
49
|
+
# @return [Array<Hash>] Array of TOC entries
|
|
50
|
+
def get_table_of_contents
|
|
51
|
+
return [] unless @document
|
|
52
|
+
|
|
53
|
+
@document.chapters.map.with_index do |chapter, index|
|
|
54
|
+
{
|
|
55
|
+
index: index,
|
|
56
|
+
title: chapter.title || "Chapter #{index + 1}",
|
|
57
|
+
level: 0, # Could be enhanced to support nested TOC
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Get content for specific page
|
|
63
|
+
#
|
|
64
|
+
# @param chapter_index [Integer] Chapter index
|
|
65
|
+
# @param page_offset [Integer] Page offset within chapter
|
|
66
|
+
# @param lines_per_page [Integer] Number of lines per page
|
|
67
|
+
# @return [Array<String>] Array of content lines
|
|
68
|
+
def get_page_content(chapter_index, page_offset, lines_per_page = 20)
|
|
69
|
+
cache_key = "#{chapter_index}_#{page_offset}_#{lines_per_page}"
|
|
70
|
+
cached_fetch(cache_key, default: []) do
|
|
71
|
+
with_chapter(chapter_index, default: []) do |chapter|
|
|
72
|
+
lines = chapter.lines || []
|
|
73
|
+
start_line = page_offset * lines_per_page
|
|
74
|
+
end_line = start_line + lines_per_page - 1
|
|
75
|
+
lines[start_line..end_line] || []
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Get wrapped content for specific page
|
|
81
|
+
#
|
|
82
|
+
# @param chapter_index [Integer] Chapter index
|
|
83
|
+
# @param page_offset [Integer] Page offset within chapter
|
|
84
|
+
# @param column_width [Integer] Column width for wrapping
|
|
85
|
+
# @param lines_per_page [Integer] Number of lines per page
|
|
86
|
+
# @return [Array<String>] Array of wrapped content lines
|
|
87
|
+
def get_wrapped_page_content(chapter_index, page_offset, column_width, lines_per_page = 20)
|
|
88
|
+
cache_key = "wrapped_#{chapter_index}_#{page_offset}_#{column_width}_#{lines_per_page}"
|
|
89
|
+
cached_fetch(cache_key, default: []) do
|
|
90
|
+
with_chapter(chapter_index, default: []) do |chapter|
|
|
91
|
+
lines = chapter.lines || []
|
|
92
|
+
wrapped_lines = wrap_lines(chapter_index, lines, column_width)
|
|
93
|
+
start_line = page_offset * lines_per_page
|
|
94
|
+
end_line = start_line + lines_per_page - 1
|
|
95
|
+
wrapped_lines[start_line..end_line] || []
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Get total wrapped lines for chapter
|
|
101
|
+
#
|
|
102
|
+
# @param chapter_index [Integer] Chapter index
|
|
103
|
+
# @param column_width [Integer] Column width for wrapping
|
|
104
|
+
# @return [Integer] Total wrapped lines
|
|
105
|
+
def get_chapter_wrapped_line_count(chapter_index, column_width)
|
|
106
|
+
cache_key = "line_count_#{chapter_index}_#{column_width}"
|
|
107
|
+
cached_fetch(cache_key, default: 0) do
|
|
108
|
+
with_chapter(chapter_index, default: 0) do |chapter|
|
|
109
|
+
lines = chapter.lines || []
|
|
110
|
+
wrapped_lines = wrap_lines(chapter_index, lines, column_width)
|
|
111
|
+
wrapped_lines.size
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Clear content cache
|
|
117
|
+
def clear_cache
|
|
118
|
+
@content_cache.clear
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Clear cache for specific width
|
|
122
|
+
#
|
|
123
|
+
# @param width [Integer] Column width
|
|
124
|
+
def clear_cache_for_width(width)
|
|
125
|
+
@content_cache.delete_if { |key, _| key.include?("_#{width}_") }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private
|
|
129
|
+
|
|
130
|
+
def create_error_document(error_message)
|
|
131
|
+
# Create a simple document with error information
|
|
132
|
+
ErrorDocument.new(error_message)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def wrap_lines(chapter_index, lines, column_width)
|
|
136
|
+
return lines if column_width <= 0
|
|
137
|
+
return @wrapping_service.wrap_lines(lines, chapter_index, column_width) if @wrapping_service
|
|
138
|
+
|
|
139
|
+
# Minimal fallback for tests/dev without DI
|
|
140
|
+
lines
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def cached_fetch(key, default: nil)
|
|
144
|
+
return @content_cache[key] if @content_cache.key?(key)
|
|
145
|
+
|
|
146
|
+
value = yield
|
|
147
|
+
value = default if value.nil?
|
|
148
|
+
@content_cache[key] = value
|
|
149
|
+
value
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def with_chapter(index, default: nil)
|
|
153
|
+
chapter = chapter_at(index)
|
|
154
|
+
return default unless chapter
|
|
155
|
+
|
|
156
|
+
yield chapter
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Simple error document for when EPUB loading fails
|
|
161
|
+
class ErrorDocument
|
|
162
|
+
attr_reader :error_message
|
|
163
|
+
|
|
164
|
+
def initialize(error_message)
|
|
165
|
+
@error_message = error_message
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def chapter_count
|
|
169
|
+
1
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def get_chapter(index)
|
|
173
|
+
return nil unless index.zero?
|
|
174
|
+
|
|
175
|
+
ErrorChapter.new(@error_message)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def chapters
|
|
179
|
+
[get_chapter(0)]
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Simple error chapter
|
|
184
|
+
class ErrorChapter
|
|
185
|
+
attr_reader :title, :lines
|
|
186
|
+
|
|
187
|
+
def initialize(error_message)
|
|
188
|
+
@title = 'Error Loading Book'
|
|
189
|
+
@lines = [
|
|
190
|
+
'Failed to load the EPUB file:',
|
|
191
|
+
'',
|
|
192
|
+
error_message,
|
|
193
|
+
'',
|
|
194
|
+
'Please check that the file exists and is a valid EPUB.',
|
|
195
|
+
'',
|
|
196
|
+
"Press 'q' to return to the main menu.",
|
|
197
|
+
]
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require_relative '../../core/services/base_service.rb'
|
|
5
|
+
require_relative '../storage/config_paths'
|
|
6
|
+
|
|
7
|
+
module Shoko
|
|
8
|
+
module Adapters::BookSources
|
|
9
|
+
# Coordinates Gutendex search + download to the local library.
|
|
10
|
+
class DownloadService < BaseService
|
|
11
|
+
class DownloadError < StandardError; end
|
|
12
|
+
|
|
13
|
+
def search(query:, page_url: nil)
|
|
14
|
+
payload = client.search(query: query, page_url: page_url)
|
|
15
|
+
{
|
|
16
|
+
count: payload['count'].to_i,
|
|
17
|
+
next: payload['next'],
|
|
18
|
+
previous: payload['previous'],
|
|
19
|
+
books: normalize_books(payload['results']),
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def download(book)
|
|
24
|
+
url = pick_download_url(book)
|
|
25
|
+
raise DownloadError, 'No EPUB format available' unless url
|
|
26
|
+
|
|
27
|
+
dest_dir = downloads_root
|
|
28
|
+
FileUtils.mkdir_p(dest_dir)
|
|
29
|
+
dest_path = File.join(dest_dir, filename_for(book))
|
|
30
|
+
return { path: dest_path, existing: true } if File.exist?(dest_path)
|
|
31
|
+
|
|
32
|
+
client.download(url, dest_path) { |done, total| yield(done, total) if block_given? }
|
|
33
|
+
{ path: dest_path, existing: false }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
protected
|
|
37
|
+
|
|
38
|
+
def required_dependencies
|
|
39
|
+
[:gutendex_client]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def client
|
|
45
|
+
@client ||= resolve(:gutendex_client)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def downloads_root
|
|
49
|
+
Adapters::Storage::ConfigPaths.downloads_root
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def normalize_books(items)
|
|
53
|
+
Array(items).map do |raw|
|
|
54
|
+
{
|
|
55
|
+
id: raw['id'],
|
|
56
|
+
title: raw['title'],
|
|
57
|
+
authors: Array(raw['authors']).filter_map { |a| a['name'] },
|
|
58
|
+
languages: Array(raw['languages']).map(&:to_s),
|
|
59
|
+
download_count: raw['download_count'],
|
|
60
|
+
formats: raw['formats'] || {},
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def pick_download_url(book)
|
|
66
|
+
formats = value_for(book, :formats, 'formats', {})
|
|
67
|
+
return nil unless formats.respond_to?(:each)
|
|
68
|
+
|
|
69
|
+
keys = formats.keys.map(&:to_s)
|
|
70
|
+
epub_key = keys.find { |k| k.start_with?('application/epub+zip') } ||
|
|
71
|
+
keys.find { |k| k.include?('application/epub') } ||
|
|
72
|
+
keys.find { |k| k.include?('epub') }
|
|
73
|
+
return nil unless epub_key
|
|
74
|
+
|
|
75
|
+
formats[epub_key] || formats[epub_key.to_sym]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def filename_for(book)
|
|
79
|
+
id = value_for(book, :id, 'id', 'book').to_s
|
|
80
|
+
title = value_for(book, :title, 'title', 'book').to_s
|
|
81
|
+
slug = title.downcase.gsub(/[^a-z0-9]+/, '-').gsub(/^-|-$/, '')
|
|
82
|
+
slug = "book-#{id}" if slug.empty?
|
|
83
|
+
"#{slug}-#{id}.epub"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def value_for(book, key_sym, key_str, default)
|
|
87
|
+
return book[key_sym] if book.respond_to?(:key?) && book.key?(key_sym)
|
|
88
|
+
return book[key_str] if book.respond_to?(:key?) && book.key?(key_str)
|
|
89
|
+
|
|
90
|
+
default
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|