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,782 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../adapters/output/ui/components/annotations_overlay_component.rb'
|
|
4
|
+
require_relative '../../adapters/output/ui/components/annotation_editor_overlay_component.rb'
|
|
5
|
+
|
|
6
|
+
module Shoko
|
|
7
|
+
module Application::Controllers
|
|
8
|
+
# Handles all UI-related functionality: modes, overlays, popups, sidebar
|
|
9
|
+
class UIController
|
|
10
|
+
# Raised when required dependencies are missing for a UI action.
|
|
11
|
+
class MissingDependencyError < StandardError; end
|
|
12
|
+
|
|
13
|
+
# Builds the annotation editor screen component for annotation editor mode.
|
|
14
|
+
class AnnotationEditorMode
|
|
15
|
+
def initialize(controller, dependencies)
|
|
16
|
+
@controller = controller
|
|
17
|
+
@dependencies = dependencies
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def build_component(**)
|
|
21
|
+
Shoko::Adapters::Output::Ui::Components::Screens::AnnotationEditorScreenComponent.new(
|
|
22
|
+
@controller,
|
|
23
|
+
**,
|
|
24
|
+
dependencies: @dependencies
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize(state, dependencies)
|
|
30
|
+
@state = state
|
|
31
|
+
@dependencies = dependencies
|
|
32
|
+
@current_mode = nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def switch_mode(mode, **)
|
|
36
|
+
annotation_editor_mode =
|
|
37
|
+
mode == :annotation_editor ? AnnotationEditorMode.new(self, @dependencies) : nil
|
|
38
|
+
close_annotations_overlay unless annotation_editor_mode
|
|
39
|
+
close_annotation_editor_overlay unless annotation_editor_mode
|
|
40
|
+
@state.dispatch(Shoko::Application::Actions::UpdateReaderModeAction.new(mode))
|
|
41
|
+
|
|
42
|
+
# Rendered via screen/sidebar components; no standalone mode component
|
|
43
|
+
@current_mode = annotation_editor_mode&.build_component(**)
|
|
44
|
+
|
|
45
|
+
# Keep input dispatcher in sync with mode to prevent cross-mode key leaks
|
|
46
|
+
begin
|
|
47
|
+
input_controller = @dependencies.resolve(:input_controller)
|
|
48
|
+
input_controller.activate_for_mode(mode) if input_controller.respond_to?(:activate_for_mode)
|
|
49
|
+
rescue StandardError
|
|
50
|
+
# If not available, ignore; read mode remains default
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def open_toc
|
|
55
|
+
toggle_sidebar(:toc)
|
|
56
|
+
rescue StandardError => e
|
|
57
|
+
set_message("TOC error: #{e.message}", 3)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def open_bookmarks
|
|
61
|
+
toggle_sidebar(:bookmarks)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def open_annotations_tab
|
|
65
|
+
toggle_sidebar(:annotations)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def open_annotations
|
|
69
|
+
overlay = Application::Selectors::ReaderSelectors.annotations_overlay(@state)
|
|
70
|
+
if overlay&.visible?
|
|
71
|
+
close_annotations_overlay
|
|
72
|
+
else
|
|
73
|
+
show_annotations_overlay
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def open_annotation_editor_overlay(text:, range:, chapter_index:, annotation: nil)
|
|
78
|
+
show_annotation_editor_overlay(text: text,
|
|
79
|
+
range: range,
|
|
80
|
+
chapter_index: chapter_index,
|
|
81
|
+
annotation: annotation)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
# Unified sidebar toggling for :toc, :annotations, :bookmarks
|
|
87
|
+
def toggle_sidebar(tab)
|
|
88
|
+
close_annotations_overlay
|
|
89
|
+
if sidebar_visible?
|
|
90
|
+
return close_sidebar_with_restore(tab) if sidebar_open_for?(tab)
|
|
91
|
+
|
|
92
|
+
switch_sidebar_tab(tab)
|
|
93
|
+
else
|
|
94
|
+
open_sidebar_for(tab)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def sidebar_open_for?(tab)
|
|
99
|
+
@state.get(%i[reader sidebar_visible]) &&
|
|
100
|
+
@state.get(%i[reader sidebar_active_tab]) == tab
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def close_sidebar_with_restore(tab)
|
|
104
|
+
prev_mode = @state.get(%i[reader sidebar_prev_view_mode])
|
|
105
|
+
if prev_mode
|
|
106
|
+
@state.dispatch(
|
|
107
|
+
Shoko::Application::Actions::UpdateConfigAction.new(view_mode: prev_mode)
|
|
108
|
+
)
|
|
109
|
+
@state.dispatch(
|
|
110
|
+
Shoko::Application::Actions::UpdateSelectionsAction.new(sidebar_prev_view_mode: nil)
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
@state.dispatch(Shoko::Application::Actions::UpdateSidebarAction.new(visible: false))
|
|
114
|
+
@state.dispatch(Shoko::Application::Actions::UpdateReaderModeAction.new(:read))
|
|
115
|
+
set_message("#{tab.to_s.capitalize} closed", 1) unless tab == :toc
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def open_sidebar_for(tab)
|
|
119
|
+
# Store current view and force single-page view
|
|
120
|
+
@state.dispatch(
|
|
121
|
+
Shoko::Application::Actions::UpdateSelectionsAction.new(
|
|
122
|
+
sidebar_prev_view_mode: @state.get(%i[config view_mode])
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
@state.dispatch(
|
|
126
|
+
Shoko::Application::Actions::UpdateConfigAction.new(view_mode: :single)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
updates = { active_tab: tab, visible: true }
|
|
130
|
+
case tab
|
|
131
|
+
when :toc
|
|
132
|
+
doc = safe_resolve(:document)
|
|
133
|
+
entries = toc_entries_for(doc)
|
|
134
|
+
collapsed = toc_collapsed_for(entries)
|
|
135
|
+
current_chapter = (@state.get(%i[reader current_chapter]) || 0).to_i
|
|
136
|
+
selected = toc_index_for_chapter(entries, current_chapter)
|
|
137
|
+
updates[:toc_collapsed] = collapsed
|
|
138
|
+
updates[:toc_selected] = ensure_visible_toc_selection(entries, collapsed, selected)
|
|
139
|
+
when :annotations
|
|
140
|
+
updates[:annotations_selected] =
|
|
141
|
+
@state.get(%i[reader sidebar_annotations_selected]) || 0
|
|
142
|
+
when :bookmarks
|
|
143
|
+
updates[:bookmarks_selected] =
|
|
144
|
+
@state.get(%i[reader sidebar_bookmarks_selected]) || 0
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
@state.dispatch(Shoko::Application::Actions::UpdateSidebarAction.new(updates))
|
|
148
|
+
@state.dispatch(Shoko::Application::Actions::UpdateReaderModeAction.new(:read))
|
|
149
|
+
set_message("#{tab.to_s.capitalize} opened", 1) unless tab == :toc
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def switch_sidebar_tab(tab)
|
|
153
|
+
return unless sidebar_visible?
|
|
154
|
+
|
|
155
|
+
current_tab = @state.get(%i[reader sidebar_active_tab])
|
|
156
|
+
return if current_tab == tab
|
|
157
|
+
|
|
158
|
+
updates = { active_tab: tab }
|
|
159
|
+
case tab
|
|
160
|
+
when :toc
|
|
161
|
+
doc = safe_resolve(:document)
|
|
162
|
+
entries = toc_entries_for(doc)
|
|
163
|
+
collapsed = toc_collapsed_for(entries)
|
|
164
|
+
selected = @state.get(%i[reader sidebar_toc_selected])
|
|
165
|
+
if selected.nil?
|
|
166
|
+
current_chapter = (@state.get(%i[reader current_chapter]) || 0).to_i
|
|
167
|
+
selected = toc_index_for_chapter(entries, current_chapter)
|
|
168
|
+
end
|
|
169
|
+
updates[:toc_collapsed] = collapsed
|
|
170
|
+
updates[:toc_selected] = ensure_visible_toc_selection(entries, collapsed, selected)
|
|
171
|
+
when :annotations
|
|
172
|
+
updates[:annotations_selected] = @state.get(%i[reader sidebar_annotations_selected]) || 0
|
|
173
|
+
when :bookmarks
|
|
174
|
+
updates[:bookmarks_selected] = @state.get(%i[reader sidebar_bookmarks_selected]) || 0
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
@state.dispatch(Shoko::Application::Actions::UpdateSidebarAction.new(updates))
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
public
|
|
181
|
+
|
|
182
|
+
def activate_sidebar_tab(tab)
|
|
183
|
+
if sidebar_visible?
|
|
184
|
+
switch_sidebar_tab(tab)
|
|
185
|
+
else
|
|
186
|
+
open_sidebar_for(tab)
|
|
187
|
+
end
|
|
188
|
+
rescue StandardError => e
|
|
189
|
+
set_message("Sidebar error: #{e.message}", 3)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def handle_sidebar_toc_click(index)
|
|
193
|
+
return unless sidebar_visible?
|
|
194
|
+
return unless index.is_a?(Integer)
|
|
195
|
+
|
|
196
|
+
doc = safe_resolve(:document)
|
|
197
|
+
entries = toc_entries_for(doc)
|
|
198
|
+
return if entries.empty?
|
|
199
|
+
return unless index.between?(0, entries.length - 1)
|
|
200
|
+
|
|
201
|
+
collapsed = toc_collapsed_for(entries)
|
|
202
|
+
updates = { toc_selected: index }
|
|
203
|
+
|
|
204
|
+
if toc_entry_has_children?(entries, index)
|
|
205
|
+
collapsed = toggle_toc_collapsed(collapsed, index)
|
|
206
|
+
updates[:toc_collapsed] = collapsed
|
|
207
|
+
updates[:toc_selected] = ensure_visible_toc_selection(entries, collapsed, index)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
@state.dispatch(Shoko::Application::Actions::UpdateSidebarAction.new(updates))
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def set_sidebar_toc_selected(index)
|
|
214
|
+
return unless sidebar_visible?
|
|
215
|
+
|
|
216
|
+
doc = safe_resolve(:document)
|
|
217
|
+
entries = toc_entries_for(doc)
|
|
218
|
+
return if entries.empty?
|
|
219
|
+
|
|
220
|
+
idx = index.to_i.clamp(0, entries.length - 1)
|
|
221
|
+
collapsed = toc_collapsed_for(entries)
|
|
222
|
+
idx = ensure_visible_toc_selection(entries, collapsed, idx)
|
|
223
|
+
|
|
224
|
+
updates = { toc_selected: idx }
|
|
225
|
+
updates[:toc_collapsed] = collapsed if collapsed != @state.get(%i[reader sidebar_toc_collapsed])
|
|
226
|
+
@state.dispatch(Shoko::Application::Actions::UpdateSidebarAction.new(updates))
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def show_help
|
|
230
|
+
switch_mode(:help)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def toggle_view_mode
|
|
234
|
+
@state.dispatch(Shoko::Application::Actions::ToggleViewModeAction.new)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def increase_line_spacing
|
|
238
|
+
modes = %i[compact normal relaxed]
|
|
239
|
+
current = modes.index(@state.get(%i[config line_spacing])) || 1
|
|
240
|
+
return unless current < 2
|
|
241
|
+
|
|
242
|
+
@state.dispatch(Shoko::Application::Actions::UpdateConfigAction.new(line_spacing: modes[current + 1]))
|
|
243
|
+
@state.dispatch(Shoko::Application::Actions::UpdatePageAction.new(last_width: 0))
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def decrease_line_spacing
|
|
247
|
+
modes = %i[compact normal relaxed]
|
|
248
|
+
current = modes.index(@state.get(%i[config line_spacing])) || 1
|
|
249
|
+
return unless current.positive?
|
|
250
|
+
|
|
251
|
+
@state.dispatch(Shoko::Application::Actions::UpdateConfigAction.new(line_spacing: modes[current - 1]))
|
|
252
|
+
@state.dispatch(Shoko::Application::Actions::UpdatePageAction.new(last_width: 0))
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def toggle_page_numbering_mode
|
|
256
|
+
current_mode = @state.get(%i[config page_numbering_mode])
|
|
257
|
+
new_mode = current_mode == :absolute ? :dynamic : :absolute
|
|
258
|
+
@state.dispatch(Shoko::Application::Actions::UpdateConfigAction.new(page_numbering_mode: new_mode))
|
|
259
|
+
set_message("Page numbering: #{new_mode}")
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Sidebar navigation helpers
|
|
263
|
+
def sidebar_down
|
|
264
|
+
update_sidebar_selection(+1)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def sidebar_up
|
|
268
|
+
update_sidebar_selection(-1)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def sidebar_select
|
|
272
|
+
return unless sidebar_visible?
|
|
273
|
+
|
|
274
|
+
case @state.get(%i[reader sidebar_active_tab])
|
|
275
|
+
when :toc
|
|
276
|
+
doc = safe_resolve(:document)
|
|
277
|
+
entries = toc_entries_for(doc)
|
|
278
|
+
selected_entry_index = (@state.get(%i[reader sidebar_toc_selected]) || 0).to_i
|
|
279
|
+
selected_entry_index = selected_entry_index.clamp(0, [entries.length - 1, 0].max)
|
|
280
|
+
chapter_index = entries[selected_entry_index]&.chapter_index
|
|
281
|
+
return unless chapter_index
|
|
282
|
+
|
|
283
|
+
nav_service = @dependencies.resolve(:navigation_service)
|
|
284
|
+
nav_service.jump_to_chapter(chapter_index)
|
|
285
|
+
|
|
286
|
+
# Close the sidebar and restore previous view mode if it was stored
|
|
287
|
+
prev_mode = @state.get(%i[reader sidebar_prev_view_mode])
|
|
288
|
+
if prev_mode
|
|
289
|
+
@state.dispatch(Shoko::Application::Actions::UpdateConfigAction.new(view_mode: prev_mode))
|
|
290
|
+
@state.dispatch(Shoko::Application::Actions::UpdateSelectionsAction.new(sidebar_prev_view_mode: nil))
|
|
291
|
+
end
|
|
292
|
+
@state.dispatch(Shoko::Application::Actions::UpdateSidebarAction.new(visible: false))
|
|
293
|
+
@state.dispatch(Shoko::Application::Actions::UpdateReaderModeAction.new(:read))
|
|
294
|
+
when :bookmarks
|
|
295
|
+
bookmarks = @state.get(%i[reader bookmarks]) || []
|
|
296
|
+
selected = (@state.get(%i[reader sidebar_bookmarks_selected]) || 0).to_i
|
|
297
|
+
selected = selected.clamp(0, [bookmarks.length - 1, 0].max)
|
|
298
|
+
bookmark = bookmarks[selected]
|
|
299
|
+
return unless bookmark
|
|
300
|
+
|
|
301
|
+
bookmark_service = safe_resolve(:bookmark_service)
|
|
302
|
+
if bookmark_service
|
|
303
|
+
bookmark_service.jump_to_bookmark(bookmark)
|
|
304
|
+
safe_resolve(:state_controller)&.save_progress
|
|
305
|
+
end
|
|
306
|
+
close_sidebar_with_restore(:bookmarks)
|
|
307
|
+
when :annotations
|
|
308
|
+
annotations = @state.get(%i[reader annotations]) || []
|
|
309
|
+
selected = (@state.get(%i[reader sidebar_annotations_selected]) || 0).to_i
|
|
310
|
+
selected = selected.clamp(0, [annotations.length - 1, 0].max)
|
|
311
|
+
annotation = annotations[selected]
|
|
312
|
+
return unless annotation
|
|
313
|
+
|
|
314
|
+
state_controller = safe_resolve(:state_controller)
|
|
315
|
+
state_controller&.jump_to_annotation(annotation)
|
|
316
|
+
close_sidebar_with_restore(:annotations)
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def handle_popup_action(action_data)
|
|
321
|
+
# Handle both old string-based actions and new action objects
|
|
322
|
+
action_type = action_data.is_a?(Hash) ? action_data[:action] : action_data
|
|
323
|
+
|
|
324
|
+
case action_type
|
|
325
|
+
when :create_annotation, 'Create Annotation'
|
|
326
|
+
handle_create_annotation_action(action_data)
|
|
327
|
+
when :copy_to_clipboard, 'Copy to Clipboard'
|
|
328
|
+
handle_copy_to_clipboard_action(action_data)
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
skip_editor = %i[create_annotation].include?(action_type) || action_type == 'Create Annotation'
|
|
332
|
+
cleanup_popup_state(skip_editor: skip_editor)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def cleanup_popup_state(skip_editor: false)
|
|
336
|
+
@state.dispatch(Shoko::Application::Actions::ClearPopupMenuAction.new)
|
|
337
|
+
@state.dispatch(Shoko::Application::Actions::ClearSelectionAction.new)
|
|
338
|
+
close_annotations_overlay
|
|
339
|
+
close_annotation_editor_overlay unless skip_editor
|
|
340
|
+
# Also reset any mouse-driven selection held outside state (MouseableReader)
|
|
341
|
+
begin
|
|
342
|
+
reader_controller = @dependencies.resolve(:reader_controller)
|
|
343
|
+
reader_controller&.send(:clear_selection!)
|
|
344
|
+
rescue StandardError
|
|
345
|
+
# Best-effort; ignore if not available
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Refresh annotations from persistence into state
|
|
350
|
+
def refresh_annotations
|
|
351
|
+
state_controller = @dependencies.resolve(:state_controller)
|
|
352
|
+
state_controller.refresh_annotations if state_controller.respond_to?(:refresh_annotations)
|
|
353
|
+
rescue StandardError
|
|
354
|
+
# Best-effort; ignore failures silently here
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# Provide current book path for modes/components that need persistence context
|
|
358
|
+
def current_book_path
|
|
359
|
+
@state.get(%i[reader book_path])
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def set_message(text, duration = 2)
|
|
363
|
+
notifier = @dependencies.resolve(:notification_service)
|
|
364
|
+
notifier.set_message(@state, text, duration)
|
|
365
|
+
rescue StandardError
|
|
366
|
+
# Fallback to direct dispatch if service not available
|
|
367
|
+
@state.dispatch(Shoko::Application::Actions::UpdateMessageAction.new(text))
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
attr_reader :current_mode
|
|
371
|
+
|
|
372
|
+
private
|
|
373
|
+
|
|
374
|
+
def sidebar_visible?
|
|
375
|
+
@state.get(%i[reader sidebar_visible])
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def show_annotations_overlay
|
|
379
|
+
overlay = Shoko::Adapters::Output::Ui::Components::AnnotationsOverlayComponent.new(@state)
|
|
380
|
+
@state.dispatch(Shoko::Application::Actions::UpdateAnnotationsOverlayAction.new(overlay))
|
|
381
|
+
set_message('Annotations overlay open (↑/↓ navigate, Enter open, e edit, d delete)', 3)
|
|
382
|
+
rescue StandardError
|
|
383
|
+
cleanup_annotations_overlay_fallback
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def close_annotations_overlay
|
|
387
|
+
overlay = Application::Selectors::ReaderSelectors.annotations_overlay(@state)
|
|
388
|
+
return unless overlay
|
|
389
|
+
|
|
390
|
+
overlay.hide if overlay.respond_to?(:hide)
|
|
391
|
+
@state.dispatch(Shoko::Application::Actions::ClearAnnotationsOverlayAction.new)
|
|
392
|
+
rescue StandardError
|
|
393
|
+
cleanup_annotations_overlay_fallback
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def show_annotation_editor_overlay(text:, range:, chapter_index:, annotation: nil)
|
|
397
|
+
message = 'Annotation editor unavailable'
|
|
398
|
+
overlay = Shoko::Adapters::Output::Ui::Components::AnnotationEditorOverlayComponent.new(
|
|
399
|
+
selected_text: text,
|
|
400
|
+
range: range,
|
|
401
|
+
chapter_index: chapter_index,
|
|
402
|
+
annotation: annotation
|
|
403
|
+
)
|
|
404
|
+
@state.dispatch(Shoko::Application::Actions::UpdateAnnotationEditorOverlayAction.new(overlay))
|
|
405
|
+
if activate_annotation_editor_overlay_session
|
|
406
|
+
message = 'Annotation editor active (Ctrl+S save, Esc cancel)'
|
|
407
|
+
else
|
|
408
|
+
cleanup_annotation_editor_overlay_fallback
|
|
409
|
+
end
|
|
410
|
+
rescue StandardError => e
|
|
411
|
+
cleanup_annotation_editor_overlay_fallback
|
|
412
|
+
log_dependency_error(:show_annotation_editor_overlay, e)
|
|
413
|
+
ensure
|
|
414
|
+
set_message(message, 3)
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def close_annotation_editor_overlay
|
|
418
|
+
overlay = Application::Selectors::ReaderSelectors.annotation_editor_overlay(@state)
|
|
419
|
+
return unless overlay
|
|
420
|
+
|
|
421
|
+
overlay.hide if overlay.respond_to?(:hide)
|
|
422
|
+
@state.dispatch(Shoko::Application::Actions::ClearAnnotationEditorOverlayAction.new)
|
|
423
|
+
deactivate_annotation_editor_overlay_session
|
|
424
|
+
rescue StandardError
|
|
425
|
+
cleanup_annotation_editor_overlay_fallback
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def update_sidebar_selection(delta)
|
|
429
|
+
return unless sidebar_visible?
|
|
430
|
+
|
|
431
|
+
tab = @state.get(%i[reader sidebar_active_tab])
|
|
432
|
+
key, action_key, max = case tab
|
|
433
|
+
when :toc
|
|
434
|
+
doc = safe_resolve(:document)
|
|
435
|
+
entries = toc_entries_for(doc)
|
|
436
|
+
raw_collapsed = @state.get(%i[reader sidebar_toc_collapsed])
|
|
437
|
+
collapsed = toc_collapsed_for(entries, raw_collapsed)
|
|
438
|
+
indices = navigable_toc_entry_indices(entries, collapsed)
|
|
439
|
+
first_index = indices.first
|
|
440
|
+
last_index = indices.last
|
|
441
|
+
cur = (@state.get(%i[reader sidebar_toc_selected]) || first_index || 0).to_i
|
|
442
|
+
cur = ensure_visible_toc_selection(entries, collapsed, cur)
|
|
443
|
+
target = if delta.positive?
|
|
444
|
+
indices.find { |idx| idx > cur } || last_index || cur
|
|
445
|
+
elsif delta.negative?
|
|
446
|
+
indices.reverse.find { |idx| idx < cur } || first_index || cur
|
|
447
|
+
else
|
|
448
|
+
cur
|
|
449
|
+
end
|
|
450
|
+
updates = { toc_selected: target }
|
|
451
|
+
updates[:toc_collapsed] = collapsed if raw_collapsed != collapsed
|
|
452
|
+
@state.dispatch(Shoko::Application::Actions::UpdateSidebarAction.new(updates))
|
|
453
|
+
return
|
|
454
|
+
when :annotations
|
|
455
|
+
cur = @state.get(%i[reader sidebar_annotations_selected]) || 0
|
|
456
|
+
max = (@state.get(%i[reader annotations]) || []).length - 1
|
|
457
|
+
[:sidebar_annotations_selected, :annotations_selected, max]
|
|
458
|
+
when :bookmarks
|
|
459
|
+
cur = @state.get(%i[reader sidebar_bookmarks_selected]) || 0
|
|
460
|
+
max = (@state.get(%i[reader bookmarks]) || []).length - 1
|
|
461
|
+
[:sidebar_bookmarks_selected, :bookmarks_selected, max]
|
|
462
|
+
else
|
|
463
|
+
[nil, nil, nil]
|
|
464
|
+
end
|
|
465
|
+
return unless key && action_key
|
|
466
|
+
|
|
467
|
+
current = @state.get([:reader, key]) || 0
|
|
468
|
+
max0 = [max, 0].max
|
|
469
|
+
new_val = (current + delta).clamp(0, max0)
|
|
470
|
+
@state.dispatch(Shoko::Application::Actions::UpdateSidebarAction.new(action_key => new_val))
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def toc_entries_for(doc)
|
|
474
|
+
entries = doc.respond_to?(:toc_entries) ? Array(doc.toc_entries) : []
|
|
475
|
+
return entries unless entries.empty?
|
|
476
|
+
|
|
477
|
+
chapters = doc.respond_to?(:chapters) ? Array(doc.chapters) : []
|
|
478
|
+
chapters.each_with_index.map do |chapter, idx|
|
|
479
|
+
title = chapter.respond_to?(:title) ? chapter.title.to_s : ''
|
|
480
|
+
title = "Chapter #{idx + 1}" if title.strip.empty?
|
|
481
|
+
Core::Models::TOCEntry.new(
|
|
482
|
+
title: title,
|
|
483
|
+
href: nil,
|
|
484
|
+
level: 0,
|
|
485
|
+
chapter_index: idx,
|
|
486
|
+
navigable: true
|
|
487
|
+
)
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def toc_collapsed_for(entries, raw = nil)
|
|
492
|
+
raw = @state.get(%i[reader sidebar_toc_collapsed]) if raw.nil?
|
|
493
|
+
entries = Array(entries)
|
|
494
|
+
return [] if entries.empty?
|
|
495
|
+
return default_toc_collapsed(entries) if raw.nil?
|
|
496
|
+
|
|
497
|
+
normalize_toc_collapsed(entries, raw)
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
def toggle_toc_collapsed(collapsed, index)
|
|
501
|
+
list = Array(collapsed).dup
|
|
502
|
+
if list.include?(index)
|
|
503
|
+
list.delete(index)
|
|
504
|
+
else
|
|
505
|
+
list << index
|
|
506
|
+
end
|
|
507
|
+
list
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def ensure_visible_toc_selection(entries, collapsed, current)
|
|
511
|
+
visible = toc_visible_indices(entries, collapsed)
|
|
512
|
+
return current if visible.include?(current)
|
|
513
|
+
return visible.first || 0 if visible.empty?
|
|
514
|
+
|
|
515
|
+
current_level = entries[current]&.level
|
|
516
|
+
if current_level
|
|
517
|
+
visible_set = visible.each_with_object({}) { |idx, memo| memo[idx] = true }
|
|
518
|
+
(current - 1).downto(0) do |idx|
|
|
519
|
+
next unless visible_set[idx]
|
|
520
|
+
return idx if entries[idx].level < current_level
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
visible.reverse.find { |idx| idx < current } || visible.first
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def toc_visible_indices(entries, collapsed)
|
|
528
|
+
entries = Array(entries)
|
|
529
|
+
return [] if entries.empty?
|
|
530
|
+
|
|
531
|
+
collapsed_set = Array(collapsed).each_with_object({}) { |idx, memo| memo[idx] = true }
|
|
532
|
+
visible = []
|
|
533
|
+
skip_levels = []
|
|
534
|
+
|
|
535
|
+
entries.each_with_index do |entry, idx|
|
|
536
|
+
level = entry.level
|
|
537
|
+
skip_levels.pop while skip_levels.any? && level <= skip_levels.last
|
|
538
|
+
next if skip_levels.any?
|
|
539
|
+
|
|
540
|
+
visible << idx
|
|
541
|
+
next unless collapsed_set[idx]
|
|
542
|
+
next unless toc_entry_has_children?(entries, idx)
|
|
543
|
+
|
|
544
|
+
skip_levels << level
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
visible
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def default_toc_collapsed(entries)
|
|
551
|
+
entries.each_index.select { |idx| toc_entry_has_children?(entries, idx) }
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def normalize_toc_collapsed(entries, raw)
|
|
555
|
+
max_index = entries.length - 1
|
|
556
|
+
Array(raw).map(&:to_i).uniq.select do |idx|
|
|
557
|
+
idx.between?(0, max_index) && toc_entry_has_children?(entries, idx)
|
|
558
|
+
end
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def toc_entry_has_children?(entries, index)
|
|
562
|
+
next_entry = entries[index + 1]
|
|
563
|
+
next_entry && next_entry.level > entries[index].level
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def navigable_toc_entry_indices(entries, collapsed)
|
|
567
|
+
visible = toc_visible_indices(entries, collapsed)
|
|
568
|
+
indices = visible.select { |idx| entries[idx]&.chapter_index }
|
|
569
|
+
return indices unless indices.empty?
|
|
570
|
+
|
|
571
|
+
visible
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
def toc_index_for_chapter(entries, chapter_index)
|
|
575
|
+
Array(entries).find_index { |entry| entry&.chapter_index == chapter_index } || 0
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
def safe_resolve(name)
|
|
579
|
+
@dependencies.resolve(name)
|
|
580
|
+
rescue StandardError
|
|
581
|
+
nil
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
def handle_create_annotation_action(action_data)
|
|
585
|
+
selection_range = if action_data.is_a?(Hash)
|
|
586
|
+
action_data[:data][:selection_range]
|
|
587
|
+
else
|
|
588
|
+
@state.get(%i[
|
|
589
|
+
reader selection
|
|
590
|
+
])
|
|
591
|
+
end
|
|
592
|
+
# Extract selected text from the controller that manages it
|
|
593
|
+
selected_text = extract_selected_text_from_selection(selection_range)
|
|
594
|
+
close_annotations_overlay
|
|
595
|
+
show_annotation_editor_overlay(text: selected_text,
|
|
596
|
+
range: selection_range,
|
|
597
|
+
chapter_index: @state.get(%i[reader current_chapter]))
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
def handle_copy_to_clipboard_action(_action_data)
|
|
601
|
+
clipboard_service = @dependencies.resolve(:clipboard_service)
|
|
602
|
+
# Get selected text from current selection
|
|
603
|
+
selection = @state.get(%i[reader selection])
|
|
604
|
+
selected_text = extract_selected_text_from_selection(selection)
|
|
605
|
+
|
|
606
|
+
if clipboard_service.available? && selected_text && !selected_text.strip.empty?
|
|
607
|
+
success = clipboard_service.copy_with_feedback(selected_text) do |msg|
|
|
608
|
+
set_message(msg)
|
|
609
|
+
end
|
|
610
|
+
set_message(' Failed to copy to clipboard') unless success
|
|
611
|
+
else
|
|
612
|
+
set_message(' Copy to clipboard not available')
|
|
613
|
+
end
|
|
614
|
+
switch_mode(:read)
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
# Extract selected text from selection range using SelectionService
|
|
618
|
+
def extract_selected_text_from_selection(selection_range)
|
|
619
|
+
selection_service = @dependencies.resolve(:selection_service)
|
|
620
|
+
if selection_service.respond_to?(:extract_from_state)
|
|
621
|
+
selection_service.extract_from_state(@state, selection_range)
|
|
622
|
+
else
|
|
623
|
+
rendered_lines = Shoko::Application::Selectors::ReaderSelectors.rendered_lines(@state)
|
|
624
|
+
selection_service.extract_text(selection_range, rendered_lines)
|
|
625
|
+
end
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
def open_annotation_from_overlay(annotation)
|
|
629
|
+
with_normalized_annotation(annotation) do |normalized|
|
|
630
|
+
state_controller = @dependencies.resolve(:state_controller)
|
|
631
|
+
state_controller.jump_to_annotation(normalized) if state_controller.respond_to?(:jump_to_annotation)
|
|
632
|
+
close_annotations_overlay
|
|
633
|
+
end
|
|
634
|
+
rescue StandardError
|
|
635
|
+
close_annotations_overlay
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
def edit_annotation_from_overlay(annotation)
|
|
639
|
+
with_normalized_annotation(annotation) do |normalized|
|
|
640
|
+
close_annotations_overlay
|
|
641
|
+
show_annotation_editor_overlay(text: normalized[:text],
|
|
642
|
+
range: normalized[:range],
|
|
643
|
+
chapter_index: normalized[:chapter_index],
|
|
644
|
+
annotation: normalized)
|
|
645
|
+
end
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
def delete_annotation_from_overlay(annotation)
|
|
649
|
+
with_normalized_annotation(annotation) do |normalized|
|
|
650
|
+
state_controller = @dependencies.resolve(:state_controller)
|
|
651
|
+
new_index = if state_controller.respond_to?(:delete_annotation_by_id)
|
|
652
|
+
state_controller.delete_annotation_by_id(normalized)
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
overlay = Application::Selectors::ReaderSelectors.annotations_overlay(@state)
|
|
656
|
+
overlay.selected_index = new_index if overlay.respond_to?(:selected_index=) && !new_index.nil?
|
|
657
|
+
|
|
658
|
+
annotations = @state.get(%i[reader annotations]) || []
|
|
659
|
+
close_annotations_overlay if annotations.empty?
|
|
660
|
+
set_message('Annotation deleted', 2)
|
|
661
|
+
end
|
|
662
|
+
rescue StandardError
|
|
663
|
+
close_annotations_overlay
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
def cleanup_annotations_overlay_fallback
|
|
667
|
+
@state.dispatch(Shoko::Application::Actions::ClearAnnotationsOverlayAction.new)
|
|
668
|
+
rescue StandardError
|
|
669
|
+
nil
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
def normalize_annotation(annotation)
|
|
673
|
+
return nil unless annotation.is_a?(Hash)
|
|
674
|
+
|
|
675
|
+
annotation.transform_keys do |key|
|
|
676
|
+
key.is_a?(String) ? key.to_sym : key
|
|
677
|
+
end
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
def with_normalized_annotation(annotation)
|
|
681
|
+
normalized = normalize_annotation(annotation)
|
|
682
|
+
return unless normalized
|
|
683
|
+
|
|
684
|
+
yield normalized
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
def cleanup_annotation_editor_overlay_fallback
|
|
688
|
+
@state.dispatch(Shoko::Application::Actions::ClearAnnotationEditorOverlayAction.new)
|
|
689
|
+
deactivate_annotation_editor_overlay_session
|
|
690
|
+
rescue StandardError
|
|
691
|
+
nil
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
def handle_annotation_editor_overlay_event(result)
|
|
695
|
+
overlay = Application::Selectors::ReaderSelectors.annotation_editor_overlay(@state)
|
|
696
|
+
return unless overlay
|
|
697
|
+
|
|
698
|
+
case result[:type]
|
|
699
|
+
when :save
|
|
700
|
+
save_annotation_from_overlay(result[:note], overlay)
|
|
701
|
+
when :cancel
|
|
702
|
+
cancel_annotation_editor_overlay
|
|
703
|
+
end
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
def save_annotation_from_overlay(note, overlay)
|
|
707
|
+
svc = @dependencies.resolve(:annotation_service)
|
|
708
|
+
path = current_book_path
|
|
709
|
+
unless svc && path
|
|
710
|
+
cancel_annotation_editor_overlay
|
|
711
|
+
return
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
begin
|
|
715
|
+
if overlay.annotation_id
|
|
716
|
+
svc.update(path, overlay.annotation_id, note)
|
|
717
|
+
set_message('Annotation updated', 2)
|
|
718
|
+
else
|
|
719
|
+
svc.add(path, overlay.selected_text, note, overlay.selection_range, overlay.chapter_index, nil)
|
|
720
|
+
set_message('Annotation saved!', 2)
|
|
721
|
+
end
|
|
722
|
+
refresh_annotations
|
|
723
|
+
rescue StandardError => e
|
|
724
|
+
set_message("Save failed: #{e.message}", 3)
|
|
725
|
+
ensure
|
|
726
|
+
close_annotation_editor_overlay
|
|
727
|
+
@state.dispatch(Shoko::Application::Actions::ClearSelectionAction.new)
|
|
728
|
+
end
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
def cancel_annotation_editor_overlay
|
|
732
|
+
close_annotation_editor_overlay
|
|
733
|
+
set_message('Annotation cancelled', 2)
|
|
734
|
+
@state.dispatch(Shoko::Application::Actions::ClearSelectionAction.new)
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
def activate_annotation_editor_overlay_session
|
|
738
|
+
reader_controller = resolve_required(:reader_controller)
|
|
739
|
+
input_controller = resolve_required(:input_controller)
|
|
740
|
+
reader_controller.activate_annotation_editor_overlay_session
|
|
741
|
+
input_controller.enter_modal_mode(:annotation_editor)
|
|
742
|
+
true
|
|
743
|
+
rescue MissingDependencyError => e
|
|
744
|
+
log_dependency_error(:activate_annotation_editor_overlay_session, e)
|
|
745
|
+
false
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
def deactivate_annotation_editor_overlay_session
|
|
749
|
+
input_controller = resolve_optional(:input_controller)
|
|
750
|
+
input_controller&.exit_modal_mode(:annotation_editor)
|
|
751
|
+
reader_controller = resolve_optional(:reader_controller)
|
|
752
|
+
reader_controller&.deactivate_annotation_editor_overlay_session
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
def resolve_required(key)
|
|
756
|
+
service = @dependencies.resolve(key)
|
|
757
|
+
raise MissingDependencyError, "Dependency :#{key} not registered" unless service
|
|
758
|
+
|
|
759
|
+
service
|
|
760
|
+
rescue MissingDependencyError
|
|
761
|
+
raise
|
|
762
|
+
rescue StandardError => e
|
|
763
|
+
raise MissingDependencyError, "Dependency :#{key} failed to resolve: #{e.message}"
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
def resolve_optional(key)
|
|
767
|
+
@dependencies.resolve(key)
|
|
768
|
+
rescue StandardError
|
|
769
|
+
nil
|
|
770
|
+
end
|
|
771
|
+
|
|
772
|
+
def log_dependency_error(context, error)
|
|
773
|
+
logger = resolve_optional(:logger)
|
|
774
|
+
return unless logger.respond_to?(:error)
|
|
775
|
+
|
|
776
|
+
logger.error('Annotation editor activation failed', context: context, error: error.message)
|
|
777
|
+
rescue StandardError
|
|
778
|
+
nil
|
|
779
|
+
end
|
|
780
|
+
end
|
|
781
|
+
end
|
|
782
|
+
end
|