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.
Files changed (294) hide show
  1. checksums.yaml +7 -0
  2. data/.bundle/config +4 -0
  3. data/.bundle/config.bak +3 -0
  4. data/.rspec_status +42 -0
  5. data/.rubocop.yml +124 -0
  6. data/Gemfile +19 -0
  7. data/LICENSE +21 -0
  8. data/README.md +82 -0
  9. data/Rakefile +29 -0
  10. data/bin/start +15 -0
  11. data/lib/shoko/adapters/book_sources/document_service.rb +201 -0
  12. data/lib/shoko/adapters/book_sources/download_service.rb +95 -0
  13. data/lib/shoko/adapters/book_sources/epub/epub_resource_loader.rb +137 -0
  14. data/lib/shoko/adapters/book_sources/epub/parsers/html_processor.rb +151 -0
  15. data/lib/shoko/adapters/book_sources/epub/parsers/metadata_extractor.rb +53 -0
  16. data/lib/shoko/adapters/book_sources/epub/parsers/opf/entry_reader.rb +77 -0
  17. data/lib/shoko/adapters/book_sources/epub/parsers/opf/metadata_extractor.rb +67 -0
  18. data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_context.rb +86 -0
  19. data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_document_index.rb +75 -0
  20. data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_document_scanner.rb +47 -0
  21. data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_extractor.rb +46 -0
  22. data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_label_resolver.rb +83 -0
  23. data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_list_item.rb +55 -0
  24. data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_result.rb +8 -0
  25. data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_selector.rb +100 -0
  26. data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_source_locator.rb +93 -0
  27. data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_traversal.rb +103 -0
  28. data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_walker.rb +56 -0
  29. data/lib/shoko/adapters/book_sources/epub/parsers/opf_processor.rb +102 -0
  30. data/lib/shoko/adapters/book_sources/epub/parsers/xhtml_content_parser.rb +661 -0
  31. data/lib/shoko/adapters/book_sources/epub/parsers/xml_text_normalizer.rb +41 -0
  32. data/lib/shoko/adapters/book_sources/epub_document.rb +253 -0
  33. data/lib/shoko/adapters/book_sources/epub_finder/directory_scanner.rb +134 -0
  34. data/lib/shoko/adapters/book_sources/epub_finder/scanner_context.rb +28 -0
  35. data/lib/shoko/adapters/book_sources/epub_finder.rb +161 -0
  36. data/lib/shoko/adapters/book_sources/epub_importer.rb +268 -0
  37. data/lib/shoko/adapters/book_sources/gutendex_client.rb +150 -0
  38. data/lib/shoko/adapters/book_sources/library_scanner.rb +93 -0
  39. data/lib/shoko/adapters/book_sources/source_fingerprint.rb +57 -0
  40. data/lib/shoko/adapters/input/annotations/mouse_handler.rb +84 -0
  41. data/lib/shoko/adapters/input/command_bridge.rb +148 -0
  42. data/lib/shoko/adapters/input/command_factory.rb +255 -0
  43. data/lib/shoko/adapters/input/commands.rb +60 -0
  44. data/lib/shoko/adapters/input/dispatcher.rb +69 -0
  45. data/lib/shoko/adapters/input/input_controller.rb +250 -0
  46. data/lib/shoko/adapters/input/key_definitions.rb +108 -0
  47. data/lib/shoko/adapters/input/validators/file_path_validator.rb +81 -0
  48. data/lib/shoko/adapters/input/validators/terminal_size_validator.rb +76 -0
  49. data/lib/shoko/adapters/monitoring/logger.rb +150 -0
  50. data/lib/shoko/adapters/monitoring/perf_tracer.rb +183 -0
  51. data/lib/shoko/adapters/monitoring/performance_monitor.rb +110 -0
  52. data/lib/shoko/adapters/output/clipboard/clipboard_service.rb +125 -0
  53. data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler/image_builder.rb +149 -0
  54. data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler/text_wrapper.rb +149 -0
  55. data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler/tokenizer.rb +91 -0
  56. data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler.rb +144 -0
  57. data/lib/shoko/adapters/output/formatting/formatting_service/plain_lines_builder.rb +54 -0
  58. data/lib/shoko/adapters/output/formatting/formatting_service.rb +247 -0
  59. data/lib/shoko/adapters/output/formatting/wrapping_service.rb +228 -0
  60. data/lib/shoko/adapters/output/instrumentation_service.rb +52 -0
  61. data/lib/shoko/adapters/output/kitty/image_transcoder.rb +71 -0
  62. data/lib/shoko/adapters/output/kitty/kitty_graphics.rb +114 -0
  63. data/lib/shoko/adapters/output/kitty/kitty_image_renderer.rb +239 -0
  64. data/lib/shoko/adapters/output/kitty/kitty_unicode_placeholders.rb +139 -0
  65. data/lib/shoko/adapters/output/kitty/kitty_unicode_placeholders_diacritic_codepoints.txt +26 -0
  66. data/lib/shoko/adapters/output/notification_service.rb +58 -0
  67. data/lib/shoko/adapters/output/render_registry.rb +45 -0
  68. data/lib/shoko/adapters/output/rendering/models/line_geometry.rb +60 -0
  69. data/lib/shoko/adapters/output/rendering/models/page_rendering_context.rb +22 -0
  70. data/lib/shoko/adapters/output/rendering/models/render_params.rb +28 -0
  71. data/lib/shoko/adapters/output/rendering/models/rendering_context.rb +58 -0
  72. data/lib/shoko/adapters/output/terminal/buffer.rb +275 -0
  73. data/lib/shoko/adapters/output/terminal/constants/terminal_defaults.rb +11 -0
  74. data/lib/shoko/adapters/output/terminal/input/decoder.rb +347 -0
  75. data/lib/shoko/adapters/output/terminal/input.rb +161 -0
  76. data/lib/shoko/adapters/output/terminal/output.rb +105 -0
  77. data/lib/shoko/adapters/output/terminal/terminal.rb +167 -0
  78. data/lib/shoko/adapters/output/terminal/terminal_sanitizer.rb +243 -0
  79. data/lib/shoko/adapters/output/terminal/terminal_service.rb +138 -0
  80. data/lib/shoko/adapters/output/terminal/text_metrics.rb +273 -0
  81. data/lib/shoko/adapters/output/ui/builders/page_setup_builder.rb +47 -0
  82. data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay/footer_renderer.rb +80 -0
  83. data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay/geometry.rb +61 -0
  84. data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay/note_renderer.rb +86 -0
  85. data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay_component.rb +234 -0
  86. data/lib/shoko/adapters/output/ui/components/annotations_overlay/list_renderer.rb +142 -0
  87. data/lib/shoko/adapters/output/ui/components/annotations_overlay_component.rb +185 -0
  88. data/lib/shoko/adapters/output/ui/components/base_component.rb +110 -0
  89. data/lib/shoko/adapters/output/ui/components/component_interface.rb +80 -0
  90. data/lib/shoko/adapters/output/ui/components/content_component.rb +61 -0
  91. data/lib/shoko/adapters/output/ui/components/enhanced_popup_menu.rb +191 -0
  92. data/lib/shoko/adapters/output/ui/components/footer_component.rb +120 -0
  93. data/lib/shoko/adapters/output/ui/components/header_component.rb +46 -0
  94. data/lib/shoko/adapters/output/ui/components/layouts/horizontal.rb +63 -0
  95. data/lib/shoko/adapters/output/ui/components/layouts/vertical.rb +73 -0
  96. data/lib/shoko/adapters/output/ui/components/main_menu_component.rb +103 -0
  97. data/lib/shoko/adapters/output/ui/components/reading/base_view_renderer.rb +199 -0
  98. data/lib/shoko/adapters/output/ui/components/reading/config_helpers.rb +42 -0
  99. data/lib/shoko/adapters/output/ui/components/reading/help_renderer.rb +62 -0
  100. data/lib/shoko/adapters/output/ui/components/reading/inline_segment_highlighter.rb +144 -0
  101. data/lib/shoko/adapters/output/ui/components/reading/kitty_image_line_renderer.rb +262 -0
  102. data/lib/shoko/adapters/output/ui/components/reading/line_content_composer.rb +114 -0
  103. data/lib/shoko/adapters/output/ui/components/reading/line_drawer.rb +87 -0
  104. data/lib/shoko/adapters/output/ui/components/reading/line_geometry_builder.rb +41 -0
  105. data/lib/shoko/adapters/output/ui/components/reading/rendered_lines_recorder.rb +64 -0
  106. data/lib/shoko/adapters/output/ui/components/reading/single_view_renderer.rb +156 -0
  107. data/lib/shoko/adapters/output/ui/components/reading/split_view_renderer.rb +221 -0
  108. data/lib/shoko/adapters/output/ui/components/reading/view_renderer_factory.rb +20 -0
  109. data/lib/shoko/adapters/output/ui/components/reading/wrapped_lines_fetcher.rb +139 -0
  110. data/lib/shoko/adapters/output/ui/components/rect.rb +15 -0
  111. data/lib/shoko/adapters/output/ui/components/render_style.rb +84 -0
  112. data/lib/shoko/adapters/output/ui/components/screen_component.rb +24 -0
  113. data/lib/shoko/adapters/output/ui/components/screens/annotation_detail_screen_component.rb +175 -0
  114. data/lib/shoko/adapters/output/ui/components/screens/annotation_edit_screen_component.rb +221 -0
  115. data/lib/shoko/adapters/output/ui/components/screens/annotation_editor_screen_component.rb +205 -0
  116. data/lib/shoko/adapters/output/ui/components/screens/annotation_rendering_helpers.rb +190 -0
  117. data/lib/shoko/adapters/output/ui/components/screens/annotations_screen_component.rb +266 -0
  118. data/lib/shoko/adapters/output/ui/components/screens/base_screen_component.rb +49 -0
  119. data/lib/shoko/adapters/output/ui/components/screens/browse_screen_component.rb +319 -0
  120. data/lib/shoko/adapters/output/ui/components/screens/download_books_screen_component.rb +340 -0
  121. data/lib/shoko/adapters/output/ui/components/screens/library_screen_component.rb +205 -0
  122. data/lib/shoko/adapters/output/ui/components/screens/loading_overlay_component.rb +49 -0
  123. data/lib/shoko/adapters/output/ui/components/screens/menu_screen_component.rb +107 -0
  124. data/lib/shoko/adapters/output/ui/components/screens/settings_screen_component.rb +238 -0
  125. data/lib/shoko/adapters/output/ui/components/sidebar/annotations_tab_renderer.rb +159 -0
  126. data/lib/shoko/adapters/output/ui/components/sidebar/bookmarks_tab_renderer.rb +139 -0
  127. data/lib/shoko/adapters/output/ui/components/sidebar/tab_header_component.rb +157 -0
  128. data/lib/shoko/adapters/output/ui/components/sidebar/toc_tab_renderer.rb +111 -0
  129. data/lib/shoko/adapters/output/ui/components/sidebar/toc_tab_support.rb +1606 -0
  130. data/lib/shoko/adapters/output/ui/components/sidebar_panel_component.rb +217 -0
  131. data/lib/shoko/adapters/output/ui/components/surface.rb +88 -0
  132. data/lib/shoko/adapters/output/ui/components/tooltip_overlay_component.rb +224 -0
  133. data/lib/shoko/adapters/output/ui/components/ui/box_drawer.rb +32 -0
  134. data/lib/shoko/adapters/output/ui/components/ui/list_helpers.rb +33 -0
  135. data/lib/shoko/adapters/output/ui/components/ui/overlay_layout.rb +79 -0
  136. data/lib/shoko/adapters/output/ui/components/ui/text_utils.rb +46 -0
  137. data/lib/shoko/adapters/output/ui/constants/highlighting.rb +21 -0
  138. data/lib/shoko/adapters/output/ui/constants/messages.rb +12 -0
  139. data/lib/shoko/adapters/output/ui/constants/themes.rb +79 -0
  140. data/lib/shoko/adapters/output/ui/constants/ui_constants.rb +85 -0
  141. data/lib/shoko/adapters/output/ui/rendering/frame_coordinator.rb +42 -0
  142. data/lib/shoko/adapters/output/ui/rendering/reader_render_coordinator.rb +169 -0
  143. data/lib/shoko/adapters/output/ui/rendering/render_pipeline.rb +55 -0
  144. data/lib/shoko/adapters/storage/atomic_file_writer.rb +43 -0
  145. data/lib/shoko/adapters/storage/background_worker.rb +66 -0
  146. data/lib/shoko/adapters/storage/book_cache_pipeline.rb +653 -0
  147. data/lib/shoko/adapters/storage/cache/epub/memory_cache.rb +99 -0
  148. data/lib/shoko/adapters/storage/cache/epub/persistence.rb +131 -0
  149. data/lib/shoko/adapters/storage/cache/epub/serializer/deserialize.rb +225 -0
  150. data/lib/shoko/adapters/storage/cache/epub/serializer/helpers.rb +63 -0
  151. data/lib/shoko/adapters/storage/cache/epub/serializer/serialize.rb +83 -0
  152. data/lib/shoko/adapters/storage/cache/epub/serializer.rb +5 -0
  153. data/lib/shoko/adapters/storage/cache/epub/source_reference.rb +58 -0
  154. data/lib/shoko/adapters/storage/cache_paths.rb +21 -0
  155. data/lib/shoko/adapters/storage/cache_pointer_manager.rb +60 -0
  156. data/lib/shoko/adapters/storage/config_paths.rb +30 -0
  157. data/lib/shoko/adapters/storage/epub_cache.rb +195 -0
  158. data/lib/shoko/adapters/storage/file_writer_service.rb +47 -0
  159. data/lib/shoko/adapters/storage/json_cache_store/chapters.rb +141 -0
  160. data/lib/shoko/adapters/storage/json_cache_store/layouts.rb +67 -0
  161. data/lib/shoko/adapters/storage/json_cache_store/manifest.rb +42 -0
  162. data/lib/shoko/adapters/storage/json_cache_store/payload_helpers.rb +113 -0
  163. data/lib/shoko/adapters/storage/json_cache_store/resources.rb +84 -0
  164. data/lib/shoko/adapters/storage/json_cache_store.rb +167 -0
  165. data/lib/shoko/adapters/storage/lazy_file_string.rb +65 -0
  166. data/lib/shoko/adapters/storage/pagination_cache.rb +127 -0
  167. data/lib/shoko/adapters/storage/recent_files.rb +78 -0
  168. data/lib/shoko/adapters/storage/repositories/annotation_repository.rb +182 -0
  169. data/lib/shoko/adapters/storage/repositories/base_repository.rb +81 -0
  170. data/lib/shoko/adapters/storage/repositories/bookmark_repository.rb +132 -0
  171. data/lib/shoko/adapters/storage/repositories/cached_library_repository.rb +129 -0
  172. data/lib/shoko/adapters/storage/repositories/config_repository.rb +262 -0
  173. data/lib/shoko/adapters/storage/repositories/progress_repository.rb +166 -0
  174. data/lib/shoko/adapters/storage/repositories/storage/annotation_file_store.rb +128 -0
  175. data/lib/shoko/adapters/storage/repositories/storage/bookmark_file_store.rb +109 -0
  176. data/lib/shoko/adapters/storage/repositories/storage/file_store_utils.rb +20 -0
  177. data/lib/shoko/adapters/storage/repositories/storage/progress_file_store.rb +59 -0
  178. data/lib/shoko/application/annotation_editor_overlay_session.rb +138 -0
  179. data/lib/shoko/application/cli.rb +134 -0
  180. data/lib/shoko/application/controllers/menu/input_controller.rb +189 -0
  181. data/lib/shoko/application/controllers/menu/state_controller.rb +642 -0
  182. data/lib/shoko/application/controllers/menu_controller.rb +469 -0
  183. data/lib/shoko/application/controllers/mouseable_reader.rb +377 -0
  184. data/lib/shoko/application/controllers/reader_controller.rb +449 -0
  185. data/lib/shoko/application/controllers/state_controller.rb +410 -0
  186. data/lib/shoko/application/controllers/ui_controller.rb +782 -0
  187. data/lib/shoko/application/dependency_container.rb +301 -0
  188. data/lib/shoko/application/infrastructure/event_bus.rb +80 -0
  189. data/lib/shoko/application/infrastructure/observer_state_store.rb +136 -0
  190. data/lib/shoko/application/infrastructure/state_store.rb +413 -0
  191. data/lib/shoko/application/main_menu/menu_progress_presenter.rb +83 -0
  192. data/lib/shoko/application/pending_jump_handler.rb +122 -0
  193. data/lib/shoko/application/reader_lifecycle.rb +65 -0
  194. data/lib/shoko/application/reader_startup_orchestrator.rb +113 -0
  195. data/lib/shoko/application/selectors/config_selectors.rb +62 -0
  196. data/lib/shoko/application/selectors/menu_selectors.rb +62 -0
  197. data/lib/shoko/application/selectors/reader_selectors.rb +186 -0
  198. data/lib/shoko/application/state/actions/base_action.rb +24 -0
  199. data/lib/shoko/application/state/actions/quit_to_menu_action.rb +16 -0
  200. data/lib/shoko/application/state/actions/switch_reader_mode_action.rb +22 -0
  201. data/lib/shoko/application/state/actions/toggle_view_mode_action.rb +31 -0
  202. data/lib/shoko/application/state/actions/update_annotation_editor_overlay_action.rb +27 -0
  203. data/lib/shoko/application/state/actions/update_annotations_action.rb +20 -0
  204. data/lib/shoko/application/state/actions/update_annotations_overlay_action.rb +27 -0
  205. data/lib/shoko/application/state/actions/update_bookmarks_action.rb +20 -0
  206. data/lib/shoko/application/state/actions/update_chapter_action.rb +24 -0
  207. data/lib/shoko/application/state/actions/update_config_action.rb +22 -0
  208. data/lib/shoko/application/state/actions/update_field_helpers.rb +26 -0
  209. data/lib/shoko/application/state/actions/update_menu_action.rb +21 -0
  210. data/lib/shoko/application/state/actions/update_message_action.rb +35 -0
  211. data/lib/shoko/application/state/actions/update_page_action.rb +21 -0
  212. data/lib/shoko/application/state/actions/update_pagination_state_action.rb +21 -0
  213. data/lib/shoko/application/state/actions/update_popup_menu_action.rb +27 -0
  214. data/lib/shoko/application/state/actions/update_reader_meta_action.rb +21 -0
  215. data/lib/shoko/application/state/actions/update_reader_mode_action.rb +20 -0
  216. data/lib/shoko/application/state/actions/update_rendered_lines_action.rb +40 -0
  217. data/lib/shoko/application/state/actions/update_selection_action.rb +27 -0
  218. data/lib/shoko/application/state/actions/update_selections_action.rb +21 -0
  219. data/lib/shoko/application/state/actions/update_sidebar_action.rb +34 -0
  220. data/lib/shoko/application/state/actions/update_ui_loading_action.rb +23 -0
  221. data/lib/shoko/application/ui/reader_view_model_builder.rb +74 -0
  222. data/lib/shoko/application/ui/view_models/reader_view_model.rb +177 -0
  223. data/lib/shoko/application/unified_application.rb +48 -0
  224. data/lib/shoko/application/use_cases/catalog_service.rb +117 -0
  225. data/lib/shoko/application/use_cases/commands/annotation_editor_commands.rb +105 -0
  226. data/lib/shoko/application/use_cases/commands/application_commands.rb +208 -0
  227. data/lib/shoko/application/use_cases/commands/base_command.rb +166 -0
  228. data/lib/shoko/application/use_cases/commands/bookmark_commands.rb +114 -0
  229. data/lib/shoko/application/use_cases/commands/conditional_navigation_commands.rb +57 -0
  230. data/lib/shoko/application/use_cases/commands/menu_commands.rb +170 -0
  231. data/lib/shoko/application/use_cases/commands/navigation_commands.rb +183 -0
  232. data/lib/shoko/application/use_cases/commands/reader_commands.rb +46 -0
  233. data/lib/shoko/application/use_cases/commands/sidebar_commands.rb +55 -0
  234. data/lib/shoko/application/use_cases/settings_service.rb +123 -0
  235. data/lib/shoko/core/events/annotation_events.rb +94 -0
  236. data/lib/shoko/core/events/base_domain_event.rb +169 -0
  237. data/lib/shoko/core/events/bookmark_events.rb +41 -0
  238. data/lib/shoko/core/events/domain_event_bus.rb +163 -0
  239. data/lib/shoko/core/events/progress_events.rb +108 -0
  240. data/lib/shoko/core/models/bookmark.rb +36 -0
  241. data/lib/shoko/core/models/bookmark_data.rb +10 -0
  242. data/lib/shoko/core/models/chapter.rb +25 -0
  243. data/lib/shoko/core/models/content_block.rb +44 -0
  244. data/lib/shoko/core/models/reader_settings.rb +20 -0
  245. data/lib/shoko/core/models/selection_anchor.rb +73 -0
  246. data/lib/shoko/core/models/toc_entry.rb +14 -0
  247. data/lib/shoko/core/ports/annotation_repository.rb +0 -0
  248. data/lib/shoko/core/ports/book_repository.rb +0 -0
  249. data/lib/shoko/core/ports/book_source.rb +0 -0
  250. data/lib/shoko/core/ports/bookmark_repository.rb +0 -0
  251. data/lib/shoko/core/ports/cache.rb +0 -0
  252. data/lib/shoko/core/ports/input_handler.rb +0 -0
  253. data/lib/shoko/core/ports/renderer.rb +0 -0
  254. data/lib/shoko/core/ports/storage.rb +0 -0
  255. data/lib/shoko/core/services/annotation_service.rb +102 -0
  256. data/lib/shoko/core/services/base_service.rb +60 -0
  257. data/lib/shoko/core/services/bookmark_service.rb +267 -0
  258. data/lib/shoko/core/services/coordinate_service.rb +265 -0
  259. data/lib/shoko/core/services/layout_service.rb +95 -0
  260. data/lib/shoko/core/services/navigation/absolute_change_applier.rb +96 -0
  261. data/lib/shoko/core/services/navigation/absolute_layout.rb +101 -0
  262. data/lib/shoko/core/services/navigation/absolute_strategy.rb +179 -0
  263. data/lib/shoko/core/services/navigation/context_builder.rb +52 -0
  264. data/lib/shoko/core/services/navigation/context_helpers.rb +63 -0
  265. data/lib/shoko/core/services/navigation/dynamic_change_applier.rb +50 -0
  266. data/lib/shoko/core/services/navigation/dynamic_strategy.rb +51 -0
  267. data/lib/shoko/core/services/navigation/image_offset_snapper.rb +150 -0
  268. data/lib/shoko/core/services/navigation/nav_context.rb +27 -0
  269. data/lib/shoko/core/services/navigation/state_updater.rb +29 -0
  270. data/lib/shoko/core/services/navigation/strategy_factory.rb +20 -0
  271. data/lib/shoko/core/services/navigation_service.rb +150 -0
  272. data/lib/shoko/core/services/page_calculator_service.rb +242 -0
  273. data/lib/shoko/core/services/pagination/internal/absolute_page_map_builder.rb +28 -0
  274. data/lib/shoko/core/services/pagination/internal/chapter_cache.rb +60 -0
  275. data/lib/shoko/core/services/pagination/internal/dynamic_page_map_builder.rb +157 -0
  276. data/lib/shoko/core/services/pagination/internal/layout_metrics_calculator.rb +73 -0
  277. data/lib/shoko/core/services/pagination/internal/page_hydrator.rb +145 -0
  278. data/lib/shoko/core/services/pagination/internal/pagination_workflow.rb +152 -0
  279. data/lib/shoko/core/services/pagination/page_info_calculator.rb +247 -0
  280. data/lib/shoko/core/services/pagination/pagination_cache_preloader.rb +173 -0
  281. data/lib/shoko/core/services/pagination/pagination_coordinator.rb +202 -0
  282. data/lib/shoko/core/services/pagination/pagination_orchestrator.rb +291 -0
  283. data/lib/shoko/core/services/pagination.rb +10 -0
  284. data/lib/shoko/core/services/progress_helper.rb +22 -0
  285. data/lib/shoko/core/services/selection_service.rb +126 -0
  286. data/lib/shoko/core/validator.rb +76 -0
  287. data/lib/shoko/shared/errors.rb +97 -0
  288. data/lib/shoko/shared/version.rb +5 -0
  289. data/lib/shoko/test_support/terminal_double.rb +175 -0
  290. data/lib/shoko/test_support/test_mode.rb +78 -0
  291. data/lib/shoko.rb +279 -0
  292. data/lib/zip.rb +732 -0
  293. data/zip.rb +5 -0
  294. metadata +370 -0
