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,265 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_service'
|
|
4
|
+
require_relative '../models/selection_anchor'
|
|
5
|
+
|
|
6
|
+
module Shoko
|
|
7
|
+
module Core
|
|
8
|
+
module Services
|
|
9
|
+
# Domain service for coordinate system management with dependency injection.
|
|
10
|
+
# Migrated from legacy Services::CoordinateService to follow DI pattern.
|
|
11
|
+
class CoordinateService < BaseService
|
|
12
|
+
# Convert mouse coordinates (0-based) to terminal coordinates (1-based)
|
|
13
|
+
def mouse_to_terminal(mouse_x, mouse_y)
|
|
14
|
+
{
|
|
15
|
+
x: mouse_x + 1,
|
|
16
|
+
y: mouse_y + 1,
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Convert terminal coordinates (1-based) to mouse coordinates (0-based)
|
|
21
|
+
def terminal_to_mouse(terminal_x, terminal_y)
|
|
22
|
+
{
|
|
23
|
+
x: [terminal_x - 1, 0].max,
|
|
24
|
+
y: [terminal_y - 1, 0].max,
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Normalize selection range ensuring start <= end. Accepts either
|
|
29
|
+
# geometry-based anchors or legacy screen coordinate hashes.
|
|
30
|
+
#
|
|
31
|
+
# @param selection_range [Hash]
|
|
32
|
+
# @param rendered_lines [Hash]
|
|
33
|
+
# @return [Hash,nil]
|
|
34
|
+
def normalize_selection_range(selection_range, rendered_lines = nil)
|
|
35
|
+
return nil unless selection_range
|
|
36
|
+
|
|
37
|
+
start_anchor = normalize_anchor(selection_range[:start], rendered_lines)
|
|
38
|
+
end_anchor = normalize_anchor(selection_range[:end], rendered_lines)
|
|
39
|
+
return nil unless start_anchor && end_anchor
|
|
40
|
+
|
|
41
|
+
start_anchor, end_anchor = end_anchor, start_anchor if (start_anchor <=> end_anchor)&.positive?
|
|
42
|
+
|
|
43
|
+
{
|
|
44
|
+
start: start_anchor.to_h,
|
|
45
|
+
end: end_anchor.to_h,
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Validate coordinate bounds
|
|
50
|
+
def validate_coordinates?(col, row, max_col, max_row)
|
|
51
|
+
col.between?(1, max_col) && row.between?(1, max_row)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Calculate distance between two points
|
|
55
|
+
def calculate_distance(x_start, y_start, x_end, y_end)
|
|
56
|
+
Math.sqrt(((x_end - x_start)**2) + ((y_end - y_start)**2))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Calculate optimal popup position near selection end
|
|
60
|
+
def calculate_popup_position(selection_end, popup_width, popup_height)
|
|
61
|
+
terminal_height, terminal_width = @dependencies.resolve(:terminal_service).size
|
|
62
|
+
|
|
63
|
+
# Start with position below selection end
|
|
64
|
+
end_y = selection_end[:y]
|
|
65
|
+
popup_x = selection_end[:x]
|
|
66
|
+
popup_y = end_y + 1
|
|
67
|
+
|
|
68
|
+
# Adjust if popup would go off right edge
|
|
69
|
+
popup_x = [terminal_width - popup_width, 1].max if popup_x + popup_width > terminal_width
|
|
70
|
+
|
|
71
|
+
# Adjust if popup would go off bottom edge
|
|
72
|
+
if popup_y + popup_height > terminal_height
|
|
73
|
+
# Try to position above selection instead
|
|
74
|
+
popup_y = [end_y - popup_height, 1].max
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
{
|
|
78
|
+
x: popup_x,
|
|
79
|
+
y: popup_y,
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Check if coordinates are within bounds
|
|
84
|
+
def within_bounds?(col, row, bounds)
|
|
85
|
+
bx = bounds.x
|
|
86
|
+
by = bounds.y
|
|
87
|
+
bw = bounds.width
|
|
88
|
+
bh = bounds.height
|
|
89
|
+
col >= bx && col < (bx + bw) && row >= by && row < (by + bh)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Convert line-relative coordinates to absolute terminal coordinates
|
|
93
|
+
def line_to_terminal(line_col, line_start_col, terminal_row)
|
|
94
|
+
{
|
|
95
|
+
x: line_start_col + line_col,
|
|
96
|
+
y: terminal_row,
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Normalize position hash to consistent format
|
|
101
|
+
def normalize_position(pos)
|
|
102
|
+
return nil unless pos
|
|
103
|
+
|
|
104
|
+
{
|
|
105
|
+
x: pos[:x] || pos['x'],
|
|
106
|
+
y: pos[:y] || pos['y'],
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Determine the column bounds (start..end) for a given click/selection position
|
|
111
|
+
# by inspecting rendered geometry. Maintained for compatibility with
|
|
112
|
+
# consumers that still reason about legacy coordinate ranges.
|
|
113
|
+
def column_bounds_for(click_pos, rendered_lines)
|
|
114
|
+
pos = normalize_position(click_pos)
|
|
115
|
+
return nil unless pos
|
|
116
|
+
|
|
117
|
+
anchor_geometry = locate_geometry(rendered_lines, pos[:x], pos[:y])
|
|
118
|
+
return nil unless anchor_geometry
|
|
119
|
+
|
|
120
|
+
start_col = anchor_geometry.column_origin
|
|
121
|
+
{
|
|
122
|
+
start: start_col,
|
|
123
|
+
end: start_col + anchor_geometry.visible_width,
|
|
124
|
+
}
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def column_overlaps?(line_start, line_end, bounds)
|
|
128
|
+
return false unless bounds
|
|
129
|
+
|
|
130
|
+
!(line_end < bounds[:start] || line_start > bounds[:end])
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Build an anchor from screen coordinates (0-based) using rendered geometry.
|
|
134
|
+
def anchor_from_point(point, rendered_lines, bias: :nearest)
|
|
135
|
+
pos = normalize_position(point)
|
|
136
|
+
return nil unless pos
|
|
137
|
+
|
|
138
|
+
geometry = locate_geometry(rendered_lines, pos[:x], pos[:y])
|
|
139
|
+
return nil unless geometry
|
|
140
|
+
|
|
141
|
+
cell_index = cell_index_for_geometry(geometry, pos[:x], bias)
|
|
142
|
+
|
|
143
|
+
Shoko::Core::Models::SelectionAnchor.new(
|
|
144
|
+
page_id: geometry.page_id,
|
|
145
|
+
column_id: geometry.column_id,
|
|
146
|
+
geometry_key: geometry.key,
|
|
147
|
+
line_offset: geometry.line_offset,
|
|
148
|
+
cell_index: cell_index,
|
|
149
|
+
row: geometry.row,
|
|
150
|
+
column_origin: geometry.column_origin
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Convenience helper for highlight logic: find the rendered line geometry
|
|
155
|
+
# that covers the provided terminal row.
|
|
156
|
+
def geometry_for_row(rendered_lines, row)
|
|
157
|
+
index = geometry_index_by_row(rendered_lines)
|
|
158
|
+
candidates = index[row]
|
|
159
|
+
candidates && candidates.first
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
protected
|
|
163
|
+
|
|
164
|
+
def required_dependencies
|
|
165
|
+
[] # No dependencies required for coordinate operations
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
private
|
|
169
|
+
|
|
170
|
+
def normalize_anchor(anchor, rendered_lines)
|
|
171
|
+
return nil unless anchor
|
|
172
|
+
|
|
173
|
+
selection_anchor = Shoko::Core::Models::SelectionAnchor.from(anchor)
|
|
174
|
+
return selection_anchor if selection_anchor&.geometry_key
|
|
175
|
+
|
|
176
|
+
return nil unless rendered_lines
|
|
177
|
+
|
|
178
|
+
anchor_from_point(anchor, rendered_lines)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def locate_geometry(rendered_lines, mouse_x, mouse_y)
|
|
182
|
+
row = mouse_y.to_i + 1
|
|
183
|
+
col = mouse_x.to_i + 1
|
|
184
|
+
|
|
185
|
+
candidates = geometry_index_by_row(rendered_lines)[row] || []
|
|
186
|
+
return nil if candidates.empty?
|
|
187
|
+
|
|
188
|
+
candidates.each do |geometry|
|
|
189
|
+
line_start = geometry.column_origin
|
|
190
|
+
line_end = line_start + geometry.visible_width
|
|
191
|
+
|
|
192
|
+
if col < line_start
|
|
193
|
+
return geometry if geometry.visible_width.zero?
|
|
194
|
+
|
|
195
|
+
next
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
return geometry if col <= line_end || geometry.visible_width.zero?
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# No direct hit; fall back to the last geometry on the row for trailing whitespace selection.
|
|
202
|
+
candidates.last
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def cell_index_for_geometry(geometry, mouse_x, bias)
|
|
206
|
+
cells = geometry.cells
|
|
207
|
+
return 0 if cells.empty?
|
|
208
|
+
|
|
209
|
+
target_col = mouse_x.to_i + 1
|
|
210
|
+
relative = target_col - geometry.column_origin
|
|
211
|
+
relative = 0 if relative.negative?
|
|
212
|
+
|
|
213
|
+
cells.each_with_index do |cell, index|
|
|
214
|
+
cell_start = cell.screen_x
|
|
215
|
+
cell_end = cell_start + cell.display_width
|
|
216
|
+
|
|
217
|
+
if relative < cell_start
|
|
218
|
+
return clamp_cell_index(index, cells.length, bias)
|
|
219
|
+
elsif relative < cell_end
|
|
220
|
+
return bias == :trailing ? [index + 1, cells.length].min : index
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
return cells.length if bias == :trailing
|
|
225
|
+
|
|
226
|
+
[cells.length - 1, 0].max
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def clamp_cell_index(index, cell_count, bias)
|
|
230
|
+
case bias
|
|
231
|
+
when :trailing
|
|
232
|
+
[[index, cell_count].min, 0].max
|
|
233
|
+
when :leading
|
|
234
|
+
[[index, cell_count - 1].min, 0].max
|
|
235
|
+
else
|
|
236
|
+
index
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def geometry_index_by_row(rendered_lines)
|
|
241
|
+
return {} unless rendered_lines
|
|
242
|
+
|
|
243
|
+
key = rendered_lines.object_id
|
|
244
|
+
if @geometry_index_key != key
|
|
245
|
+
@geometry_index_key = key
|
|
246
|
+
@geometry_index_by_row = build_geometry_index(rendered_lines)
|
|
247
|
+
end
|
|
248
|
+
@geometry_index_by_row || {}
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def build_geometry_index(rendered_lines)
|
|
252
|
+
index = Hash.new { |h, k| h[k] = [] }
|
|
253
|
+
rendered_lines.each_value do |line_info|
|
|
254
|
+
geometry = line_info[:geometry]
|
|
255
|
+
next unless geometry
|
|
256
|
+
|
|
257
|
+
index[geometry.row] << geometry
|
|
258
|
+
end
|
|
259
|
+
index.each_value { |rows| rows.sort_by!(&:column_origin) }
|
|
260
|
+
index
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_service'
|
|
4
|
+
|
|
5
|
+
module Shoko
|
|
6
|
+
module Core
|
|
7
|
+
module Services
|
|
8
|
+
# Domain service for layout calculations with dependency injection.
|
|
9
|
+
# Migrated from legacy Services::LayoutService to follow DI pattern.
|
|
10
|
+
class LayoutService < BaseService
|
|
11
|
+
# Shared layout constants so pagination and rendering stay in sync
|
|
12
|
+
SPLIT_LEFT_MARGIN = 2
|
|
13
|
+
SPLIT_RIGHT_MARGIN = 2
|
|
14
|
+
SPLIT_COLUMN_GAP = 4
|
|
15
|
+
SPLIT_MIN_USABLE_WIDTH = 40
|
|
16
|
+
MIN_COLUMN_WIDTH = 20
|
|
17
|
+
CONTENT_TOP_PADDING = 2
|
|
18
|
+
CONTENT_BOTTOM_PADDING = 1
|
|
19
|
+
CONTENT_VERTICAL_PADDING = CONTENT_TOP_PADDING + CONTENT_BOTTOM_PADDING
|
|
20
|
+
|
|
21
|
+
# Calculate layout metrics for given dimensions and view mode
|
|
22
|
+
def calculate_metrics(width, height, view_mode)
|
|
23
|
+
col_width = view_mode == :split ? split_column_width(width) : single_column_width(width)
|
|
24
|
+
content_height = content_area_height(height)
|
|
25
|
+
[col_width, content_height]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Adjust height for line spacing
|
|
29
|
+
def adjust_for_line_spacing(height, line_spacing)
|
|
30
|
+
return 1 if height <= 0
|
|
31
|
+
|
|
32
|
+
spacing = begin
|
|
33
|
+
line_spacing&.to_sym
|
|
34
|
+
rescue StandardError
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
return [(height + 1) / 2, 1].max if spacing == :relaxed
|
|
39
|
+
|
|
40
|
+
multiplier = resolve_multiplier(spacing)
|
|
41
|
+
adjusted = (height * multiplier).floor
|
|
42
|
+
adjusted = height if multiplier >= 1.0 && adjusted < height
|
|
43
|
+
[adjusted, 1].max
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Calculate center start row for content
|
|
47
|
+
def calculate_center_start_row(content_height, lines_count, line_spacing)
|
|
48
|
+
actual_lines = line_spacing == :relaxed ? [(lines_count * 2) - 1, 0].max : lines_count
|
|
49
|
+
padding = [(content_height - actual_lines) / 2, 0].max
|
|
50
|
+
[3 + padding, 3].max
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Calculate optimal column width for text content
|
|
54
|
+
def calculate_optimal_column_width(available_width, _content_length)
|
|
55
|
+
# Use golden ratio for optimal reading width
|
|
56
|
+
optimal = (available_width * 0.618).to_i
|
|
57
|
+
optimal.clamp(40, 80) # Keep within readable bounds
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Calculate padding for centered content
|
|
61
|
+
def calculate_centered_padding(container_width, content_width)
|
|
62
|
+
[(container_width - content_width) / 2, 0].max
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def content_area_height(height)
|
|
66
|
+
[height - CONTENT_VERTICAL_PADDING, 1].max
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def split_column_width(width)
|
|
70
|
+
usable_width = [width - SPLIT_LEFT_MARGIN - SPLIT_RIGHT_MARGIN, SPLIT_MIN_USABLE_WIDTH].max
|
|
71
|
+
[(usable_width - SPLIT_COLUMN_GAP) / 2, MIN_COLUMN_WIDTH].max
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def single_column_width(width)
|
|
75
|
+
(width * Core::Models::ReaderSettings::SINGLE_VIEW_WIDTH_PERCENT).to_i.clamp(30, 120)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def resolve_multiplier(line_spacing)
|
|
79
|
+
key = begin
|
|
80
|
+
line_spacing&.to_sym
|
|
81
|
+
rescue StandardError
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
Shoko::Core::Models::ReaderSettings::LINE_SPACING_MULTIPLIERS.fetch(key, 1.0)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
protected
|
|
88
|
+
|
|
89
|
+
def required_dependencies
|
|
90
|
+
[] # No dependencies required for layout calculations
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'context_helpers'
|
|
4
|
+
|
|
5
|
+
module Shoko
|
|
6
|
+
module Core
|
|
7
|
+
module Services
|
|
8
|
+
module Navigation
|
|
9
|
+
# Applies absolute-mode changes and computes offsets from layout data.
|
|
10
|
+
class AbsoluteChangeApplier
|
|
11
|
+
CHANGE_PATHS = {
|
|
12
|
+
current_chapter: %i[reader current_chapter],
|
|
13
|
+
current_page: %i[reader current_page],
|
|
14
|
+
single_page: %i[reader single_page],
|
|
15
|
+
left_page: %i[reader left_page],
|
|
16
|
+
right_page: %i[reader right_page],
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
def initialize(state_updater:, absolute_layout:, image_snapper:, advance_callback:)
|
|
20
|
+
@state_updater = state_updater
|
|
21
|
+
@absolute_layout = absolute_layout
|
|
22
|
+
@image_snapper = image_snapper
|
|
23
|
+
@advance_callback = advance_callback
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def apply(changes)
|
|
27
|
+
return if changes.nil? || changes.empty?
|
|
28
|
+
|
|
29
|
+
layout_state = @absolute_layout.build
|
|
30
|
+
return if handle_advance(changes[:advance_chapter], layout_state)
|
|
31
|
+
|
|
32
|
+
updates = build_updates(changes)
|
|
33
|
+
updates = apply_align_to_last(updates, changes, layout_state)
|
|
34
|
+
updates = @image_snapper.snap(updates, layout_state) if @image_snapper
|
|
35
|
+
@state_updater.apply(updates)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def handle_advance(advance, layout_state)
|
|
41
|
+
return false unless advance
|
|
42
|
+
|
|
43
|
+
current_chapter = ContextHelpers.current_chapter(layout_state.snapshot)
|
|
44
|
+
case advance
|
|
45
|
+
when :next
|
|
46
|
+
@advance_callback.call(current_chapter + 1)
|
|
47
|
+
true
|
|
48
|
+
when :prev
|
|
49
|
+
previous = current_chapter - 1
|
|
50
|
+
return true if previous.negative?
|
|
51
|
+
|
|
52
|
+
offset = @absolute_layout.max_offset_for(layout_state.snapshot, previous, layout_state.stride)
|
|
53
|
+
updates = { %i[reader current_chapter] => previous }
|
|
54
|
+
apply_offset(updates, layout_state, offset)
|
|
55
|
+
@state_updater.apply(updates)
|
|
56
|
+
true
|
|
57
|
+
else
|
|
58
|
+
false
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def build_updates(changes)
|
|
63
|
+
updates = {}
|
|
64
|
+
CHANGE_PATHS.each do |key, path|
|
|
65
|
+
updates[path] = changes[key] if changes.key?(key)
|
|
66
|
+
end
|
|
67
|
+
updates
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def apply_align_to_last(updates, changes, layout_state)
|
|
71
|
+
return updates unless changes[:align_to_last]
|
|
72
|
+
|
|
73
|
+
total = ContextHelpers.total_chapters(layout_state.snapshot)
|
|
74
|
+
total = 1 if total.to_i <= 0
|
|
75
|
+
last_chapter = total - 1
|
|
76
|
+
offset = @absolute_layout.max_offset_for(layout_state.snapshot, last_chapter, layout_state.stride)
|
|
77
|
+
updates[%i[reader current_chapter]] = last_chapter
|
|
78
|
+
apply_offset(updates, layout_state, offset)
|
|
79
|
+
updates
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def apply_offset(updates, layout_state, offset)
|
|
83
|
+
updates[%i[reader current_page]] = offset
|
|
84
|
+
if layout_state.view_mode == :split
|
|
85
|
+
updates[%i[reader left_page]] = offset
|
|
86
|
+
updates[%i[reader right_page]] = offset + layout_state.stride
|
|
87
|
+
else
|
|
88
|
+
updates[%i[reader single_page]] = offset
|
|
89
|
+
end
|
|
90
|
+
updates
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'context_helpers'
|
|
4
|
+
|
|
5
|
+
module Shoko
|
|
6
|
+
module Core
|
|
7
|
+
module Services
|
|
8
|
+
module Navigation
|
|
9
|
+
# Computes absolute-layout metrics and enriches navigation contexts.
|
|
10
|
+
class AbsoluteLayout
|
|
11
|
+
# Snapshot of layout-derived values for absolute navigation.
|
|
12
|
+
LayoutState = Struct.new(:snapshot, :view_mode, :metrics, :stride, keyword_init: true)
|
|
13
|
+
|
|
14
|
+
def initialize(state_store:, layout_service:)
|
|
15
|
+
@state_store = state_store
|
|
16
|
+
@layout_service = layout_service
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def build
|
|
20
|
+
snapshot = ContextHelpers.safe_snapshot(@state_store)
|
|
21
|
+
view_mode = ContextHelpers.current_view_mode(snapshot)
|
|
22
|
+
metrics = {
|
|
23
|
+
single: lines_for(snapshot, :single),
|
|
24
|
+
split: lines_for(snapshot, :split),
|
|
25
|
+
}
|
|
26
|
+
stride = view_mode == :split ? metrics[:split] : metrics[:single]
|
|
27
|
+
stride = metrics[:single] if stride.to_i <= 0
|
|
28
|
+
stride = 1 if stride.to_i <= 0
|
|
29
|
+
LayoutState.new(snapshot: snapshot, view_mode: view_mode, metrics: metrics, stride: stride)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def populate_context(ctx)
|
|
33
|
+
return ctx unless ctx.mode == :absolute
|
|
34
|
+
|
|
35
|
+
layout_state = build
|
|
36
|
+
ctx.lines_per_page = layout_state.metrics[:single]
|
|
37
|
+
ctx.column_lines_per_page = layout_state.metrics[:split]
|
|
38
|
+
ctx.max_page_in_chapter = page_count(layout_state.snapshot, ctx.current_chapter)
|
|
39
|
+
ctx.max_offset_in_chapter = max_offset_for(layout_state.snapshot, ctx.current_chapter, layout_state.stride)
|
|
40
|
+
ctx
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def page_count(snapshot, chapter_index)
|
|
44
|
+
return 0 if chapter_index.nil?
|
|
45
|
+
|
|
46
|
+
snapshot.dig(:reader, :page_map)&.[](chapter_index) || 0
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def max_offset_for(snapshot, chapter_index, stride)
|
|
50
|
+
return 0 if chapter_index.nil? || stride.to_i <= 0
|
|
51
|
+
|
|
52
|
+
pages = page_count(snapshot, chapter_index).to_i
|
|
53
|
+
return 0 if pages <= 1
|
|
54
|
+
|
|
55
|
+
(pages - 1) * stride
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def column_width(snapshot, view_mode)
|
|
59
|
+
return fallback_width(snapshot) unless @layout_service
|
|
60
|
+
|
|
61
|
+
width = fallback_width(snapshot)
|
|
62
|
+
height = fallback_height(snapshot)
|
|
63
|
+
col_width, = @layout_service.calculate_metrics(width, height, view_mode)
|
|
64
|
+
col_width = width if col_width.to_i <= 0
|
|
65
|
+
col_width
|
|
66
|
+
rescue StandardError
|
|
67
|
+
fallback_width(snapshot)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def lines_for(snapshot, view_mode)
|
|
73
|
+
return fallback_lines(view_mode) unless @layout_service
|
|
74
|
+
|
|
75
|
+
width = fallback_width(snapshot)
|
|
76
|
+
height = fallback_height(snapshot)
|
|
77
|
+
_, content_height = @layout_service.calculate_metrics(width, height, view_mode)
|
|
78
|
+
line_spacing = snapshot.dig(:config, :line_spacing) || Shoko::Core::Models::ReaderSettings::DEFAULT_LINE_SPACING
|
|
79
|
+
lines = @layout_service.adjust_for_line_spacing(content_height, line_spacing)
|
|
80
|
+
lines = 1 if lines.to_i <= 0
|
|
81
|
+
lines
|
|
82
|
+
rescue StandardError
|
|
83
|
+
1
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def fallback_width(snapshot)
|
|
87
|
+
snapshot.dig(:ui, :terminal_width) || 80
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def fallback_height(snapshot)
|
|
91
|
+
snapshot.dig(:ui, :terminal_height) || 24
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def fallback_lines(view_mode)
|
|
95
|
+
view_mode == :split ? 2 : 1
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|