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,217 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_component'
|
|
4
|
+
require_relative 'surface'
|
|
5
|
+
require_relative 'rect'
|
|
6
|
+
require_relative 'sidebar/tab_header_component'
|
|
7
|
+
require_relative 'sidebar/toc_tab_renderer'
|
|
8
|
+
require_relative 'sidebar/annotations_tab_renderer'
|
|
9
|
+
require_relative 'sidebar/bookmarks_tab_renderer'
|
|
10
|
+
require_relative 'ui/text_utils'
|
|
11
|
+
|
|
12
|
+
module Shoko
|
|
13
|
+
module Adapters::Output::Ui::Components
|
|
14
|
+
# Collapsible sidebar panel with tabbed interface for TOC, Annotations, and Bookmarks
|
|
15
|
+
class SidebarPanelComponent < BaseComponent
|
|
16
|
+
include Adapters::Output::Ui::Constants::UI
|
|
17
|
+
|
|
18
|
+
TABS = %i[toc annotations bookmarks].freeze
|
|
19
|
+
TAB_TITLES = { toc: 'Contents', annotations: 'Annotations', bookmarks: 'Bookmarks' }.freeze
|
|
20
|
+
TAB_KEYS = { toc: 'T', annotations: 'A', bookmarks: 'B' }.freeze
|
|
21
|
+
HELP_TEXTS = {
|
|
22
|
+
toc: '↑↓ Navigate • ⏎ Jump • / Filter',
|
|
23
|
+
annotations: '↑↓ Navigate • ⏎ Jump • e Edit • d Delete',
|
|
24
|
+
bookmarks: '↑↓ Navigate • ⏎ Jump • d Delete',
|
|
25
|
+
}.freeze
|
|
26
|
+
DEFAULT_WIDTH_PERCENT = 30
|
|
27
|
+
MIN_WIDTH = 24
|
|
28
|
+
HEADER_HEIGHT = 2
|
|
29
|
+
TAB_HEIGHT = 3
|
|
30
|
+
HELP_HEIGHT = 1
|
|
31
|
+
|
|
32
|
+
def initialize(state, dependencies)
|
|
33
|
+
super() # Call BaseComponent constructor
|
|
34
|
+
@state = state
|
|
35
|
+
@dependencies = dependencies
|
|
36
|
+
@tab_header = Sidebar::TabHeaderComponent.new(state)
|
|
37
|
+
@toc_renderer = Sidebar::TocTabRenderer.new(state, dependencies)
|
|
38
|
+
@annotations_renderer = Sidebar::AnnotationsTabRenderer.new(state)
|
|
39
|
+
@bookmarks_renderer = Sidebar::BookmarksTabRenderer.new(state, dependencies)
|
|
40
|
+
|
|
41
|
+
# Observe sidebar state changes
|
|
42
|
+
state.add_observer(self,
|
|
43
|
+
%i[reader sidebar_visible],
|
|
44
|
+
%i[reader sidebar_active_tab],
|
|
45
|
+
%i[reader sidebar_toc_selected],
|
|
46
|
+
%i[reader sidebar_toc_collapsed],
|
|
47
|
+
%i[reader sidebar_annotations_selected],
|
|
48
|
+
%i[reader sidebar_bookmarks_selected])
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def preferred_width(total_width)
|
|
52
|
+
state = @state
|
|
53
|
+
return :hidden unless state.get(%i[reader sidebar_visible])
|
|
54
|
+
|
|
55
|
+
# Calculate width as percentage of total, with minimum
|
|
56
|
+
preferred = (total_width * DEFAULT_WIDTH_PERCENT / 100.0).round
|
|
57
|
+
[preferred, MIN_WIDTH].max
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def do_render(surface, bounds)
|
|
61
|
+
state = @state
|
|
62
|
+
bw = bounds.width
|
|
63
|
+
bh = bounds.height
|
|
64
|
+
return unless state.get(%i[reader sidebar_visible]) && bw >= MIN_WIDTH
|
|
65
|
+
|
|
66
|
+
# Cache frequently-used bounds values
|
|
67
|
+
bx = bounds.x
|
|
68
|
+
by = bounds.y
|
|
69
|
+
# bw, bh already cached above
|
|
70
|
+
|
|
71
|
+
# Draw modern border
|
|
72
|
+
draw_border(surface, bounds)
|
|
73
|
+
|
|
74
|
+
content_bounds = content_bounds_for(bounds)
|
|
75
|
+
return unless content_bounds
|
|
76
|
+
|
|
77
|
+
# Render minimal header with title only
|
|
78
|
+
header_bounds = Rect.new(x: bx, y: by, width: bw,
|
|
79
|
+
height: HEADER_HEIGHT)
|
|
80
|
+
render_header(surface, header_bounds)
|
|
81
|
+
|
|
82
|
+
# Render active tab content
|
|
83
|
+
render_active_tab(surface, content_bounds)
|
|
84
|
+
|
|
85
|
+
# Render help text
|
|
86
|
+
help_bounds = Rect.new(
|
|
87
|
+
x: bx,
|
|
88
|
+
y: content_bounds.y + content_bounds.height,
|
|
89
|
+
width: bw,
|
|
90
|
+
height: HELP_HEIGHT
|
|
91
|
+
)
|
|
92
|
+
render_help(surface, help_bounds)
|
|
93
|
+
|
|
94
|
+
# Render tab navigation at bottom
|
|
95
|
+
tab_bounds = Rect.new(
|
|
96
|
+
x: bx,
|
|
97
|
+
y: by + bh - TAB_HEIGHT,
|
|
98
|
+
width: bw,
|
|
99
|
+
height: TAB_HEIGHT
|
|
100
|
+
)
|
|
101
|
+
@tab_header.render(surface, tab_bounds)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def sidebar_bounds_for(total_width, total_height)
|
|
105
|
+
return nil unless @state.get(%i[reader sidebar_visible])
|
|
106
|
+
|
|
107
|
+
width = preferred_width(total_width)
|
|
108
|
+
return nil unless width.is_a?(Integer) && width.positive?
|
|
109
|
+
|
|
110
|
+
width = [width, total_width].min
|
|
111
|
+
Rect.new(x: 1, y: 1, width: width, height: total_height)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def tab_for_point(col, row, sidebar_bounds)
|
|
115
|
+
return nil unless sidebar_bounds
|
|
116
|
+
|
|
117
|
+
tab_bounds = tab_bounds_for(sidebar_bounds)
|
|
118
|
+
@tab_header.tab_for_point(tab_bounds, col, row)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def toc_entry_at(col, row, sidebar_bounds)
|
|
122
|
+
return nil unless sidebar_bounds
|
|
123
|
+
|
|
124
|
+
active_tab = Shoko::Application::Selectors::ReaderSelectors.sidebar_active_tab(@state)
|
|
125
|
+
return nil unless active_tab == :toc
|
|
126
|
+
|
|
127
|
+
content_bounds = content_bounds_for(sidebar_bounds)
|
|
128
|
+
return nil unless content_bounds
|
|
129
|
+
|
|
130
|
+
@toc_renderer.entry_at(content_bounds, col, row)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def toc_scroll_metrics(sidebar_bounds)
|
|
134
|
+
return nil unless sidebar_bounds
|
|
135
|
+
|
|
136
|
+
active_tab = Shoko::Application::Selectors::ReaderSelectors.sidebar_active_tab(@state)
|
|
137
|
+
return nil unless active_tab == :toc
|
|
138
|
+
|
|
139
|
+
content_bounds = content_bounds_for(sidebar_bounds)
|
|
140
|
+
return nil unless content_bounds
|
|
141
|
+
|
|
142
|
+
@toc_renderer.scroll_metrics(content_bounds)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
def draw_border(surface, bounds)
|
|
148
|
+
# Draw modern vertical border on the right edge
|
|
149
|
+
h = bounds.height
|
|
150
|
+
w = bounds.width
|
|
151
|
+
reset = Terminal::ANSI::RESET
|
|
152
|
+
dim = COLOR_TEXT_DIM
|
|
153
|
+
(1..h).each do |y|
|
|
154
|
+
surface.write(bounds, y, w, "#{dim}│#{reset}")
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def render_header(surface, bounds)
|
|
159
|
+
state = @state
|
|
160
|
+
|
|
161
|
+
# Simple clean title
|
|
162
|
+
active_tab = Shoko::Application::Selectors::ReaderSelectors.sidebar_active_tab(state)
|
|
163
|
+
title = TAB_TITLES[active_tab] || 'Sidebar'
|
|
164
|
+
reset = Terminal::ANSI::RESET
|
|
165
|
+
surface.write(bounds, 1, 2, "#{SELECTION_HIGHLIGHT}#{title}#{reset}")
|
|
166
|
+
|
|
167
|
+
# Close indicator
|
|
168
|
+
w = bounds.width
|
|
169
|
+
key = TAB_KEYS[active_tab] || 'T'
|
|
170
|
+
close_text = "#{COLOR_TEXT_DIM}[#{key}]#{reset}"
|
|
171
|
+
surface.write(bounds, 1, w - 5, close_text)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def render_help(surface, bounds)
|
|
175
|
+
state = @state
|
|
176
|
+
|
|
177
|
+
active_tab = Shoko::Application::Selectors::ReaderSelectors.sidebar_active_tab(state)
|
|
178
|
+
reset = Terminal::ANSI::RESET
|
|
179
|
+
width = bounds.width
|
|
180
|
+
hint = HELP_TEXTS[active_tab]
|
|
181
|
+
return unless hint
|
|
182
|
+
|
|
183
|
+
max_hint_width = [width - 4, 1].max
|
|
184
|
+
clipped_hint = UI::TextUtils.truncate_text(hint, max_hint_width)
|
|
185
|
+
surface.write(bounds, 1, 2, "#{COLOR_TEXT_DIM}#{clipped_hint}#{reset}")
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def render_active_tab(surface, bounds)
|
|
189
|
+
active_tab = Shoko::Application::Selectors::ReaderSelectors.sidebar_active_tab(@state)
|
|
190
|
+
renderer = { toc: @toc_renderer, annotations: @annotations_renderer,
|
|
191
|
+
bookmarks: @bookmarks_renderer }[active_tab]
|
|
192
|
+
renderer&.render(surface, bounds)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def content_bounds_for(bounds)
|
|
196
|
+
content_height = bounds.height - HEADER_HEIGHT - TAB_HEIGHT - HELP_HEIGHT
|
|
197
|
+
return nil if content_height <= 0
|
|
198
|
+
|
|
199
|
+
Rect.new(
|
|
200
|
+
x: bounds.x,
|
|
201
|
+
y: bounds.y + HEADER_HEIGHT,
|
|
202
|
+
width: bounds.width,
|
|
203
|
+
height: content_height
|
|
204
|
+
)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def tab_bounds_for(sidebar_bounds)
|
|
208
|
+
Rect.new(
|
|
209
|
+
x: sidebar_bounds.x,
|
|
210
|
+
y: sidebar_bounds.y + sidebar_bounds.height - TAB_HEIGHT,
|
|
211
|
+
width: sidebar_bounds.width,
|
|
212
|
+
height: TAB_HEIGHT
|
|
213
|
+
)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../terminal/terminal.rb'
|
|
4
|
+
require_relative '../../terminal/text_metrics.rb'
|
|
5
|
+
|
|
6
|
+
module Shoko
|
|
7
|
+
module Adapters::Output::Ui::Components
|
|
8
|
+
# Terminal wrapper that applies bounds and basic clipping
|
|
9
|
+
class Surface
|
|
10
|
+
def initialize(output = Terminal)
|
|
11
|
+
@output = output
|
|
12
|
+
@style_stack = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Write text at local (row, col) relative to bounds
|
|
16
|
+
# Applies basic clipping to the provided bounds
|
|
17
|
+
def write(bounds, row, col, text)
|
|
18
|
+
b_height = bounds.height
|
|
19
|
+
b_width = bounds.width
|
|
20
|
+
return if b_height <= 0 || b_width <= 0
|
|
21
|
+
|
|
22
|
+
b_y = bounds.y
|
|
23
|
+
b_x = bounds.x
|
|
24
|
+
b_right = bounds.right
|
|
25
|
+
b_bottom = bounds.bottom
|
|
26
|
+
|
|
27
|
+
abs_row = b_y + row - 1
|
|
28
|
+
abs_col = b_x + col - 1
|
|
29
|
+
|
|
30
|
+
return if abs_row < b_y || abs_row > b_bottom
|
|
31
|
+
return if abs_col < b_x || abs_col > b_right
|
|
32
|
+
|
|
33
|
+
max_width = b_right - abs_col + 1
|
|
34
|
+
clipped = Shoko::Adapters::Output::Terminal::TextMetrics.truncate_to(
|
|
35
|
+
text.to_s,
|
|
36
|
+
max_width,
|
|
37
|
+
start_column: [abs_col - 1, 0].max
|
|
38
|
+
)
|
|
39
|
+
clipped = apply_dim(clipped) if dimmed?
|
|
40
|
+
return if clipped.nil? || clipped.empty?
|
|
41
|
+
|
|
42
|
+
@output.write(abs_row, abs_col, clipped)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Write using absolute terminal coordinates while still clipping to bounds.
|
|
46
|
+
#
|
|
47
|
+
# This is intended for overlay components that operate in absolute
|
|
48
|
+
# coordinates (e.g., mouse hit regions, selection geometry).
|
|
49
|
+
def write_abs(bounds, abs_row, abs_col, text)
|
|
50
|
+
local_row = abs_row.to_i - bounds.y + 1
|
|
51
|
+
local_col = abs_col.to_i - bounds.x + 1
|
|
52
|
+
write(bounds, local_row, local_col, text)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Convenience to fill an area with a character
|
|
56
|
+
def fill(bounds, char)
|
|
57
|
+
w = bounds.width
|
|
58
|
+
h = bounds.height
|
|
59
|
+
line = char.to_s * w
|
|
60
|
+
(0...h).each do |r|
|
|
61
|
+
write(bounds, r + 1, 1, line)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def with_dimmed
|
|
66
|
+
@style_stack << :dim
|
|
67
|
+
yield
|
|
68
|
+
ensure
|
|
69
|
+
@style_stack.pop
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def dimmed?
|
|
75
|
+
@style_stack.include?(:dim)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def apply_dim(text)
|
|
79
|
+
dim = Terminal::ANSI::DIM
|
|
80
|
+
reset = Terminal::ANSI::RESET
|
|
81
|
+
return text if text.empty?
|
|
82
|
+
|
|
83
|
+
transformed = text.gsub(reset, "#{reset}#{dim}")
|
|
84
|
+
"#{dim}#{transformed}#{reset}"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_component'
|
|
4
|
+
require_relative '../../terminal/text_metrics.rb'
|
|
5
|
+
require_relative '../../../../core/models/selection_anchor.rb'
|
|
6
|
+
module Shoko
|
|
7
|
+
module Adapters::Output::Ui::Components
|
|
8
|
+
# Unified overlay component that handles all tooltip/popup rendering
|
|
9
|
+
# including text selection highlighting, popup menus, and annotations.
|
|
10
|
+
#
|
|
11
|
+
# This component consolidates the scattered rendering logic and provides
|
|
12
|
+
# consistent coordinate handling for the fragile tooltip system.
|
|
13
|
+
class TooltipOverlayComponent < BaseComponent
|
|
14
|
+
include Adapters::Output::Ui::Constants::UI
|
|
15
|
+
|
|
16
|
+
def initialize(controller, coordinate_service:)
|
|
17
|
+
super()
|
|
18
|
+
@controller = controller
|
|
19
|
+
@coordinate_service = coordinate_service
|
|
20
|
+
@last_selection_segments = []
|
|
21
|
+
@geometry_cache_key = nil
|
|
22
|
+
@geometry_cache = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Render all overlay elements: highlights, popups, tooltips
|
|
26
|
+
def do_render(surface, bounds)
|
|
27
|
+
# Render in specific order to ensure proper layering
|
|
28
|
+
clear_previous_selection_artifacts(surface, bounds)
|
|
29
|
+
render_saved_annotations(surface, bounds)
|
|
30
|
+
render_active_selection(surface, bounds)
|
|
31
|
+
render_popup_menu(surface, bounds)
|
|
32
|
+
render_annotations_overlay(surface, bounds)
|
|
33
|
+
render_annotation_editor_overlay(surface, bounds)
|
|
34
|
+
render_toast_notification(surface, bounds)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def render_saved_annotations(surface, bounds)
|
|
40
|
+
state = @controller.state
|
|
41
|
+
anns = state.get(%i[reader annotations])
|
|
42
|
+
return unless anns
|
|
43
|
+
|
|
44
|
+
current_ch = state.get(%i[reader current_chapter])
|
|
45
|
+
chapter_annotations = anns.select { |annotation| annotation['chapter_index'] == current_ch }
|
|
46
|
+
chapter_annotations.each do |annotation|
|
|
47
|
+
render_text_highlight(surface, bounds, annotation['range'], HIGHLIGHT_BG_SAVED)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def render_active_selection(surface, bounds)
|
|
52
|
+
# Render current selection highlight
|
|
53
|
+
selection_range = @controller.state.get(%i[reader selection])
|
|
54
|
+
|
|
55
|
+
unless selection_range
|
|
56
|
+
# No active selection; keep any previously rendered segments for one clear pass
|
|
57
|
+
@pending_clear = true if @last_selection_segments.any?
|
|
58
|
+
return
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Reset tracking for this frame
|
|
62
|
+
@last_selection_segments.clear
|
|
63
|
+
render_text_highlight(surface, bounds, selection_range, HIGHLIGHT_BG_ACTIVE)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def render_popup_menu(surface, bounds)
|
|
67
|
+
popup_menu = @controller.state.get(%i[reader popup_menu])
|
|
68
|
+
return unless popup_menu&.visible
|
|
69
|
+
|
|
70
|
+
# Unified component rendering path
|
|
71
|
+
popup_menu.render(surface, bounds)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def render_annotations_overlay(surface, bounds)
|
|
75
|
+
overlay = @controller.state.get(%i[reader annotations_overlay])
|
|
76
|
+
return unless overlay.respond_to?(:visible?) && overlay.visible?
|
|
77
|
+
|
|
78
|
+
overlay.render(surface, bounds)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def render_annotation_editor_overlay(surface, bounds)
|
|
82
|
+
overlay = @controller.state.get(%i[reader annotation_editor_overlay])
|
|
83
|
+
return unless overlay.respond_to?(:visible?) && overlay.visible?
|
|
84
|
+
|
|
85
|
+
overlay.render(surface, bounds)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def render_toast_notification(surface, bounds)
|
|
89
|
+
message = Application::Selectors::ReaderSelectors.message(@controller.state)
|
|
90
|
+
message = message.to_s
|
|
91
|
+
return if message.empty?
|
|
92
|
+
|
|
93
|
+
ui = Adapters::Output::Ui::Constants::UI
|
|
94
|
+
width = bounds.width
|
|
95
|
+
max_width = [width - 2, 1].max
|
|
96
|
+
label_max = [max_width - 1, 1].max
|
|
97
|
+
label = " #{message} "
|
|
98
|
+
label = Shoko::Adapters::Output::Terminal::TextMetrics.truncate_to(label, label_max)
|
|
99
|
+
content = "|#{label}"
|
|
100
|
+
col = [width - Shoko::Adapters::Output::Terminal::TextMetrics.visible_length(content) + 1, 1].max
|
|
101
|
+
|
|
102
|
+
toast = "#{Terminal::ANSI::RESET}#{ui::TOAST_ACCENT}|#{ui::TOAST_FG}#{label}#{Terminal::ANSI::RESET}"
|
|
103
|
+
surface.write(bounds, 1, col, toast)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def render_text_highlight(surface, bounds, range, color)
|
|
107
|
+
rendered_lines = Application::Selectors::ReaderSelectors.rendered_lines(@controller.state)
|
|
108
|
+
return if rendered_lines.empty?
|
|
109
|
+
|
|
110
|
+
normalized_range = @coordinate_service.normalize_selection_range(range, rendered_lines)
|
|
111
|
+
return unless normalized_range
|
|
112
|
+
|
|
113
|
+
start_anchor = Shoko::Core::Models::SelectionAnchor.from(normalized_range[:start])
|
|
114
|
+
end_anchor = Shoko::Core::Models::SelectionAnchor.from(normalized_range[:end])
|
|
115
|
+
return unless start_anchor && end_anchor
|
|
116
|
+
|
|
117
|
+
cache = geometry_cache_for(rendered_lines)
|
|
118
|
+
ordered = cache[:ordered]
|
|
119
|
+
index_by_key = cache[:index_by_key]
|
|
120
|
+
return if ordered.empty?
|
|
121
|
+
|
|
122
|
+
start_idx = index_by_key[start_anchor.geometry_key]
|
|
123
|
+
end_idx = index_by_key[end_anchor.geometry_key]
|
|
124
|
+
return unless start_idx && end_idx
|
|
125
|
+
|
|
126
|
+
ordered[start_idx..end_idx].each do |geometry|
|
|
127
|
+
start_cell = geometry.key == start_anchor.geometry_key ? start_anchor.cell_index : 0
|
|
128
|
+
end_cell = geometry.key == end_anchor.geometry_key ? end_anchor.cell_index : geometry.cells.length
|
|
129
|
+
render_geometry_highlight(surface, bounds, geometry, start_cell, end_cell, color)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def render_geometry_highlight(surface, bounds, geometry, start_cell, end_cell, color)
|
|
134
|
+
return if end_cell <= start_cell
|
|
135
|
+
|
|
136
|
+
start_char = char_index_for_cell(geometry, start_cell)
|
|
137
|
+
end_char = char_index_for_cell(geometry, end_cell)
|
|
138
|
+
return if end_char <= start_char
|
|
139
|
+
|
|
140
|
+
segment_text = geometry.plain_text[start_char...end_char]
|
|
141
|
+
return if segment_text.nil? || segment_text.empty?
|
|
142
|
+
|
|
143
|
+
highlight = "#{color}#{COLOR_TEXT_PRIMARY}#{segment_text}#{Terminal::ANSI::RESET}"
|
|
144
|
+
start_col = screen_column_for_cell(geometry, start_cell)
|
|
145
|
+
surface.write_abs(bounds, geometry.row, start_col, highlight)
|
|
146
|
+
record_selection_segment(geometry.row, start_col, segment_text)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def screen_column_for_cell(geometry, cell_index)
|
|
150
|
+
if cell_index <= 0
|
|
151
|
+
geometry.column_origin
|
|
152
|
+
elsif cell_index >= geometry.cells.length
|
|
153
|
+
geometry.column_origin + geometry.visible_width
|
|
154
|
+
else
|
|
155
|
+
geometry.column_origin + geometry.cells[cell_index].screen_x
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def char_index_for_cell(geometry, cell_index)
|
|
160
|
+
cells = geometry.cells
|
|
161
|
+
return 0 if cells.empty?
|
|
162
|
+
|
|
163
|
+
if cell_index <= 0
|
|
164
|
+
0
|
|
165
|
+
elsif cell_index >= cells.length
|
|
166
|
+
geometry.plain_text.length
|
|
167
|
+
else
|
|
168
|
+
cells[cell_index].char_start
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def geometry_cache_for(rendered_lines)
|
|
173
|
+
cache_key = rendered_lines.object_id
|
|
174
|
+
return @geometry_cache if @geometry_cache_key == cache_key && @geometry_cache
|
|
175
|
+
|
|
176
|
+
geometry_by_key = {}
|
|
177
|
+
rendered_lines.each do |key, info|
|
|
178
|
+
geometry = info[:geometry]
|
|
179
|
+
next unless geometry
|
|
180
|
+
|
|
181
|
+
geometry_by_key[key] = geometry
|
|
182
|
+
end
|
|
183
|
+
ordered = order_geometry(geometry_by_key.values)
|
|
184
|
+
index_by_key = {}
|
|
185
|
+
ordered.each_with_index { |geo, idx| index_by_key[geo.key] = idx }
|
|
186
|
+
|
|
187
|
+
@geometry_cache_key = cache_key
|
|
188
|
+
@geometry_cache = { ordered: ordered, index_by_key: index_by_key }
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def order_geometry(geometries)
|
|
192
|
+
geometries.sort_by do |geo|
|
|
193
|
+
[geo.page_id || 0, geo.line_offset || 0, geo.column_id || 0, geo.row || 0, geo.column_origin || 0]
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def record_selection_segment(row, col, text)
|
|
198
|
+
@last_selection_segments << {
|
|
199
|
+
row: row,
|
|
200
|
+
col: col,
|
|
201
|
+
text: text,
|
|
202
|
+
}
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# If selection was present on previous frame but not this one, explicitly repaint
|
|
206
|
+
# the previously highlighted character cells to clear any lingering background color
|
|
207
|
+
def clear_previous_selection_artifacts(surface, bounds)
|
|
208
|
+
return unless @pending_clear && @last_selection_segments.any?
|
|
209
|
+
|
|
210
|
+
@last_selection_segments.each do |seg|
|
|
211
|
+
safe_text = seg[:text] || ''
|
|
212
|
+
reset = Terminal::ANSI::RESET
|
|
213
|
+
repaint = "#{reset}#{COLOR_TEXT_PRIMARY}#{safe_text}#{reset}"
|
|
214
|
+
surface.write_abs(bounds, seg[:row], seg[:col], repaint)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
@last_selection_segments.clear
|
|
218
|
+
@pending_clear = false
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Column bounds and overlap checks are now handled by CoordinateService
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../../terminal/text_metrics.rb'
|
|
4
|
+
|
|
5
|
+
module Shoko
|
|
6
|
+
module Adapters::Output::Ui::Components
|
|
7
|
+
module UI
|
|
8
|
+
# Helper for drawing bordered boxes with optional labels.
|
|
9
|
+
module BoxDrawer
|
|
10
|
+
def draw_box(surface, bounds, row, col, height, width, label: nil)
|
|
11
|
+
# Top border
|
|
12
|
+
hline = '─' * (width - 2)
|
|
13
|
+
surface.write(bounds, row, col, "╭#{hline}╮")
|
|
14
|
+
if label && width > 4
|
|
15
|
+
label_text = "[ #{label} ]"
|
|
16
|
+
available = width - 3
|
|
17
|
+
clipped = Shoko::Adapters::Output::Terminal::TextMetrics.truncate_to(label_text, available, start_column: bounds.x + col)
|
|
18
|
+
surface.write(bounds, row, col + 2, clipped) unless clipped.empty?
|
|
19
|
+
end
|
|
20
|
+
# Sides
|
|
21
|
+
(1...(height - 1)).each do |index|
|
|
22
|
+
y_pos = row + index
|
|
23
|
+
surface.write(bounds, y_pos, col, '│')
|
|
24
|
+
surface.write(bounds, y_pos, col + width - 1, '│')
|
|
25
|
+
end
|
|
26
|
+
# Bottom
|
|
27
|
+
surface.write(bounds, row + height - 1, col, "╰#{hline}╯")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shoko
|
|
4
|
+
module Adapters::Output::Ui::Components
|
|
5
|
+
module UI
|
|
6
|
+
# Shared helpers for list-based components to keep pagination logic consistent.
|
|
7
|
+
module ListHelpers
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def visible_window(total_items, per_page, selected)
|
|
11
|
+
return [0, 0] if total_items <= 0 || per_page <= 0
|
|
12
|
+
|
|
13
|
+
clamped_selected = selected.clamp(0, total_items - 1)
|
|
14
|
+
start_index = if clamped_selected < per_page
|
|
15
|
+
0
|
|
16
|
+
else
|
|
17
|
+
clamped_selected - per_page + 1
|
|
18
|
+
end
|
|
19
|
+
max_start = [total_items - per_page, 0].max
|
|
20
|
+
start_index = [start_index, max_start].min
|
|
21
|
+
end_index = [start_index + per_page - 1, total_items - 1].min
|
|
22
|
+
[start_index, end_index]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def slice_visible(items, per_page, selected)
|
|
26
|
+
total = items.length
|
|
27
|
+
start_index, end_index = visible_window(total, per_page, selected)
|
|
28
|
+
[start_index, items[start_index..end_index] || []]
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../../terminal/terminal.rb'
|
|
4
|
+
|
|
5
|
+
module Shoko
|
|
6
|
+
module Adapters::Output::Ui::Components
|
|
7
|
+
module UI
|
|
8
|
+
# Calculates overlay dimensions based on viewport bounds.
|
|
9
|
+
class OverlaySizing
|
|
10
|
+
def initialize(width_ratio:, width_padding:, min_width:, height_ratio:, height_padding:, min_height:)
|
|
11
|
+
@width_ratio = width_ratio
|
|
12
|
+
@width_padding = width_padding
|
|
13
|
+
@min_width = min_width
|
|
14
|
+
@height_ratio = height_ratio
|
|
15
|
+
@height_padding = height_padding
|
|
16
|
+
@min_height = min_height
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def width_for(total_width)
|
|
20
|
+
clamp_dimension(total_width, ratio: @width_ratio, padding: @width_padding, min: @min_width)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def height_for(total_height)
|
|
24
|
+
clamp_dimension(total_height, ratio: @height_ratio, padding: @height_padding, min: @min_height)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def clamp_dimension(total, ratio:, padding:, min:)
|
|
30
|
+
base = [(total * ratio).floor, total - padding].min
|
|
31
|
+
upper = total - padding
|
|
32
|
+
lower = [min, upper].min
|
|
33
|
+
base.clamp(lower, upper)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Provides centered overlay placement and frame geometry helpers.
|
|
38
|
+
class OverlayLayout
|
|
39
|
+
attr_reader :origin_x, :origin_y, :width, :height
|
|
40
|
+
|
|
41
|
+
def initialize(origin_x:, origin_y:, width:, height:)
|
|
42
|
+
@origin_x = origin_x
|
|
43
|
+
@origin_y = origin_y
|
|
44
|
+
@width = width
|
|
45
|
+
@height = height
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.centered(bounds, width:, height:)
|
|
49
|
+
origin_x = [(bounds.width - width) / 2, 1].max + 1
|
|
50
|
+
origin_y = [(bounds.height - height) / 2, 1].max + 1
|
|
51
|
+
new(origin_x: origin_x, origin_y: origin_y, width: width, height: height)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def inner_x
|
|
55
|
+
origin_x + 1
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def inner_y
|
|
59
|
+
origin_y + 1
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def inner_width
|
|
63
|
+
width - 2
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def inner_height
|
|
67
|
+
height - 2
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def fill_background(surface, bounds, background:)
|
|
71
|
+
reset = Terminal::ANSI::RESET
|
|
72
|
+
height.times do |offset|
|
|
73
|
+
surface.write(bounds, origin_y + offset, origin_x, "#{background}#{' ' * width}#{reset}")
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|