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
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../core/services/base_service.rb'
|
|
4
|
+
require_relative '../../adapters/book_sources/epub/parsers/metadata_extractor.rb'
|
|
5
|
+
require_relative '../../adapters/storage/recent_files'
|
|
6
|
+
require_relative '../../adapters/storage/repositories/cached_library_repository'
|
|
7
|
+
|
|
8
|
+
module Shoko
|
|
9
|
+
module Application::UseCases
|
|
10
|
+
# Facade providing catalog data (cached books, scan status, metadata) to higher layers.
|
|
11
|
+
# Wraps the infrastructure scanner/metadata helpers so presentation never touches them directly.
|
|
12
|
+
class CatalogService < BaseService
|
|
13
|
+
def initialize(dependencies)
|
|
14
|
+
super
|
|
15
|
+
@scanner = resolve(:library_scanner)
|
|
16
|
+
@cached_library_repository = resolve(:cached_library_repository) if registered?(:cached_library_repository)
|
|
17
|
+
@metadata_cache = {}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def required_dependencies
|
|
21
|
+
[:library_scanner]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def load_cached
|
|
25
|
+
@scanner.load_cached
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def cached_library_entries
|
|
29
|
+
return [] unless @cached_library_repository
|
|
30
|
+
|
|
31
|
+
entries = @cached_library_repository.list_entries
|
|
32
|
+
return [] if entries.empty?
|
|
33
|
+
|
|
34
|
+
recent_index = index_recent_by_path
|
|
35
|
+
entries.each do |entry|
|
|
36
|
+
path = entry[:epub_path] || entry['epub_path']
|
|
37
|
+
entry[:last_accessed] = recent_index[path] if path
|
|
38
|
+
end
|
|
39
|
+
entries
|
|
40
|
+
rescue StandardError
|
|
41
|
+
[]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def start_scan(force: false)
|
|
45
|
+
@scanner.start_scan(force: force)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def process_results
|
|
49
|
+
results = @scanner.process_results
|
|
50
|
+
update_entries(results) if results
|
|
51
|
+
results
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def entries
|
|
55
|
+
@scanner.epubs || []
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def update_entries(entries)
|
|
59
|
+
@scanner.epubs = entries
|
|
60
|
+
clear_metadata_cache
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def scan_status
|
|
64
|
+
@scanner.scan_status
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def scan_status=(value)
|
|
68
|
+
@scanner.scan_status = value
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def scan_message
|
|
72
|
+
@scanner.scan_message
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def scan_message=(value)
|
|
76
|
+
@scanner.scan_message = value
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def cleanup
|
|
80
|
+
@scanner.cleanup if @scanner.respond_to?(:cleanup)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def metadata_for(path)
|
|
84
|
+
return {} unless path
|
|
85
|
+
|
|
86
|
+
@metadata_cache[path] ||= begin
|
|
87
|
+
Adapters::BookSources::Epub::Parsers::MetadataExtractor.from_epub(path)
|
|
88
|
+
rescue StandardError
|
|
89
|
+
{}
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def size_for(path)
|
|
94
|
+
return 0 unless path
|
|
95
|
+
|
|
96
|
+
File.size(path)
|
|
97
|
+
rescue StandardError
|
|
98
|
+
0
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def clear_metadata_cache
|
|
102
|
+
@metadata_cache.clear
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def index_recent_by_path
|
|
108
|
+
items = Adapters::Storage::RecentFiles.load
|
|
109
|
+
Array(items).each_with_object({}) do |recent_item, acc|
|
|
110
|
+
path = recent_item['path'] || recent_item[:path]
|
|
111
|
+
accessed = recent_item['accessed'] || recent_item[:accessed]
|
|
112
|
+
acc[path] = accessed if path && accessed
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_command'
|
|
4
|
+
|
|
5
|
+
module Shoko
|
|
6
|
+
module Application
|
|
7
|
+
module Commands
|
|
8
|
+
# Commands for driving the Annotation Editor screen via a clean, public API.
|
|
9
|
+
class AnnotationEditorCommand < BaseCommand
|
|
10
|
+
def initialize(action, name: nil, description: nil)
|
|
11
|
+
@action = action
|
|
12
|
+
super(
|
|
13
|
+
name: name || "annotation_editor_#{action}",
|
|
14
|
+
description: description || "Annotation editor #{action.to_s.tr('_', ' ')}"
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
protected
|
|
19
|
+
|
|
20
|
+
def perform(context, params = {})
|
|
21
|
+
ui = context.dependencies.resolve(:ui_controller) if context.respond_to?(:dependencies)
|
|
22
|
+
# Prefer a context-provided editor component (menu or reader), else fall back to UI current_mode
|
|
23
|
+
mode = if context.respond_to?(:current_editor_component)
|
|
24
|
+
context.current_editor_component
|
|
25
|
+
else
|
|
26
|
+
ui&.current_mode
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
case @action
|
|
30
|
+
when :save
|
|
31
|
+
return :handled if dispatched_to_mode?(mode, :save_annotation)
|
|
32
|
+
when :cancel
|
|
33
|
+
# Reader: cleanup + back to read; Menu: back to annotations
|
|
34
|
+
if dispatched_to_mode?(mode, :cancel_annotation)
|
|
35
|
+
# Mode handled the cancel path completely
|
|
36
|
+
elsif ui
|
|
37
|
+
begin
|
|
38
|
+
ui.cleanup_popup_state
|
|
39
|
+
rescue StandardError
|
|
40
|
+
# no-op (menu has no popup state)
|
|
41
|
+
end
|
|
42
|
+
begin
|
|
43
|
+
ui.switch_mode(:read)
|
|
44
|
+
rescue StandardError
|
|
45
|
+
# fall through to menu path
|
|
46
|
+
end
|
|
47
|
+
else
|
|
48
|
+
# Menu context: switch to annotations screen
|
|
49
|
+
begin
|
|
50
|
+
context.state.dispatch(Shoko::Application::Actions::UpdateMenuAction.new(mode: :annotations))
|
|
51
|
+
rescue StandardError
|
|
52
|
+
# best-effort
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
when :backspace
|
|
56
|
+
return :handled if dispatched_to_mode?(mode, :handle_backspace)
|
|
57
|
+
when :enter
|
|
58
|
+
return :handled if dispatched_to_mode?(mode, :handle_enter)
|
|
59
|
+
when :insert_char
|
|
60
|
+
ch = (params[:key] || '').to_s
|
|
61
|
+
return :pass if ch.empty?
|
|
62
|
+
|
|
63
|
+
return :handled if dispatched_to_mode?(mode, :handle_character, ch)
|
|
64
|
+
else
|
|
65
|
+
return :pass
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
:handled
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def dispatched_to_mode?(mode, method_name, *)
|
|
74
|
+
return false unless mode.respond_to?(method_name)
|
|
75
|
+
|
|
76
|
+
mode.public_send(method_name, *)
|
|
77
|
+
true
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Factory methods for building AnnotationEditor commands.
|
|
82
|
+
module AnnotationEditorCommandFactory
|
|
83
|
+
def self.save
|
|
84
|
+
AnnotationEditorCommand.new(:save)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def self.cancel
|
|
88
|
+
AnnotationEditorCommand.new(:cancel)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def self.backspace
|
|
92
|
+
AnnotationEditorCommand.new(:backspace)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def self.enter
|
|
96
|
+
AnnotationEditorCommand.new(:enter)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def self.insert_char
|
|
100
|
+
AnnotationEditorCommand.new(:insert_char)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_command'
|
|
4
|
+
|
|
5
|
+
module Shoko
|
|
6
|
+
module Application
|
|
7
|
+
module Commands
|
|
8
|
+
# Application-level commands for mode switching and system control.
|
|
9
|
+
class ApplicationCommand < BaseCommand
|
|
10
|
+
def initialize(action, name: nil, description: nil)
|
|
11
|
+
@action = action
|
|
12
|
+
super(
|
|
13
|
+
name: name || "app_#{action}",
|
|
14
|
+
description: description || "Application #{action.to_s.tr('_', ' ')}"
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
protected
|
|
19
|
+
|
|
20
|
+
def perform(context, _params = {})
|
|
21
|
+
deps = dependencies_from(context)
|
|
22
|
+
|
|
23
|
+
case @action
|
|
24
|
+
when :quit_to_menu
|
|
25
|
+
handle_quit_to_menu(deps)
|
|
26
|
+
when :quit_application
|
|
27
|
+
handle_quit_application(deps)
|
|
28
|
+
when :toggle_view_mode
|
|
29
|
+
handle_toggle_view_mode(deps)
|
|
30
|
+
when :show_help
|
|
31
|
+
handle_show_help(deps)
|
|
32
|
+
when :show_toc
|
|
33
|
+
handle_show_toc(deps)
|
|
34
|
+
when :show_bookmarks
|
|
35
|
+
handle_show_bookmarks(deps)
|
|
36
|
+
when :show_annotations
|
|
37
|
+
handle_show_annotations(deps)
|
|
38
|
+
else
|
|
39
|
+
raise ExecutionError.new("Unknown application action: #{@action}", command_name: name)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
@action
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def handle_quit_to_menu(deps)
|
|
48
|
+
controller = resolve_optional(deps, :state_controller)
|
|
49
|
+
if controller.respond_to?(:quit_to_menu)
|
|
50
|
+
controller.quit_to_menu
|
|
51
|
+
else
|
|
52
|
+
dispatch_action(deps, Application::Actions::QuitToMenuAction.new)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def handle_quit_application(deps)
|
|
57
|
+
controller = resolve_optional(deps, :state_controller)
|
|
58
|
+
if controller.respond_to?(:quit_application)
|
|
59
|
+
controller.quit_application
|
|
60
|
+
return
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
handle_quit_to_menu(deps)
|
|
64
|
+
force_cleanup(deps)
|
|
65
|
+
Kernel.exit(0)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def handle_toggle_view_mode(deps)
|
|
69
|
+
controller = resolve_optional(deps, :ui_controller)
|
|
70
|
+
if controller.respond_to?(:toggle_view_mode)
|
|
71
|
+
controller.toggle_view_mode
|
|
72
|
+
else
|
|
73
|
+
dispatch_action(deps, Application::Actions::ToggleViewModeAction.new)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def handle_show_help(deps)
|
|
78
|
+
controller = resolve_optional(deps, :ui_controller)
|
|
79
|
+
if controller.respond_to?(:show_help)
|
|
80
|
+
controller.show_help
|
|
81
|
+
else
|
|
82
|
+
state_store = resolve_state_store(deps)
|
|
83
|
+
state_store&.set(%i[reader mode], :help)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def handle_show_toc(deps)
|
|
88
|
+
controller = resolve_optional(deps, :ui_controller)
|
|
89
|
+
return unless controller.respond_to?(:open_toc)
|
|
90
|
+
|
|
91
|
+
controller.open_toc
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def handle_show_bookmarks(deps)
|
|
95
|
+
controller = resolve_optional(deps, :ui_controller)
|
|
96
|
+
return unless controller.respond_to?(:open_bookmarks)
|
|
97
|
+
|
|
98
|
+
controller.open_bookmarks
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def handle_show_annotations(deps)
|
|
102
|
+
controller = resolve_optional(deps, :ui_controller)
|
|
103
|
+
return unless controller.respond_to?(:open_annotations)
|
|
104
|
+
|
|
105
|
+
controller.open_annotations
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def dependencies_from(context)
|
|
109
|
+
return context.dependencies if context.respond_to?(:dependencies)
|
|
110
|
+
|
|
111
|
+
raise ExecutionError.new('Command context must expose dependencies', command_name: name)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def resolve_optional(deps, key)
|
|
115
|
+
return nil if deps.respond_to?(:registered?) && !deps.registered?(key)
|
|
116
|
+
|
|
117
|
+
deps.resolve(key)
|
|
118
|
+
rescue StandardError
|
|
119
|
+
nil
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def resolve_state_store(deps)
|
|
123
|
+
resolve_optional(deps, :state_store) || resolve_optional(deps, :global_state)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def dispatch_action(deps, action)
|
|
127
|
+
state_store = resolve_state_store(deps)
|
|
128
|
+
state_store&.dispatch(action)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def force_cleanup(deps)
|
|
132
|
+
terminal = resolve_optional(deps, :terminal_service)
|
|
133
|
+
return unless terminal
|
|
134
|
+
|
|
135
|
+
if terminal.respond_to?(:force_cleanup)
|
|
136
|
+
terminal.force_cleanup
|
|
137
|
+
elsif terminal.respond_to?(:cleanup)
|
|
138
|
+
terminal.cleanup
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Command that switches the reader into a specific UI mode.
|
|
144
|
+
class ModeCommand < BaseCommand
|
|
145
|
+
def initialize(mode, name: nil, description: nil)
|
|
146
|
+
@mode = mode
|
|
147
|
+
super(
|
|
148
|
+
name: name || "mode_#{mode}",
|
|
149
|
+
description: description || "Switch to #{mode} mode"
|
|
150
|
+
)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def validate_parameters(params)
|
|
154
|
+
super
|
|
155
|
+
|
|
156
|
+
valid_modes = %i[read help search]
|
|
157
|
+
return if valid_modes.include?(@mode)
|
|
158
|
+
|
|
159
|
+
raise ValidationError.new("Mode must be one of #{valid_modes}", command_name: name)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
protected
|
|
163
|
+
|
|
164
|
+
def perform(context, _params = {})
|
|
165
|
+
state_store = context.dependencies.resolve(:state_store)
|
|
166
|
+
state_store.set(%i[reader mode], @mode)
|
|
167
|
+
|
|
168
|
+
@mode
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Factory methods for common application commands
|
|
173
|
+
module ApplicationCommandFactory
|
|
174
|
+
def self.quit_to_menu
|
|
175
|
+
ApplicationCommand.new(:quit_to_menu)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def self.quit_application
|
|
179
|
+
ApplicationCommand.new(:quit_application)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def self.toggle_view_mode
|
|
183
|
+
ApplicationCommand.new(:toggle_view_mode)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def self.show_help
|
|
187
|
+
ApplicationCommand.new(:show_help)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def self.show_toc
|
|
191
|
+
ApplicationCommand.new(:show_toc)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def self.show_bookmarks
|
|
195
|
+
ApplicationCommand.new(:show_bookmarks)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def self.show_annotations
|
|
199
|
+
ApplicationCommand.new(:show_annotations)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def self.switch_to_mode(mode)
|
|
203
|
+
ModeCommand.new(mode)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shoko
|
|
4
|
+
module Application
|
|
5
|
+
module Commands
|
|
6
|
+
# Enhanced base command with proper error handling and validation.
|
|
7
|
+
# Replaces the mixed command patterns with consistent implementation.
|
|
8
|
+
class BaseCommand
|
|
9
|
+
# Base error type for command execution and validation failures.
|
|
10
|
+
class CommandError < StandardError
|
|
11
|
+
attr_reader :command_name, :context
|
|
12
|
+
|
|
13
|
+
def initialize(message, command_name: nil, context: nil)
|
|
14
|
+
super(message)
|
|
15
|
+
@command_name = command_name
|
|
16
|
+
@context = context
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class ValidationError < CommandError; end
|
|
21
|
+
class ExecutionError < CommandError; end
|
|
22
|
+
|
|
23
|
+
attr_reader :name, :description
|
|
24
|
+
|
|
25
|
+
def initialize(name: nil, description: nil, logger: nil)
|
|
26
|
+
@name = name || self.class.name.split('::').last
|
|
27
|
+
@description = description || @name
|
|
28
|
+
@logger = logger
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Execute command with full error handling and validation
|
|
32
|
+
#
|
|
33
|
+
# @param context [Object] Execution context (controller, service, etc.)
|
|
34
|
+
# @param params [Hash] Command parameters
|
|
35
|
+
# @return [Symbol] :handled, :pass, or :error
|
|
36
|
+
def execute(context, params = {})
|
|
37
|
+
validate_context(context)
|
|
38
|
+
validate_parameters(params)
|
|
39
|
+
|
|
40
|
+
return :pass unless can_execute?(context, params)
|
|
41
|
+
|
|
42
|
+
begin
|
|
43
|
+
result = perform(context, params)
|
|
44
|
+
handle_success(context, result)
|
|
45
|
+
:handled
|
|
46
|
+
rescue StandardError => e
|
|
47
|
+
handle_error(context, e, params)
|
|
48
|
+
:error
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Check if command can be executed
|
|
53
|
+
#
|
|
54
|
+
# @param context [Object] Execution context
|
|
55
|
+
# @param params [Hash] Command parameters
|
|
56
|
+
# @return [Boolean]
|
|
57
|
+
def can_execute?(_context, _params = {})
|
|
58
|
+
true # Override in subclasses for conditional execution
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Validate execution context
|
|
62
|
+
#
|
|
63
|
+
# @param context [Object] Execution context
|
|
64
|
+
# @raise [ValidationError] if context is invalid
|
|
65
|
+
def validate_context(context)
|
|
66
|
+
raise ValidationError.new('Context cannot be nil', command_name: name) if context.nil?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Validate command parameters
|
|
70
|
+
#
|
|
71
|
+
# @param params [Hash] Command parameters
|
|
72
|
+
# @raise [ValidationError] if parameters are invalid
|
|
73
|
+
def validate_parameters(params)
|
|
74
|
+
# Override in subclasses for parameter validation
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
protected
|
|
78
|
+
|
|
79
|
+
# Perform the actual command logic
|
|
80
|
+
# Must be implemented by subclasses
|
|
81
|
+
#
|
|
82
|
+
# @param context [Object] Execution context
|
|
83
|
+
# @param params [Hash] Command parameters
|
|
84
|
+
# @return [Object] Command result
|
|
85
|
+
def perform(context, params = {})
|
|
86
|
+
raise NotImplementedError, "#{self.class.name} must implement #perform"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Handle successful command execution
|
|
90
|
+
#
|
|
91
|
+
# @param context [Object] Execution context
|
|
92
|
+
# @param result [Object] Command result
|
|
93
|
+
def handle_success(context, result)
|
|
94
|
+
log_success(context, result)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Handle command execution errors
|
|
98
|
+
#
|
|
99
|
+
# @param context [Object] Execution context
|
|
100
|
+
# @param error [StandardError] The error that occurred
|
|
101
|
+
# @param params [Hash] Command parameters
|
|
102
|
+
def handle_error(context, error, params = {})
|
|
103
|
+
log_error(context, error, params)
|
|
104
|
+
|
|
105
|
+
# Show user-friendly error message if possible
|
|
106
|
+
return unless context.respond_to?(:show_error_message)
|
|
107
|
+
|
|
108
|
+
context.show_error_message(user_friendly_error_message(error))
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def log_success(context, result)
|
|
114
|
+
logger = resolve_logger(context)
|
|
115
|
+
logger&.debug(
|
|
116
|
+
'Command executed successfully',
|
|
117
|
+
command: name,
|
|
118
|
+
context: context.class.name,
|
|
119
|
+
result: result.inspect
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def log_error(context, error, params)
|
|
124
|
+
logger = resolve_logger(context)
|
|
125
|
+
logger&.error(
|
|
126
|
+
'Command execution failed',
|
|
127
|
+
command: name,
|
|
128
|
+
context: context.class.name,
|
|
129
|
+
error: error.message,
|
|
130
|
+
params: params,
|
|
131
|
+
backtrace: error.backtrace.first(5)
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def resolve_logger(context)
|
|
136
|
+
return @logger if defined?(@logger) && @logger
|
|
137
|
+
|
|
138
|
+
candidate = begin
|
|
139
|
+
if context.respond_to?(:dependencies)
|
|
140
|
+
deps = context.dependencies
|
|
141
|
+
deps.resolve(:logger) if deps.respond_to?(:resolve)
|
|
142
|
+
elsif context.respond_to?(:logger)
|
|
143
|
+
context.logger
|
|
144
|
+
end
|
|
145
|
+
rescue StandardError
|
|
146
|
+
nil
|
|
147
|
+
end
|
|
148
|
+
@logger = candidate if candidate
|
|
149
|
+
candidate
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def user_friendly_error_message(error)
|
|
153
|
+
msg = error.message
|
|
154
|
+
case error
|
|
155
|
+
when ValidationError
|
|
156
|
+
"Invalid input: #{msg}"
|
|
157
|
+
when CommandError
|
|
158
|
+
msg
|
|
159
|
+
else
|
|
160
|
+
'An unexpected error occurred'
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_command'
|
|
4
|
+
|
|
5
|
+
module Shoko
|
|
6
|
+
module Application
|
|
7
|
+
module Commands
|
|
8
|
+
# Bookmark management commands using domain services.
|
|
9
|
+
class BookmarkCommand < BaseCommand
|
|
10
|
+
def initialize(action, name: nil, description: nil)
|
|
11
|
+
@action = action
|
|
12
|
+
super(
|
|
13
|
+
name: name || "bookmark_#{action}",
|
|
14
|
+
description: description || "Bookmark #{action.to_s.tr('_', ' ')}"
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def can_execute?(context, _params = {})
|
|
19
|
+
deps = context.dependencies
|
|
20
|
+
deps.registered?(:bookmark_service) && deps.registered?(:state_store)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
protected
|
|
24
|
+
|
|
25
|
+
def perform(context, params = {})
|
|
26
|
+
deps = context.dependencies
|
|
27
|
+
bookmark_service = deps.resolve(:bookmark_service)
|
|
28
|
+
|
|
29
|
+
case @action
|
|
30
|
+
when :add
|
|
31
|
+
handle_add_bookmark(bookmark_service, params)
|
|
32
|
+
when :remove
|
|
33
|
+
handle_remove_bookmark(bookmark_service, params)
|
|
34
|
+
when :toggle
|
|
35
|
+
handle_toggle_bookmark(bookmark_service, params)
|
|
36
|
+
when :jump_to
|
|
37
|
+
handle_jump_to_bookmark(bookmark_service, params)
|
|
38
|
+
else
|
|
39
|
+
raise ExecutionError.new("Unknown bookmark action: #{@action}", command_name: name)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
@action
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def handle_add_bookmark(service, params)
|
|
48
|
+
text_snippet = params[:text_snippet]
|
|
49
|
+
service.add_bookmark(text_snippet)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def handle_remove_bookmark(service, params)
|
|
53
|
+
bookmark = params[:bookmark]
|
|
54
|
+
|
|
55
|
+
raise ValidationError.new('Bookmark required for remove action', command_name: name) unless bookmark
|
|
56
|
+
|
|
57
|
+
service.remove_bookmark(bookmark)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def handle_toggle_bookmark(service, params)
|
|
61
|
+
text_snippet = params[:text_snippet]
|
|
62
|
+
result = service.toggle_bookmark(text_snippet)
|
|
63
|
+
|
|
64
|
+
{ result: result }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def handle_jump_to_bookmark(service, params)
|
|
68
|
+
bookmark = params[:bookmark]
|
|
69
|
+
|
|
70
|
+
raise ValidationError.new('Bookmark required for jump_to action', command_name: name) unless bookmark
|
|
71
|
+
|
|
72
|
+
service.jump_to_bookmark(bookmark)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Factory methods for bookmark commands
|
|
77
|
+
module BookmarkCommandFactory
|
|
78
|
+
def self.add_bookmark(text_snippet = nil)
|
|
79
|
+
command = BookmarkCommand.new(:add)
|
|
80
|
+
|
|
81
|
+
# If text_snippet provided, create a wrapper that includes it in params
|
|
82
|
+
if text_snippet
|
|
83
|
+
lambda do |context, params = {}|
|
|
84
|
+
command.execute(context, params.merge(text_snippet: text_snippet))
|
|
85
|
+
end
|
|
86
|
+
else
|
|
87
|
+
command
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def self.remove_bookmark(bookmark)
|
|
92
|
+
command = BookmarkCommand.new(:remove)
|
|
93
|
+
lambda do |context, params = {}|
|
|
94
|
+
command.execute(context, params.merge(bookmark: bookmark))
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def self.toggle_bookmark(text_snippet = nil)
|
|
99
|
+
command = BookmarkCommand.new(:toggle)
|
|
100
|
+
lambda do |context, params = {}|
|
|
101
|
+
command.execute(context, params.merge(text_snippet: text_snippet))
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def self.jump_to_bookmark(bookmark)
|
|
106
|
+
command = BookmarkCommand.new(:jump_to)
|
|
107
|
+
lambda do |context, params = {}|
|
|
108
|
+
command.execute(context, params.merge(bookmark: bookmark))
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|