@@ -0,0 +1,377 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ostruct'
4
+
5
+ require_relative 'reader_controller'
6
+ require_relative '../../adapters/input/annotations/mouse_handler.rb'
7
+ # Removed unused: ui/components/popup_menu
8
+ require_relative '../../adapters/output/ui/components/enhanced_popup_menu.rb'
9
+ require_relative '../../adapters/output/ui/components/tooltip_overlay_component.rb'
10
+
11
+ module Shoko
12
+ module Application
13
+ module Controllers
14
+ # A Reader that supports mouse interactions for annotations.
15
+ class MouseableReader < ReaderController
16
+ SCROLL_WHEEL_STEP = 3
17
+
18
+ def initialize(epub_path, config = nil, dependencies = nil)
19
+ # Pass dependencies to parent ReaderController
20
+ super
21
+
22
+ # Resolve coordinate service (clipboard already resolved in parent)
23
+ @coordinate_service = dependencies.resolve(:coordinate_service)
24
+
25
+ @mouse_handler = Shoko::Adapters::Input::Annotations::MouseHandler.new
26
+ @sidebar_scroll_drag_active = false
27
+ state.dispatch(Application::Actions::ClearPopupMenuAction.new)
28
+ @selected_text = nil
29
+ state.dispatch(Application::Actions::ClearSelectionAction.new)
30
+ state.dispatch(Application::Actions::ClearRenderedLinesAction.new)
31
+ refresh_annotations
32
+ end
33
+
34
+ def run
35
+ terminal_service.enable_mouse
36
+ super
37
+ ensure
38
+ terminal_service.disable_mouse
39
+ end
40
+
41
+ def read_input_keys(timeout: nil)
42
+ key = terminal_service.read_input_with_mouse(timeout: timeout)
43
+ return [] unless key
44
+
45
+ if key.start_with?("\e[<")
46
+ handle_mouse_input(key)
47
+ return []
48
+ end
49
+
50
+ keys = [key]
51
+ while (extra = terminal_service.read_key)
52
+ keys << extra
53
+ break if keys.size > 10
54
+ end
55
+ keys
56
+ end
57
+
58
+ def handle_mouse_input(input)
59
+ event = @mouse_handler.parse_mouse_event(input)
60
+ return unless event
61
+
62
+ editor_overlay = Shoko::Application::Selectors::ReaderSelectors.annotation_editor_overlay(state)
63
+ if editor_overlay.respond_to?(:visible?) && editor_overlay.visible? && event[:released]
64
+ handle_annotation_editor_click(event)
65
+ return
66
+ end
67
+
68
+ popup_menu = Shoko::Application::Selectors::ReaderSelectors.popup_menu(state)
69
+ if popup_menu&.visible && event[:released]
70
+ handle_popup_click(event)
71
+ return
72
+ end
73
+
74
+ return if handle_sidebar_mouse(event)
75
+
76
+ result = @mouse_handler.handle_event(event)
77
+ return unless result
78
+
79
+ case result[:type]
80
+ when :selection_drag
81
+ # Keep selection in state while dragging so overlay can render purely from state
82
+ update_state_selection(@mouse_handler.selection_range)
83
+ refresh_highlighting
84
+ when :selection_end
85
+ handle_selection_end
86
+ draw_screen
87
+ else
88
+ draw_screen
89
+ end
90
+ end
91
+
92
+ private
93
+
94
+ def handle_sidebar_mouse(event)
95
+ return false if @mouse_handler.selecting
96
+
97
+ terminal_coords = @coordinate_service.mouse_to_terminal(event[:x], event[:y])
98
+ height, width = terminal_service.size
99
+ sidebar_bounds = render_coordinator.sidebar_bounds(width, height)
100
+ return false unless sidebar_bounds
101
+ sidebar_component = render_coordinator.sidebar_component
102
+ return false unless sidebar_component
103
+
104
+ if @sidebar_scroll_drag_active
105
+ return handle_sidebar_scroll_drag(event, terminal_coords, sidebar_bounds, sidebar_component)
106
+ end
107
+
108
+ unless @coordinate_service.within_bounds?(
109
+ terminal_coords[:x],
110
+ terminal_coords[:y],
111
+ sidebar_bounds
112
+ )
113
+ @mouse_handler.reset
114
+ return false
115
+ end
116
+
117
+ if (delta = mouse_wheel_delta(event[:button]))
118
+ return true if handle_sidebar_wheel(delta, terminal_coords, sidebar_bounds, sidebar_component)
119
+ end
120
+
121
+ if event[:button].zero? && !event[:released]
122
+ return true if start_sidebar_scroll_drag(terminal_coords, sidebar_bounds, sidebar_component)
123
+ end
124
+
125
+ if event[:released] && event[:button].zero?
126
+ tab = sidebar_component.tab_for_point(
127
+ terminal_coords[:x],
128
+ terminal_coords[:y],
129
+ sidebar_bounds
130
+ )
131
+ if tab
132
+ ui_controller.activate_sidebar_tab(tab)
133
+ draw_screen
134
+ @mouse_handler.reset
135
+ return true
136
+ end
137
+
138
+ toc_item = sidebar_component.toc_entry_at(
139
+ terminal_coords[:x],
140
+ terminal_coords[:y],
141
+ sidebar_bounds
142
+ )
143
+ if toc_item && ui_controller.respond_to?(:handle_sidebar_toc_click)
144
+ ui_controller.handle_sidebar_toc_click(toc_item.full_index)
145
+ draw_screen
146
+ end
147
+ end
148
+
149
+ @mouse_handler.reset
150
+ true
151
+ end
152
+
153
+ def mouse_wheel_delta(button)
154
+ case button
155
+ when 64
156
+ -1
157
+ when 65
158
+ 1
159
+ end
160
+ end
161
+
162
+ def handle_sidebar_wheel(delta, terminal_coords, sidebar_bounds, sidebar_component)
163
+ metrics = sidebar_component.toc_scroll_metrics(sidebar_bounds)
164
+ return false unless metrics
165
+ return false unless metrics.row_in_track?(terminal_coords[:y])
166
+
167
+ indices = metrics.navigable_indices
168
+ return false if indices.empty?
169
+
170
+ current_full = metrics.selected_full_index || indices.first
171
+ current_pos = metrics.nav_position_for(current_full)
172
+ if current_pos.nil? && metrics.selected_visible_index
173
+ fallback_full = metrics.visible_indices[metrics.selected_visible_index]
174
+ current_pos = metrics.nav_position_for(fallback_full)
175
+ end
176
+ current_pos ||= 0
177
+
178
+ step = SCROLL_WHEEL_STEP * delta
179
+ target_pos = (current_pos + step).clamp(0, indices.length - 1)
180
+ target_full = indices[target_pos]
181
+
182
+ if ui_controller.respond_to?(:set_sidebar_toc_selected)
183
+ ui_controller.set_sidebar_toc_selected(target_full)
184
+ else
185
+ state.dispatch(Application::Actions::UpdateSidebarAction.new(toc_selected: target_full))
186
+ end
187
+ draw_screen
188
+ @mouse_handler.reset
189
+ true
190
+ end
191
+
192
+ def start_sidebar_scroll_drag(terminal_coords, sidebar_bounds, sidebar_component)
193
+ metrics = sidebar_component.toc_scroll_metrics(sidebar_bounds)
194
+ return false unless metrics
195
+ return false unless metrics.hit_scrollbar?(terminal_coords[:x], terminal_coords[:y])
196
+
197
+ @sidebar_scroll_drag_active = true
198
+ apply_sidebar_scroll_drag(metrics, terminal_coords[:y])
199
+ draw_screen
200
+ @mouse_handler.reset
201
+ true
202
+ end
203
+
204
+ def handle_sidebar_scroll_drag(event, terminal_coords, sidebar_bounds, sidebar_component)
205
+ if event[:released]
206
+ @sidebar_scroll_drag_active = false
207
+ @mouse_handler.reset
208
+ return true
209
+ end
210
+
211
+ return true unless drag_motion?(event) || event[:button].zero?
212
+
213
+ metrics = sidebar_component.toc_scroll_metrics(sidebar_bounds)
214
+ return true unless metrics
215
+
216
+ apply_sidebar_scroll_drag(metrics, terminal_coords[:y])
217
+ draw_screen
218
+ true
219
+ end
220
+
221
+ def drag_motion?(event)
222
+ (event[:button] & 32) != 0
223
+ end
224
+
225
+ def apply_sidebar_scroll_drag(metrics, abs_row)
226
+ full_index = metrics.full_index_for_abs_row(abs_row)
227
+ return unless full_index
228
+
229
+ if ui_controller.respond_to?(:set_sidebar_toc_selected)
230
+ ui_controller.set_sidebar_toc_selected(full_index)
231
+ else
232
+ state.dispatch(Application::Actions::UpdateSidebarAction.new(toc_selected: full_index))
233
+ end
234
+ end
235
+
236
+ def handle_popup_click(event)
237
+ # Use coordinate service for consistent mouse-to-terminal conversion
238
+ terminal_coords = @coordinate_service.mouse_to_terminal(event[:x], event[:y])
239
+
240
+ popup_menu = Shoko::Application::Selectors::ReaderSelectors.popup_menu(state)
241
+ item = popup_menu.handle_click(terminal_coords[:x], terminal_coords[:y])
242
+
243
+ if item
244
+ handle_popup_action(item)
245
+ else
246
+ state.dispatch(Application::Actions::ClearPopupMenuAction.new)
247
+ @mouse_handler.reset
248
+ state.dispatch(Application::Actions::ClearSelectionAction.new)
249
+ end
250
+ draw_screen
251
+ end
252
+
253
+ def handle_annotation_editor_click(event)
254
+ coords = @coordinate_service.mouse_to_terminal(event[:x], event[:y])
255
+ overlay = Shoko::Application::Selectors::ReaderSelectors.annotation_editor_overlay(state)
256
+ return unless overlay
257
+
258
+ result = overlay.handle_click(coords[:x], coords[:y])
259
+ if result
260
+ ui = dependencies.resolve(:ui_controller)
261
+ if ui.respond_to?(:handle_annotation_editor_overlay_event, true)
262
+ ui.send(:handle_annotation_editor_overlay_event, result)
263
+ end
264
+ end
265
+ @mouse_handler.reset
266
+ ensure
267
+ draw_screen
268
+ end
269
+
270
+ def handle_selection_end
271
+ update_state_selection(@mouse_handler.selection_range)
272
+ sel = state.get(%i[reader selection])
273
+ return unless sel
274
+
275
+ @selected_text = extract_selected_text(sel)
276
+
277
+ if @selected_text && !@selected_text.strip.empty?
278
+ show_popup_menu
279
+ else
280
+ @mouse_handler.reset
281
+ state.dispatch(Application::Actions::ClearSelectionAction.new)
282
+ end
283
+ end
284
+
285
+ def show_popup_menu
286
+ selection = Shoko::Application::Selectors::ReaderSelectors.selection(state)
287
+ return unless selection
288
+
289
+ # Use enhanced popup menu with coordinate service
290
+ rendered = Shoko::Application::Selectors::ReaderSelectors.rendered_lines(state)
291
+ popup_menu = Shoko::Adapters::Output::Ui::Components::EnhancedPopupMenu.new(selection, nil, @coordinate_service,
292
+ clipboard_service, rendered)
293
+ state.dispatch(Application::Actions::UpdatePopupMenuAction.new(popup_menu))
294
+ return unless popup_menu&.visible # Only proceed if menu was created successfully
295
+
296
+ # Ensure popup menu has proper focus and state
297
+ switch_mode(:popup_menu)
298
+
299
+ # Force a complete redraw to ensure popup appears correctly
300
+ draw_screen
301
+ end
302
+
303
+ # Clear any active text selection and hide popup
304
+ def clear_selection!
305
+ state.dispatch(Application::Actions::ClearPopupMenuAction.new)
306
+ @mouse_handler&.reset
307
+ state&.dispatch(Application::Actions::ClearSelectionAction.new)
308
+ end
309
+
310
+ # Old highlighting methods removed - now handled by TooltipOverlayComponent
311
+ # This eliminates the direct terminal writes that bypassed the component system
312
+
313
+ def refresh_annotations
314
+ begin
315
+ service = dependencies.resolve(:annotation_service)
316
+ annotations = service.list_for_book(path)
317
+ rescue StandardError
318
+ annotations = []
319
+ end
320
+ state.dispatch(Application::Actions::UpdateAnnotationsAction.new(annotations))
321
+ end
322
+
323
+ def extract_selected_text(range)
324
+ selection_service = dependencies.resolve(:selection_service)
325
+ if selection_service.respond_to?(:extract_from_state)
326
+ selection_service.extract_from_state(state, range)
327
+ else
328
+ rendered = Shoko::Application::Selectors::ReaderSelectors.rendered_lines(state)
329
+ selection_service.extract_text(range, rendered)
330
+ end
331
+ end
332
+
333
+ def update_state_selection(mouse_range)
334
+ anchor_range = anchor_range_from_mouse(mouse_range)
335
+ if anchor_range
336
+ state.dispatch(Application::Actions::UpdateSelectionAction.new(anchor_range))
337
+ else
338
+ state.dispatch(Application::Actions::ClearSelectionAction.new)
339
+ end
340
+ end
341
+
342
+ def anchor_range_from_mouse(mouse_range)
343
+ return nil unless mouse_range
344
+
345
+ rendered = Shoko::Application::Selectors::ReaderSelectors.rendered_lines(state)
346
+ return nil if rendered.empty?
347
+
348
+ start_anchor = @coordinate_service.anchor_from_point(mouse_range[:start], rendered, bias: :leading)
349
+ end_anchor = @coordinate_service.anchor_from_point(mouse_range[:end], rendered, bias: :trailing)
350
+ return nil unless start_anchor && end_anchor
351
+
352
+ @coordinate_service.normalize_selection_range(
353
+ { start: start_anchor.to_h, end: end_anchor.to_h }, rendered
354
+ )
355
+ end
356
+
357
+ def copy_to_clipboard(text)
358
+ ui = dependencies.resolve(:ui_controller)
359
+ clipboard_service.copy_with_feedback(text) do |message|
360
+ ui.set_message(message)
361
+ rescue StandardError
362
+ # best-effort
363
+ end
364
+ rescue Adapters::Output::Clipboard::ClipboardService::ClipboardError => e
365
+ begin
366
+ ui.set_message("Copy failed: #{e.message}")
367
+ rescue StandardError
368
+ # ignore
369
+ end
370
+ false
371
+ end
372
+
373
+ # Column helpers moved to CoordinateService
374
+ end
375
+ end
376
+ end
377
+ end