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,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../pagination'
|
|
4
|
+
require_relative '../../../adapters/output/kitty/kitty_graphics.rb'
|
|
5
|
+
|
|
6
|
+
module Shoko::Core::Services::Pagination
|
|
7
|
+
# Centralises the logic for hydrating dynamic pagination from the cache.
|
|
8
|
+
class PaginationCachePreloader
|
|
9
|
+
# Preload outcome with an optional cache key.
|
|
10
|
+
Result = Struct.new(:status, :key, keyword_init: true)
|
|
11
|
+
# Requested terminal dimensions (before defaults are applied).
|
|
12
|
+
Dimensions = Struct.new(:width, :height, keyword_init: true)
|
|
13
|
+
# Layout metadata used for pagination cache lookups.
|
|
14
|
+
LayoutSpec = Struct.new(:key, :width, :height, :view_mode, :line_spacing, :kitty_images, keyword_init: true)
|
|
15
|
+
private_constant :Result, :Dimensions, :LayoutSpec
|
|
16
|
+
|
|
17
|
+
def initialize(state:, page_calculator:, pagination_cache:)
|
|
18
|
+
@state = state
|
|
19
|
+
@page_calculator = page_calculator
|
|
20
|
+
@pagination_cache = pagination_cache
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def preload(doc, width:, height:)
|
|
24
|
+
guard = guard_preload(doc)
|
|
25
|
+
return guard if guard
|
|
26
|
+
|
|
27
|
+
dimensions = resolve_dimensions(Dimensions.new(width: width, height: height))
|
|
28
|
+
layout, miss_key = resolve_layout(doc, dimensions)
|
|
29
|
+
return Result.new(status: :miss, key: miss_key) unless layout
|
|
30
|
+
|
|
31
|
+
apply_layout_config(layout)
|
|
32
|
+
cached_pages = load_cached_pages(doc, layout.key)
|
|
33
|
+
return Result.new(status: :miss, key: layout.key) unless cached_pages
|
|
34
|
+
|
|
35
|
+
hydrate_from_cache(cached_pages, dimensions)
|
|
36
|
+
Result.new(status: :hit, key: layout.key)
|
|
37
|
+
rescue StandardError => e
|
|
38
|
+
log_failure(e)
|
|
39
|
+
Result.new(status: :error)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
attr_reader :state, :page_calculator, :pagination_cache
|
|
45
|
+
|
|
46
|
+
def guard_preload(doc)
|
|
47
|
+
return Result.new(status: :invalid) unless doc
|
|
48
|
+
return Result.new(status: :unavailable) unless dynamic_mode?
|
|
49
|
+
|
|
50
|
+
Result.new(status: :no_calculator) unless page_calculator
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def resolve_dimensions(requested)
|
|
54
|
+
width = requested.width || state.get(%i[ui terminal_width]) || 80
|
|
55
|
+
height = requested.height || state.get(%i[ui terminal_height]) || 24
|
|
56
|
+
Dimensions.new(width: width, height: height)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def resolve_layout(doc, dimensions)
|
|
60
|
+
layout = build_layout_spec(dimensions)
|
|
61
|
+
miss_key = layout.key
|
|
62
|
+
return [layout, miss_key] if pagination_cache.exists_for_document?(doc, layout.key)
|
|
63
|
+
|
|
64
|
+
[find_fallback_layout(doc, layout), miss_key]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def dynamic_mode?
|
|
68
|
+
Shoko::Application::Selectors::ConfigSelectors.page_numbering_mode(state) == :dynamic
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def build_layout_spec(dimensions)
|
|
72
|
+
view_mode = current_view_mode
|
|
73
|
+
line_spacing = current_line_spacing
|
|
74
|
+
kitty_images = Shoko::Adapters::Output::Kitty::KittyGraphics.enabled_for?(state)
|
|
75
|
+
key = pagination_cache.layout_key(
|
|
76
|
+
dimensions.width,
|
|
77
|
+
dimensions.height,
|
|
78
|
+
view_mode,
|
|
79
|
+
line_spacing,
|
|
80
|
+
kitty_images: kitty_images
|
|
81
|
+
)
|
|
82
|
+
LayoutSpec.new(
|
|
83
|
+
key: key,
|
|
84
|
+
width: dimensions.width,
|
|
85
|
+
height: dimensions.height,
|
|
86
|
+
view_mode: view_mode,
|
|
87
|
+
line_spacing: line_spacing,
|
|
88
|
+
kitty_images: kitty_images
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def apply_layout_config(layout)
|
|
93
|
+
state.apply_terminal_dimensions(layout.width, layout.height)
|
|
94
|
+
update_config(layout)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def update_config(layout)
|
|
98
|
+
updates = {}
|
|
99
|
+
updates[%i[config view_mode]] = layout.view_mode if layout.view_mode
|
|
100
|
+
updates[%i[config line_spacing]] = layout.line_spacing if layout.line_spacing
|
|
101
|
+
state.update(updates) unless updates.empty?
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def load_cached_pages(doc, key)
|
|
105
|
+
cached_pages = pagination_cache.load_for_document(doc, key)
|
|
106
|
+
cached_pages if cached_pages&.any?
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def hydrate_from_cache(cached_pages, dimensions)
|
|
110
|
+
page_calculator.hydrate_from_cache(
|
|
111
|
+
cached_pages,
|
|
112
|
+
state: state,
|
|
113
|
+
width: dimensions.width,
|
|
114
|
+
height: dimensions.height
|
|
115
|
+
)
|
|
116
|
+
page_calculator.apply_pending_precise_restore!(state)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def log_failure(error)
|
|
120
|
+
logger = begin
|
|
121
|
+
state.resolve(:logger)
|
|
122
|
+
rescue StandardError
|
|
123
|
+
nil
|
|
124
|
+
end
|
|
125
|
+
logger&.debug('PaginationCachePreloader: failed', error: error.message)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def find_fallback_layout(doc, layout)
|
|
129
|
+
keys = pagination_cache.layout_keys_for_document(doc)
|
|
130
|
+
return nil if keys.empty?
|
|
131
|
+
|
|
132
|
+
preferred = preferred_key(keys, layout)
|
|
133
|
+
return nil unless preferred
|
|
134
|
+
|
|
135
|
+
layout_from_key(preferred)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def preferred_key(keys, layout)
|
|
139
|
+
keys.find { |candidate| layout_key_matches?(candidate, layout) }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def layout_key_matches?(candidate, layout)
|
|
143
|
+
parsed = pagination_cache.parse_layout_key(candidate)
|
|
144
|
+
return false unless parsed
|
|
145
|
+
|
|
146
|
+
parsed[:view_mode] == layout.view_mode &&
|
|
147
|
+
parsed[:line_spacing] == layout.line_spacing &&
|
|
148
|
+
parsed[:kitty_images] == layout.kitty_images
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def layout_from_key(key)
|
|
152
|
+
parsed = pagination_cache.parse_layout_key(key)
|
|
153
|
+
return nil unless parsed
|
|
154
|
+
|
|
155
|
+
LayoutSpec.new(
|
|
156
|
+
key: key,
|
|
157
|
+
width: parsed[:width],
|
|
158
|
+
height: parsed[:height],
|
|
159
|
+
view_mode: parsed[:view_mode],
|
|
160
|
+
line_spacing: parsed[:line_spacing],
|
|
161
|
+
kitty_images: parsed[:kitty_images]
|
|
162
|
+
)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def current_view_mode
|
|
166
|
+
Shoko::Application::Selectors::ConfigSelectors.view_mode(state)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def current_line_spacing
|
|
170
|
+
Shoko::Application::Selectors::ConfigSelectors.line_spacing(state)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../pagination'
|
|
4
|
+
require_relative 'page_info_calculator'
|
|
5
|
+
require_relative 'pagination_orchestrator'
|
|
6
|
+
|
|
7
|
+
module Shoko::Core::Services::Pagination
|
|
8
|
+
# Coordinates pagination-related workflows for the reader.
|
|
9
|
+
class PaginationCoordinator
|
|
10
|
+
# Aggregated pagination dependencies.
|
|
11
|
+
Dependencies = Struct.new(
|
|
12
|
+
:state,
|
|
13
|
+
:doc,
|
|
14
|
+
:page_calculator,
|
|
15
|
+
:layout_service,
|
|
16
|
+
:terminal_service,
|
|
17
|
+
:pagination_cache,
|
|
18
|
+
:frame_coordinator,
|
|
19
|
+
:ui_controller,
|
|
20
|
+
:render_callback,
|
|
21
|
+
:background_worker_provider,
|
|
22
|
+
keyword_init: true
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# State flags for deferred pagination behavior.
|
|
26
|
+
Flags = Struct.new(:pending_initial_calculation, :defer_page_map, keyword_init: true)
|
|
27
|
+
|
|
28
|
+
def initialize(dependencies:)
|
|
29
|
+
@deps = dependencies
|
|
30
|
+
@orchestrator = PaginationOrchestrator.new(
|
|
31
|
+
terminal_service: dependencies.terminal_service,
|
|
32
|
+
pagination_cache: dependencies.pagination_cache,
|
|
33
|
+
frame_coordinator: dependencies.frame_coordinator
|
|
34
|
+
)
|
|
35
|
+
@flags = Flags.new(pending_initial_calculation: true, defer_page_map: false)
|
|
36
|
+
seed_flags
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def pending_initial_calculation?
|
|
40
|
+
@flags.pending_initial_calculation
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def defer_page_map?
|
|
44
|
+
@flags.defer_page_map
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def clear_defer_page_map!
|
|
48
|
+
@flags.defer_page_map = false
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def perform_initial_calculations_if_needed
|
|
52
|
+
perform_initial_calculations_with_progress if pending_initial_calculation? && !preloaded_page_data?
|
|
53
|
+
@flags.pending_initial_calculation = false
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def schedule_background_page_map_build
|
|
57
|
+
return unless defer_page_map?
|
|
58
|
+
|
|
59
|
+
submit_background_job { build_page_map_in_background }
|
|
60
|
+
rescue StandardError
|
|
61
|
+
@flags.defer_page_map = false
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def refresh_after_resize(width:, height:)
|
|
65
|
+
return if defer_page_map?
|
|
66
|
+
|
|
67
|
+
session(dimensions: [width, height])&.refresh_after_resize
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def rebuild_after_config_change
|
|
71
|
+
session(dimensions: terminal_dimensions)&.rebuild_after_config_change
|
|
72
|
+
rescue StandardError
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def rebuild_dynamic
|
|
77
|
+
result = session&.rebuild_dynamic
|
|
78
|
+
render_callback&.call
|
|
79
|
+
result
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def rebuild_pagination(_key = nil)
|
|
83
|
+
rebuild_dynamic
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def invalidate_cache
|
|
87
|
+
result = session(dimensions: terminal_dimensions)&.invalidate_cache || :missing
|
|
88
|
+
apply_invalidate_message(result)
|
|
89
|
+
:handled
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def invalidate_pagination_cache(_key = nil)
|
|
93
|
+
invalidate_cache
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def page_info
|
|
97
|
+
calculator = PageInfoCalculator.new(
|
|
98
|
+
dependencies: PageInfoCalculator::Dependencies.new(
|
|
99
|
+
state: deps.state,
|
|
100
|
+
doc: deps.doc,
|
|
101
|
+
page_calculator: deps.page_calculator,
|
|
102
|
+
layout_service: deps.layout_service,
|
|
103
|
+
terminal_service: deps.terminal_service,
|
|
104
|
+
pagination_orchestrator: @orchestrator
|
|
105
|
+
),
|
|
106
|
+
defer_page_map: defer_page_map?
|
|
107
|
+
)
|
|
108
|
+
calculator.calculate
|
|
109
|
+
rescue StandardError
|
|
110
|
+
{ type: :single, current: 0, total: 0 }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
attr_reader :deps
|
|
116
|
+
|
|
117
|
+
def render_callback
|
|
118
|
+
deps.render_callback
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def background_worker
|
|
122
|
+
provider = deps.background_worker_provider
|
|
123
|
+
provider&.call
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def terminal_dimensions
|
|
127
|
+
height, width = deps.terminal_service.size
|
|
128
|
+
[width, height]
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def session(dimensions: nil)
|
|
132
|
+
@orchestrator.session(
|
|
133
|
+
doc: deps.doc,
|
|
134
|
+
state: deps.state,
|
|
135
|
+
page_calculator: deps.page_calculator,
|
|
136
|
+
dimensions: dimensions
|
|
137
|
+
)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def perform_initial_calculations_with_progress
|
|
141
|
+
return unless deps.doc
|
|
142
|
+
|
|
143
|
+
session = session(dimensions: terminal_dimensions)
|
|
144
|
+
return unless session
|
|
145
|
+
|
|
146
|
+
session.initial_build
|
|
147
|
+
render_callback&.call
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def build_page_map_in_background
|
|
151
|
+
session(dimensions: terminal_dimensions)&.build_full_map
|
|
152
|
+
@flags.defer_page_map = false
|
|
153
|
+
render_callback&.call
|
|
154
|
+
rescue StandardError
|
|
155
|
+
@flags.defer_page_map = false
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def submit_background_job(&)
|
|
159
|
+
worker = background_worker
|
|
160
|
+
if worker
|
|
161
|
+
worker.submit(&)
|
|
162
|
+
else
|
|
163
|
+
Thread.new do
|
|
164
|
+
yield
|
|
165
|
+
rescue StandardError
|
|
166
|
+
# ignore background failures
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def preloaded_page_data?
|
|
172
|
+
if Shoko::Application::Selectors::ConfigSelectors.page_numbering_mode(deps.state) == :dynamic
|
|
173
|
+
return deps.page_calculator&.total_pages&.positive?
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
deps.state.get(%i[reader total_pages]).to_i.positive?
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def seed_flags
|
|
180
|
+
return unless deps.doc.respond_to?(:cached?) && deps.doc.cached?
|
|
181
|
+
|
|
182
|
+
@flags.pending_initial_calculation = false
|
|
183
|
+
@flags.defer_page_map = true
|
|
184
|
+
return unless deps.page_calculator && deps.page_calculator.total_pages.to_i.positive?
|
|
185
|
+
|
|
186
|
+
@flags.defer_page_map = false
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def apply_invalidate_message(result)
|
|
190
|
+
return unless deps.ui_controller
|
|
191
|
+
|
|
192
|
+
case result
|
|
193
|
+
when :deleted
|
|
194
|
+
deps.ui_controller.set_message('Pagination cache cleared')
|
|
195
|
+
when :missing
|
|
196
|
+
deps.ui_controller.set_message('No pagination cache for this layout')
|
|
197
|
+
else
|
|
198
|
+
deps.ui_controller.set_message('Failed to clear pagination cache')
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../pagination'
|
|
4
|
+
require_relative '../../../adapters/output/kitty/kitty_graphics.rb'
|
|
5
|
+
|
|
6
|
+
module Shoko::Core::Services::Pagination
|
|
7
|
+
# Handles pagination builds (dynamic/absolute) and progress overlay.
|
|
8
|
+
# Keeps heavy orchestration out of ReaderController while preserving behavior.
|
|
9
|
+
class PaginationOrchestrator
|
|
10
|
+
# Factory for selecting per-mode pagination strategies.
|
|
11
|
+
module StrategyFactory
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def select(session)
|
|
15
|
+
mode = Shoko::Application::Selectors::ConfigSelectors.page_numbering_mode(session.state)
|
|
16
|
+
mode == :dynamic ? DynamicStrategy : AbsoluteStrategy
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Base strategy type for pagination operations.
|
|
21
|
+
class Strategy
|
|
22
|
+
def initialize(session)
|
|
23
|
+
@session = session
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
attr_reader :session
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Dynamic pagination behavior.
|
|
32
|
+
class DynamicStrategy < Strategy
|
|
33
|
+
def build_full_map(progress: nil)
|
|
34
|
+
session.build_dynamic_map(progress: progress)
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def build_initial_map(progress:)
|
|
39
|
+
build_full_map(progress: progress)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def refresh_after_resize
|
|
43
|
+
session.build_dynamic_map
|
|
44
|
+
session.clamp_dynamic_index!
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def rebuild_after_config_change
|
|
48
|
+
payload = session.pending_progress_payload
|
|
49
|
+
session.state.dispatch(Shoko::Application::Actions::UpdateSelectionsAction.new(pending_progress: payload))
|
|
50
|
+
session.build_dynamic_map
|
|
51
|
+
session.clamp_dynamic_index!
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def rebuild_dynamic(progress:)
|
|
55
|
+
payload = session.pending_progress_payload
|
|
56
|
+
session.with_loading('Rebuilding pagination…') do
|
|
57
|
+
session.state.dispatch(Shoko::Application::Actions::UpdateSelectionsAction.new(pending_progress: payload))
|
|
58
|
+
session.build_dynamic_map(progress: progress)
|
|
59
|
+
end
|
|
60
|
+
:handled
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Absolute pagination behavior.
|
|
65
|
+
class AbsoluteStrategy < Strategy
|
|
66
|
+
def build_full_map(progress: nil)
|
|
67
|
+
session.build_absolute_map(progress: progress)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def build_initial_map(progress:)
|
|
71
|
+
build_full_map(progress: progress)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def refresh_after_resize
|
|
75
|
+
session.build_absolute_map
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def rebuild_after_config_change
|
|
79
|
+
session.build_absolute_map
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def rebuild_dynamic(progress:)
|
|
83
|
+
:pass
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Aggregates pagination inputs and exposes a per-document session API.
|
|
88
|
+
class PaginationSession
|
|
89
|
+
attr_reader :doc, :state, :page_calculator, :dimensions
|
|
90
|
+
|
|
91
|
+
def initialize(doc:, state:, page_calculator:, dimensions:, pagination_cache:, frame_coordinator:)
|
|
92
|
+
@doc = doc
|
|
93
|
+
@state = state
|
|
94
|
+
@page_calculator = page_calculator
|
|
95
|
+
@dimensions = dimensions
|
|
96
|
+
@pagination_cache = pagination_cache
|
|
97
|
+
@frame_coordinator = frame_coordinator
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def build_full_map!(progress: nil, &block)
|
|
101
|
+
progress ||= block
|
|
102
|
+
strategy.build_full_map(progress: progress)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def build_full_map(progress: nil, &)
|
|
106
|
+
build_full_map!(progress: progress, &)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def refresh_after_resize
|
|
110
|
+
strategy.refresh_after_resize
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def rebuild_after_config_change
|
|
114
|
+
strategy.rebuild_after_config_change
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Performs the initial pagination calculation with a loading overlay.
|
|
118
|
+
# Returns a hash with optional :page_map_cache for absolute mode.
|
|
119
|
+
def initial_build
|
|
120
|
+
cache = nil
|
|
121
|
+
with_loading('Calculating pages...') do
|
|
122
|
+
map = strategy.build_initial_map(progress: progress_callback)
|
|
123
|
+
cache = map ? build_absolute_cache_entry(map) : nil
|
|
124
|
+
end
|
|
125
|
+
{ page_map_cache: cache }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Rebuilds dynamic pagination with a loading overlay and precise restore.
|
|
129
|
+
def rebuild_dynamic
|
|
130
|
+
strategy.rebuild_dynamic(progress: progress_callback)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Remove the cached pagination entry for the supplied dimensions.
|
|
134
|
+
#
|
|
135
|
+
# @return [Symbol] :deleted when cache entry removed, :missing when no entry existed,
|
|
136
|
+
# :error when removal fails.
|
|
137
|
+
def invalidate_cache
|
|
138
|
+
return :missing unless doc && @pagination_cache
|
|
139
|
+
|
|
140
|
+
key = @pagination_cache.layout_key(width, height, view_mode, line_spacing, kitty_images: kitty_images?)
|
|
141
|
+
return :missing unless key && @pagination_cache.exists_for_document?(doc, key)
|
|
142
|
+
|
|
143
|
+
@pagination_cache.delete_for_document(doc, key)
|
|
144
|
+
:deleted
|
|
145
|
+
rescue StandardError
|
|
146
|
+
:error
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def width
|
|
150
|
+
dimensions[0]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def height
|
|
154
|
+
dimensions[1]
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def view_mode
|
|
158
|
+
Shoko::Application::Selectors::ConfigSelectors.view_mode(state)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def line_spacing
|
|
162
|
+
Shoko::Application::Selectors::ConfigSelectors.line_spacing(state)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def kitty_images?
|
|
166
|
+
Shoko::Adapters::Output::Kitty::KittyGraphics.enabled_for?(state)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def pending_progress_payload
|
|
170
|
+
current_chapter = state.get(%i[reader current_chapter]) || 0
|
|
171
|
+
current_index = state.get(%i[reader current_page_index]).to_i
|
|
172
|
+
page = page_calculator.get_page(current_index)
|
|
173
|
+
{
|
|
174
|
+
chapter_index: current_chapter,
|
|
175
|
+
line_offset: page ? page[:start_line] : 0,
|
|
176
|
+
}
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def build_dynamic_map(progress: nil)
|
|
180
|
+
Shoko::Adapters::Monitoring::PerfTracer.measure('pagination.build') do
|
|
181
|
+
page_calculator.build_dynamic_map!(width, height, doc, state) do |done, total|
|
|
182
|
+
progress&.call(done, total)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
page_calculator.apply_pending_precise_restore!(state)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def build_absolute_map(progress: nil)
|
|
189
|
+
Shoko::Adapters::Monitoring::PerfTracer.measure('pagination.build') do
|
|
190
|
+
page_calculator.build_absolute_map!(width, height, doc, state) do |done, total|
|
|
191
|
+
progress&.call(done, total)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def clamp_dynamic_index!
|
|
197
|
+
total = page_calculator.total_pages.to_i
|
|
198
|
+
return if total <= 0
|
|
199
|
+
|
|
200
|
+
current = state.get(%i[reader current_page_index]).to_i
|
|
201
|
+
clamped = current.clamp(0, total - 1)
|
|
202
|
+
state.dispatch(
|
|
203
|
+
Shoko::Application::Actions::UpdatePageAction.new(current_page_index: clamped)
|
|
204
|
+
)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def progress_callback
|
|
208
|
+
->(done, total) { update_progress(done, total) }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def with_loading(message)
|
|
212
|
+
begin_loading(message)
|
|
213
|
+
yield
|
|
214
|
+
ensure
|
|
215
|
+
end_loading
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def begin_loading(message)
|
|
219
|
+
state.dispatch(Shoko::Application::Actions::UpdateUILoadingAction.new(
|
|
220
|
+
loading_active: true,
|
|
221
|
+
loading_message: message,
|
|
222
|
+
loading_progress: 0.0
|
|
223
|
+
))
|
|
224
|
+
@frame_coordinator&.render_loading_overlay
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def end_loading
|
|
228
|
+
state.dispatch(Shoko::Application::Actions::UpdateUILoadingAction.new(
|
|
229
|
+
loading_active: false,
|
|
230
|
+
loading_message: nil
|
|
231
|
+
))
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def update_progress(done, total)
|
|
235
|
+
progress = Shoko::Core::Services::ProgressHelper.ratio(done, total)
|
|
236
|
+
state.dispatch(Shoko::Application::Actions::UpdateUILoadingAction.new(
|
|
237
|
+
loading_progress: progress
|
|
238
|
+
))
|
|
239
|
+
@frame_coordinator&.render_loading_overlay
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def build_absolute_cache_entry(page_map)
|
|
243
|
+
key = @pagination_cache&.layout_key(
|
|
244
|
+
width,
|
|
245
|
+
height,
|
|
246
|
+
view_mode,
|
|
247
|
+
line_spacing,
|
|
248
|
+
kitty_images: kitty_images?
|
|
249
|
+
)
|
|
250
|
+
{
|
|
251
|
+
key: key,
|
|
252
|
+
map: page_map,
|
|
253
|
+
total: Array(page_map).sum,
|
|
254
|
+
}
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
private
|
|
258
|
+
|
|
259
|
+
def strategy
|
|
260
|
+
@strategy ||= StrategyFactory.select(self).new(self)
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def initialize(terminal_service:, pagination_cache: nil, frame_coordinator: nil)
|
|
265
|
+
@terminal_service = terminal_service
|
|
266
|
+
@pagination_cache = pagination_cache
|
|
267
|
+
@frame_coordinator = frame_coordinator
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def session(doc:, state:, page_calculator:, dimensions: nil)
|
|
271
|
+
return nil unless doc && page_calculator
|
|
272
|
+
|
|
273
|
+
dims = dimensions || terminal_dimensions
|
|
274
|
+
PaginationSession.new(
|
|
275
|
+
doc: doc,
|
|
276
|
+
state: state,
|
|
277
|
+
page_calculator: page_calculator,
|
|
278
|
+
dimensions: dims,
|
|
279
|
+
pagination_cache: @pagination_cache,
|
|
280
|
+
frame_coordinator: @frame_coordinator
|
|
281
|
+
)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
private
|
|
285
|
+
|
|
286
|
+
def terminal_dimensions
|
|
287
|
+
height, width = @terminal_service.size
|
|
288
|
+
[width, height]
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shoko
|
|
4
|
+
module Core
|
|
5
|
+
module Services
|
|
6
|
+
# Utility helpers for normalizing progress metrics across menu and reader flows.
|
|
7
|
+
module ProgressHelper
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# Normalize partial progress against a total, guarding against zero denominators.
|
|
11
|
+
#
|
|
12
|
+
# @param done [Numeric]
|
|
13
|
+
# @param total [Numeric]
|
|
14
|
+
# @return [Float]
|
|
15
|
+
def ratio(done, total)
|
|
16
|
+
denom = [total.to_f, 1.0].max
|
|
17
|
+
done.to_f / denom
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|