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,413 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
begin
|
|
5
|
+
require 'json'
|
|
6
|
+
rescue NameError => e
|
|
7
|
+
raise unless e.name == :Fragment
|
|
8
|
+
|
|
9
|
+
# Ensure JSON::Fragment exists for compatibility with JSON parsing in some environments.
|
|
10
|
+
module JSON
|
|
11
|
+
Fragment = Object unless const_defined?(:Fragment)
|
|
12
|
+
end
|
|
13
|
+
require 'json'
|
|
14
|
+
end
|
|
15
|
+
require_relative '../../adapters/storage/atomic_file_writer.rb'
|
|
16
|
+
require_relative '../../adapters/storage/config_paths.rb'
|
|
17
|
+
require_relative '../../adapters/output/kitty/kitty_graphics'
|
|
18
|
+
|
|
19
|
+
module Shoko
|
|
20
|
+
module Application::Infrastructure
|
|
21
|
+
# Immutable state store with event-driven updates.
|
|
22
|
+
# Single source of truth for application state with validation.
|
|
23
|
+
class StateStore
|
|
24
|
+
attr_reader :event_bus
|
|
25
|
+
|
|
26
|
+
SYMBOL_KEYS = %i[view_mode line_spacing page_numbering_mode theme].freeze
|
|
27
|
+
LINE_SPACING_ALIASES = {
|
|
28
|
+
tight: :compact,
|
|
29
|
+
wide: :relaxed,
|
|
30
|
+
}.freeze
|
|
31
|
+
private_constant :SYMBOL_KEYS, :LINE_SPACING_ALIASES
|
|
32
|
+
|
|
33
|
+
# Configuration file management (used by persistence + specs).
|
|
34
|
+
def self.config_dir
|
|
35
|
+
Shoko::Adapters::Storage::ConfigPaths.config_root
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.config_file
|
|
39
|
+
File.join(config_dir, 'config.json')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def initialize(event_bus = EventBus.new)
|
|
43
|
+
@event_bus = event_bus
|
|
44
|
+
@state = build_initial_state
|
|
45
|
+
@mutex = Mutex.new
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Cheap, read-only reference to the current state (no deep copy).
|
|
49
|
+
# Callers must not mutate the returned object.
|
|
50
|
+
def peek
|
|
51
|
+
@mutex.synchronize { @state }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Get current state snapshot (immutable)
|
|
55
|
+
#
|
|
56
|
+
# @return [Hash] Current state
|
|
57
|
+
def current_state
|
|
58
|
+
@mutex.synchronize { deep_dup(@state, true) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Get value at specific path
|
|
62
|
+
#
|
|
63
|
+
# @param path [Array<Symbol>] Path to value
|
|
64
|
+
# @return [Object] Value at path
|
|
65
|
+
def get(path)
|
|
66
|
+
@mutex.synchronize do
|
|
67
|
+
path.reduce(@state) { |state, key| state&.dig(key) }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Update state and emit events
|
|
72
|
+
#
|
|
73
|
+
# @param updates [Hash] Hash of path => value updates
|
|
74
|
+
def update(updates)
|
|
75
|
+
@mutex.synchronize do
|
|
76
|
+
old_state = @state
|
|
77
|
+
new_state = apply_updates(old_state, updates)
|
|
78
|
+
|
|
79
|
+
return if old_state == new_state
|
|
80
|
+
|
|
81
|
+
@state = new_state
|
|
82
|
+
emit_change_events(old_state, new_state, updates)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Update single path
|
|
87
|
+
#
|
|
88
|
+
# @param path [Array<Symbol>] Path to update
|
|
89
|
+
# @param value [Object] New value
|
|
90
|
+
def set(path, value)
|
|
91
|
+
update({ path => value })
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Reset to initial state
|
|
95
|
+
def reset!
|
|
96
|
+
@mutex.synchronize do
|
|
97
|
+
old_state = @state
|
|
98
|
+
@state = build_initial_state
|
|
99
|
+
@event_bus.emit_event(:state_reset, { old_state: old_state, new_state: @state })
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Validate state transition (override in subclasses)
|
|
104
|
+
#
|
|
105
|
+
# @param old_state [Hash] Previous state
|
|
106
|
+
# @param new_state [Hash] Proposed new state
|
|
107
|
+
# @param updates [Hash] Applied updates
|
|
108
|
+
# @return [Boolean] Whether transition is valid
|
|
109
|
+
def valid_transition?(_old_state, _new_state, _updates)
|
|
110
|
+
true # Base implementation allows all transitions
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Convenience methods for compatibility with legacy callers
|
|
114
|
+
def terminal_size_changed?(width, height)
|
|
115
|
+
last_width = get(%i[reader last_width])
|
|
116
|
+
last_height = get(%i[reader last_height])
|
|
117
|
+
width != last_width || height != last_height
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def update_terminal_size(width, height)
|
|
121
|
+
update({
|
|
122
|
+
%i[reader last_width] => width,
|
|
123
|
+
%i[reader last_height] => height,
|
|
124
|
+
%i[ui terminal_width] => width,
|
|
125
|
+
%i[ui terminal_height] => height,
|
|
126
|
+
})
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def apply_terminal_dimensions(width, height)
|
|
130
|
+
return unless width && height
|
|
131
|
+
|
|
132
|
+
update_terminal_size(width, height)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# State snapshot for persistence
|
|
136
|
+
def reader_snapshot
|
|
137
|
+
{
|
|
138
|
+
current_chapter: get(%i[reader current_chapter]),
|
|
139
|
+
page_offset: get(%i[reader single_page]),
|
|
140
|
+
mode: get(%i[reader mode]).to_s,
|
|
141
|
+
timestamp: Time.now.iso8601,
|
|
142
|
+
}
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Restore reader state from snapshot
|
|
146
|
+
def restore_reader_from(snapshot)
|
|
147
|
+
update({
|
|
148
|
+
%i[reader current_chapter] => snapshot['current_chapter'] || 0,
|
|
149
|
+
%i[reader single_page] => snapshot['page_offset'] || 0,
|
|
150
|
+
%i[reader left_page] => snapshot['page_offset'] || 0,
|
|
151
|
+
%i[reader mode] => (snapshot['mode'] || 'read').to_sym,
|
|
152
|
+
})
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Configuration persistence methods
|
|
156
|
+
def save_config
|
|
157
|
+
ensure_config_dir
|
|
158
|
+
write_config_file
|
|
159
|
+
rescue StandardError
|
|
160
|
+
# Ignore save errors
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def config_to_h
|
|
164
|
+
get([:config])
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Dispatch Application::Actions to update state explicitly
|
|
168
|
+
def dispatch(action)
|
|
169
|
+
return unless action.respond_to?(:apply)
|
|
170
|
+
|
|
171
|
+
action.apply(self)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
private
|
|
175
|
+
|
|
176
|
+
def build_initial_state
|
|
177
|
+
{
|
|
178
|
+
reader: {
|
|
179
|
+
# Position state
|
|
180
|
+
current_chapter: 0,
|
|
181
|
+
left_page: 0,
|
|
182
|
+
right_page: 0,
|
|
183
|
+
single_page: 0,
|
|
184
|
+
current_page_index: 0,
|
|
185
|
+
|
|
186
|
+
# Mode and UI state
|
|
187
|
+
mode: :read,
|
|
188
|
+
selection: nil,
|
|
189
|
+
message: nil,
|
|
190
|
+
running: true,
|
|
191
|
+
|
|
192
|
+
# Lists and selections
|
|
193
|
+
bookmarks: [],
|
|
194
|
+
annotations: [],
|
|
195
|
+
|
|
196
|
+
# Pagination state
|
|
197
|
+
page_map: [],
|
|
198
|
+
total_pages: 0,
|
|
199
|
+
pages_per_chapter: [],
|
|
200
|
+
|
|
201
|
+
# Terminal sizing
|
|
202
|
+
last_width: 0,
|
|
203
|
+
last_height: 0,
|
|
204
|
+
page_offset: 0,
|
|
205
|
+
|
|
206
|
+
# Dynamic pagination
|
|
207
|
+
dynamic_page_map: nil,
|
|
208
|
+
dynamic_total_pages: 0,
|
|
209
|
+
dynamic_chapter_starts: [],
|
|
210
|
+
last_dynamic_width: 0,
|
|
211
|
+
last_dynamic_height: 0,
|
|
212
|
+
|
|
213
|
+
# UI state
|
|
214
|
+
rendered_lines: {},
|
|
215
|
+
popup_menu: nil,
|
|
216
|
+
annotations_overlay: nil,
|
|
217
|
+
annotation_editor_overlay: nil,
|
|
218
|
+
|
|
219
|
+
# Sidebar state
|
|
220
|
+
sidebar_visible: false,
|
|
221
|
+
sidebar_active_tab: :toc,
|
|
222
|
+
sidebar_toc_selected: 0,
|
|
223
|
+
sidebar_annotations_selected: 0,
|
|
224
|
+
sidebar_bookmarks_selected: 0,
|
|
225
|
+
sidebar_toc_filter: nil,
|
|
226
|
+
sidebar_toc_filter_active: false,
|
|
227
|
+
sidebar_toc_collapsed: nil,
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
menu: {
|
|
231
|
+
selected: 0,
|
|
232
|
+
mode: :menu,
|
|
233
|
+
browse_selected: 0,
|
|
234
|
+
settings_selected: 1,
|
|
235
|
+
search_query: '',
|
|
236
|
+
search_cursor: 0,
|
|
237
|
+
search_active: false,
|
|
238
|
+
download_query: '',
|
|
239
|
+
download_cursor: 0,
|
|
240
|
+
download_selected: 0,
|
|
241
|
+
download_results: [],
|
|
242
|
+
download_count: 0,
|
|
243
|
+
download_next: nil,
|
|
244
|
+
download_prev: nil,
|
|
245
|
+
download_status: :idle,
|
|
246
|
+
download_message: '',
|
|
247
|
+
download_progress: 0.0,
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
config: {
|
|
251
|
+
view_mode: :split,
|
|
252
|
+
line_spacing: :compact,
|
|
253
|
+
page_numbering_mode: :dynamic,
|
|
254
|
+
theme: :dark,
|
|
255
|
+
show_page_numbers: true,
|
|
256
|
+
highlight_quotes: true,
|
|
257
|
+
highlight_keywords: false,
|
|
258
|
+
prefetch_pages: 20,
|
|
259
|
+
kitty_images: Shoko::Adapters::Output::Kitty::KittyGraphics.supported?,
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
ui: {
|
|
263
|
+
terminal_width: 80,
|
|
264
|
+
terminal_height: 24,
|
|
265
|
+
},
|
|
266
|
+
}
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def apply_updates(state, updates)
|
|
270
|
+
# Copy only the branches we need to touch instead of duplicating the entire tree.
|
|
271
|
+
clones = {}.compare_by_identity
|
|
272
|
+
new_root = duplicate_node(state, clones)
|
|
273
|
+
|
|
274
|
+
updates.each do |path, value|
|
|
275
|
+
validate_update(path, value)
|
|
276
|
+
keys = Array(path)
|
|
277
|
+
target = new_root
|
|
278
|
+
keys[0...-1].each do |key|
|
|
279
|
+
existing = target[key]
|
|
280
|
+
duplicated = duplicate_node(existing, clones)
|
|
281
|
+
duplicated = {} if duplicated.nil?
|
|
282
|
+
target[key] = duplicated
|
|
283
|
+
target = duplicated
|
|
284
|
+
end
|
|
285
|
+
target[keys.last] = value
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
new_root
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def validate_update(path, value)
|
|
292
|
+
# Add validation logic here
|
|
293
|
+
path_array = Array(path)
|
|
294
|
+
|
|
295
|
+
case path_array
|
|
296
|
+
when %i[reader current_chapter]
|
|
297
|
+
raise ArgumentError, 'current_chapter must be non-negative' if value.negative?
|
|
298
|
+
when %i[config view_mode]
|
|
299
|
+
raise ArgumentError, 'invalid view_mode' unless %i[single split].include?(value)
|
|
300
|
+
when %i[config kitty_images]
|
|
301
|
+
raise ArgumentError, 'kitty_images must be boolean' unless [true, false].include?(value)
|
|
302
|
+
when %i[ui terminal_width], %i[ui terminal_height]
|
|
303
|
+
raise ArgumentError, 'terminal dimensions must be positive' if value <= 0
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def set_nested(hash, path, value)
|
|
308
|
+
*keys, last_key = path
|
|
309
|
+
|
|
310
|
+
if keys.empty?
|
|
311
|
+
hash[last_key] = value
|
|
312
|
+
else
|
|
313
|
+
# Create mutable path to target
|
|
314
|
+
target = hash
|
|
315
|
+
keys.each do |key|
|
|
316
|
+
target[key] = {} unless target.key?(key)
|
|
317
|
+
target = target[key]
|
|
318
|
+
end
|
|
319
|
+
target[last_key] = value
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def duplicate_node(node, clones)
|
|
324
|
+
return node unless node.is_a?(Hash)
|
|
325
|
+
return clones[node] if clones.key?(node)
|
|
326
|
+
|
|
327
|
+
duped = node.dup
|
|
328
|
+
clones[node] = duped
|
|
329
|
+
duped
|
|
330
|
+
rescue StandardError
|
|
331
|
+
node
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def deep_dup(obj, freeze_result = false)
|
|
335
|
+
case obj
|
|
336
|
+
when Hash
|
|
337
|
+
result = obj.transform_values { |v| deep_dup(v, freeze_result) }
|
|
338
|
+
freeze_result ? result.freeze : result
|
|
339
|
+
when Array
|
|
340
|
+
result = obj.map { |v| deep_dup(v, freeze_result) }
|
|
341
|
+
freeze_result ? result.freeze : result
|
|
342
|
+
else
|
|
343
|
+
begin
|
|
344
|
+
obj.dup
|
|
345
|
+
rescue StandardError
|
|
346
|
+
obj
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def emit_change_events(old_state, new_state, updates)
|
|
352
|
+
updates.each do |path, new_value|
|
|
353
|
+
arr_path = Array(path)
|
|
354
|
+
old_value = get_nested_value(old_state, arr_path)
|
|
355
|
+
next if old_value == new_value
|
|
356
|
+
|
|
357
|
+
@event_bus.emit_event(:state_changed, {
|
|
358
|
+
path: arr_path,
|
|
359
|
+
old_value: old_value,
|
|
360
|
+
new_value: new_value,
|
|
361
|
+
full_state: new_state,
|
|
362
|
+
})
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def get_nested_value(hash, path)
|
|
367
|
+
path.reduce(hash) { |h, key| h&.dig(key) }
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def ensure_config_dir
|
|
371
|
+
FileUtils.mkdir_p(self.class.config_dir)
|
|
372
|
+
rescue StandardError
|
|
373
|
+
nil
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def write_config_file
|
|
377
|
+
payload = JSON.pretty_generate(config_to_h)
|
|
378
|
+
Shoko::Adapters::Storage::AtomicFileWriter.write(self.class.config_file, payload)
|
|
379
|
+
rescue StandardError
|
|
380
|
+
nil
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# Load config from file on initialization
|
|
384
|
+
def load_config_from_file
|
|
385
|
+
config_file = self.class.config_file
|
|
386
|
+
return unless File.exist?(config_file)
|
|
387
|
+
|
|
388
|
+
data = parse_config_file(config_file)
|
|
389
|
+
apply_config_data(data) if data
|
|
390
|
+
rescue StandardError
|
|
391
|
+
# Use defaults on error
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def parse_config_file(path)
|
|
395
|
+
JSON.parse(File.read(path), symbolize_names: true)
|
|
396
|
+
rescue StandardError
|
|
397
|
+
nil
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def apply_config_data(data)
|
|
401
|
+
config_updates = {}
|
|
402
|
+
data.each do |key, value|
|
|
403
|
+
next unless get([:config]).key?(key)
|
|
404
|
+
|
|
405
|
+
value = value.to_sym if SYMBOL_KEYS.include?(key)
|
|
406
|
+
value = LINE_SPACING_ALIASES.fetch(value, value) if key == :line_spacing
|
|
407
|
+
config_updates[[:config, key]] = value
|
|
408
|
+
end
|
|
409
|
+
update(config_updates) unless config_updates.empty?
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shoko
|
|
4
|
+
module Application
|
|
5
|
+
module MainMenu
|
|
6
|
+
# Encapsulates loading-state updates while a book is preprocessed.
|
|
7
|
+
class MenuProgressPresenter
|
|
8
|
+
MIN_PROGRESS_DELTA = 0.01
|
|
9
|
+
|
|
10
|
+
def initialize(state)
|
|
11
|
+
@state = state
|
|
12
|
+
@last_message = nil
|
|
13
|
+
@last_progress = nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def show(path:, index:, mode:)
|
|
17
|
+
@last_message = 'Preparing book...'
|
|
18
|
+
@last_progress = 0.0
|
|
19
|
+
dispatch(
|
|
20
|
+
loading_active: true,
|
|
21
|
+
loading_path: path,
|
|
22
|
+
loading_progress: 0.0,
|
|
23
|
+
loading_index: index,
|
|
24
|
+
loading_mode: mode,
|
|
25
|
+
loading_message: @last_message
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def update(done:, total:)
|
|
30
|
+
progress = Shoko::Core::Services::ProgressHelper.ratio(done, total)
|
|
31
|
+
update_status(progress: progress)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def update_message(message)
|
|
35
|
+
update_status(message: message)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def set_progress(progress)
|
|
39
|
+
update_status(progress: progress)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def update_status(message: nil, progress: nil)
|
|
43
|
+
updates = {}
|
|
44
|
+
|
|
45
|
+
if message && message != @last_message
|
|
46
|
+
updates[:loading_message] = message
|
|
47
|
+
@last_message = message
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
unless progress.nil?
|
|
51
|
+
normalized = progress.to_f.clamp(0.0, 1.0)
|
|
52
|
+
if @last_progress.nil? || (normalized - @last_progress).abs >= MIN_PROGRESS_DELTA
|
|
53
|
+
updates[:loading_progress] = normalized
|
|
54
|
+
@last_progress = normalized
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
dispatch(updates) unless updates.empty?
|
|
59
|
+
!updates.empty?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def clear
|
|
63
|
+
@last_message = nil
|
|
64
|
+
@last_progress = nil
|
|
65
|
+
dispatch(
|
|
66
|
+
loading_active: false,
|
|
67
|
+
loading_path: nil,
|
|
68
|
+
loading_progress: nil,
|
|
69
|
+
loading_index: nil,
|
|
70
|
+
loading_mode: nil,
|
|
71
|
+
loading_message: nil
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def dispatch(payload)
|
|
78
|
+
@state.dispatch(Shoko::Application::Actions::UpdateMenuAction.new(payload))
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../application/selectors/reader_selectors'
|
|
4
|
+
|
|
5
|
+
module Shoko
|
|
6
|
+
module Application
|
|
7
|
+
# Applies a pending jump payload captured in state before reader starts.
|
|
8
|
+
class PendingJumpHandler
|
|
9
|
+
def initialize(state, dependencies, ui_controller)
|
|
10
|
+
@state = state
|
|
11
|
+
@dependencies = dependencies
|
|
12
|
+
@ui_controller = ui_controller
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def apply
|
|
16
|
+
payload = state.get(%i[reader pending_jump])
|
|
17
|
+
return unless payload
|
|
18
|
+
|
|
19
|
+
apply_chapter_jump(payload)
|
|
20
|
+
apply_selection(payload)
|
|
21
|
+
open_annotation_editor(payload)
|
|
22
|
+
ensure
|
|
23
|
+
clear_pending_jump
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
attr_reader :state, :dependencies, :ui_controller
|
|
29
|
+
|
|
30
|
+
def apply_chapter_jump(payload)
|
|
31
|
+
chapter_index = payload[:chapter_index] || payload['chapter_index']
|
|
32
|
+
return unless chapter_index
|
|
33
|
+
|
|
34
|
+
navigation = resolve_optional(:navigation_service)
|
|
35
|
+
navigation&.jump_to_chapter(chapter_index)
|
|
36
|
+
rescue StandardError
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def apply_selection(payload)
|
|
41
|
+
range = payload[:selection_range] || payload['selection_range']
|
|
42
|
+
return unless range
|
|
43
|
+
|
|
44
|
+
normalized = normalize_selection(range)
|
|
45
|
+
return unless normalized
|
|
46
|
+
|
|
47
|
+
state.dispatch(Shoko::Application::Actions::UpdateSelectionAction.new(normalized))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def open_annotation_editor(payload)
|
|
51
|
+
return unless edit_requested?(payload)
|
|
52
|
+
|
|
53
|
+
annotation = normalized_annotation(payload)
|
|
54
|
+
return unless annotation
|
|
55
|
+
|
|
56
|
+
ui_controller.open_annotation_editor_overlay(
|
|
57
|
+
text: annotation[:text],
|
|
58
|
+
range: annotation[:range],
|
|
59
|
+
chapter_index: annotation[:chapter_index],
|
|
60
|
+
annotation: annotation
|
|
61
|
+
)
|
|
62
|
+
rescue StandardError
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def normalize_selection(range)
|
|
67
|
+
service = resolve_optional(:selection_service)
|
|
68
|
+
if service.respond_to?(:normalize_range)
|
|
69
|
+
normalized = service.normalize_range(state, range)
|
|
70
|
+
return normalized if normalized
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
coord = resolve_optional(:coordinate_service)
|
|
74
|
+
return range unless coord
|
|
75
|
+
|
|
76
|
+
rendered = Shoko::Application::Selectors::ReaderSelectors.rendered_lines(state)
|
|
77
|
+
coord.normalize_selection_range(range, rendered)
|
|
78
|
+
rescue StandardError
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def clear_pending_jump
|
|
83
|
+
state.dispatch(Shoko::Application::Actions::UpdateSelectionsAction.new(pending_jump: nil))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def resolve_optional(key)
|
|
87
|
+
return nil unless dependencies.respond_to?(:resolve)
|
|
88
|
+
|
|
89
|
+
dependencies.resolve(key)
|
|
90
|
+
rescue StandardError
|
|
91
|
+
nil
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def truthy?(value)
|
|
95
|
+
return value unless value.is_a?(String)
|
|
96
|
+
|
|
97
|
+
!%w[false 0 no].include?(value.downcase)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def edit_requested?(payload)
|
|
101
|
+
truthy?(payload[:edit] || payload['edit'])
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def normalized_annotation(payload)
|
|
105
|
+
raw = payload[:annotation] || payload['annotation']
|
|
106
|
+
return unless raw
|
|
107
|
+
|
|
108
|
+
{
|
|
109
|
+
id: value_from(raw, :id),
|
|
110
|
+
text: value_from(raw, :text),
|
|
111
|
+
note: value_from(raw, :note),
|
|
112
|
+
chapter_index: value_from(raw, :chapter_index),
|
|
113
|
+
range: value_from(raw, :range),
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def value_from(hash, key)
|
|
118
|
+
hash[key] || hash[key.to_s]
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shoko
|
|
4
|
+
module Application
|
|
5
|
+
# Manages reader startup and shutdown concerns (terminal setup, background worker).
|
|
6
|
+
class ReaderLifecycle
|
|
7
|
+
def initialize(controller, dependencies:, terminal_service:)
|
|
8
|
+
@controller = controller
|
|
9
|
+
@dependencies = dependencies
|
|
10
|
+
@terminal_service = terminal_service
|
|
11
|
+
@background_worker = nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def ensure_background_worker(name: 'reader-background')
|
|
15
|
+
@background_worker ||= resolve_existing(:background_worker)
|
|
16
|
+
return @background_worker if @background_worker
|
|
17
|
+
|
|
18
|
+
factory = resolve_optional(:background_worker_factory)
|
|
19
|
+
return nil unless factory.respond_to?(:call)
|
|
20
|
+
|
|
21
|
+
@background_worker = factory.call(name: name)
|
|
22
|
+
@dependencies.register(:background_worker, @background_worker) if @background_worker
|
|
23
|
+
@background_worker
|
|
24
|
+
rescue StandardError
|
|
25
|
+
nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
attr_reader :background_worker
|
|
29
|
+
|
|
30
|
+
def run
|
|
31
|
+
ensure_background_worker
|
|
32
|
+
@terminal_service.setup
|
|
33
|
+
@controller.mark_metrics_start!
|
|
34
|
+
Shoko::Application::ReaderStartupOrchestrator.new(@dependencies).start(@controller)
|
|
35
|
+
@controller.main_loop
|
|
36
|
+
ensure
|
|
37
|
+
shutdown_background_worker
|
|
38
|
+
@terminal_service.cleanup
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def shutdown_background_worker
|
|
42
|
+
@background_worker&.shutdown
|
|
43
|
+
ensure
|
|
44
|
+
@background_worker = nil
|
|
45
|
+
@dependencies.register(:background_worker, nil)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def resolve_existing(name)
|
|
51
|
+
return nil unless @dependencies.respond_to?(:registered?) && @dependencies.registered?(name)
|
|
52
|
+
|
|
53
|
+
@dependencies.resolve(name)
|
|
54
|
+
rescue StandardError
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def resolve_optional(name)
|
|
59
|
+
@dependencies.resolve(name)
|
|
60
|
+
rescue StandardError
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|