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,449 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ # Legacy reader modes removed; TOC/bookmarks live in the sidebar.
5
+ require_relative '../../adapters/output/ui/constants/ui_constants.rb'
6
+ require_relative '../../shared/errors'
7
+ require_relative '../../adapters/output/ui/constants/messages.rb'
8
+ require_relative '../annotation_editor_overlay_session'
9
+ require_relative '../reader_lifecycle'
10
+ require_relative '../../adapters/output/ui/rendering/reader_render_coordinator.rb'
11
+ require_relative '../../core/services/pagination/pagination_coordinator.rb'
12
+ require_relative '../../adapters/input/dispatcher.rb'
13
+ require_relative '../../adapters/output/ui/rendering/frame_coordinator.rb'
14
+ require_relative '../../adapters/output/ui/rendering/render_pipeline.rb'
15
+ require_relative '../pending_jump_handler'
16
+
17
+ module Shoko
18
+ module Application
19
+ module Controllers
20
+ # Coordinator class for the reading experience.
21
+ #
22
+ # This refactored ReaderController now delegates responsibilities to focused controllers/services:
23
+ # - Core::Services::NavigationService: handles page/chapter navigation (via input bindings)
24
+ # - UIController: handles mode switching and UI state
25
+ # - StateController: handles persistence and state management
26
+ # - Adapters::Input::InputController: handles all input processing
27
+ #
28
+ # The ReaderController now focuses only on:
29
+ # - Component layout and rendering coordination
30
+ # - Controller coordination and delegation
31
+ # - Main application loop
32
+ #
33
+ # @attr_reader doc [EPUBDocument] The loaded EPUB document.
34
+ # @attr_reader state [Application::Infrastructure::ObserverStateStore] The current state of the reader.
35
+ class ReaderController
36
+ extend Forwardable
37
+ include Adapters::Output::Ui::Constants::UI
38
+ # Helpers::ReaderHelpers removed; wrapping is provided by DI-backed WrappingService
39
+ include Adapters::Input::KeyDefinitions::Helpers
40
+
41
+ # Core runtime context for the reader.
42
+ Context = Struct.new(:path, :dependencies, :state, :doc, :metrics_start_time, :memo, keyword_init: true)
43
+ # Service references used across the reader lifecycle.
44
+ Services = Struct.new(:page_calculator, :terminal_service, :clipboard_service, :instrumentation, keyword_init: true)
45
+ # Group UI/state/input controllers for delegation.
46
+ ControllerRefs = Struct.new(:ui_controller, :state_controller, :input_controller, keyword_init: true)
47
+ # Group lifecycle/render/pagination coordinators for delegation.
48
+ Coordinators = Struct.new(:lifecycle, :pagination_coordinator, :render_coordinator, keyword_init: true)
49
+
50
+ attr_reader :context, :services, :controllers, :coordinators
51
+
52
+ def_delegators :context, :path, :dependencies, :state, :doc, :metrics_start_time
53
+ def_delegators :services, :page_calculator, :terminal_service, :clipboard_service, :instrumentation
54
+ def_delegators :controllers, :ui_controller, :state_controller, :input_controller
55
+ def_delegators :coordinators, :lifecycle, :pagination_coordinator, :render_coordinator
56
+
57
+ # Navigation is handled via Core::Services::NavigationService through input commands
58
+
59
+ def_delegators :ui_controller, :switch_mode, :open_toc, :open_bookmarks, :open_annotations_tab,
60
+ :open_annotations,
61
+ :show_help, :toggle_view_mode, :increase_line_spacing, :decrease_line_spacing,
62
+ :toggle_page_numbering_mode, :sidebar_down, :sidebar_up, :sidebar_select,
63
+ :handle_popup_action
64
+
65
+ def_delegators :state_controller, :save_progress, :load_progress, :load_bookmarks,
66
+ :add_bookmark, :jump_to_bookmark, :delete_selected_bookmark, :quit_to_menu,
67
+ :quit_application
68
+
69
+ def_delegators :input_controller, :handle_popup_navigation, :handle_popup_action_key,
70
+ :handle_popup_cancel, :handle_popup_menu_input
71
+
72
+ def_delegators :render_coordinator, :draw_screen, :refresh_highlighting, :force_redraw,
73
+ :render_loading_overlay, :build_component_layout, :rebuild_root_layout,
74
+ :apply_theme_palette
75
+
76
+ def_delegators :pagination_coordinator, :pending_initial_calculation?,
77
+ :perform_initial_calculations_if_needed, :defer_page_map?,
78
+ :schedule_background_page_map_build, :clear_defer_page_map!,
79
+ :rebuild_pagination, :invalidate_pagination_cache
80
+
81
+ def_delegators :lifecycle, :run, :background_worker
82
+
83
+ def initialize(epub_path, _config = nil, dependencies = nil)
84
+ deps = dependencies || Application::ContainerFactory.create_default_container
85
+ state_store = deps.resolve(:global_state)
86
+ @context = Context.new(path: epub_path,
87
+ dependencies: deps,
88
+ state: state_store,
89
+ doc: nil,
90
+ metrics_start_time: nil,
91
+ memo: {})
92
+ @services = Services.new(
93
+ page_calculator: deps.resolve(:page_calculator),
94
+ terminal_service: deps.resolve(:terminal_service),
95
+ clipboard_service: deps.resolve(:clipboard_service),
96
+ instrumentation: resolve_optional(:instrumentation_service)
97
+ )
98
+ lifecycle = ReaderLifecycle.new(self,
99
+ dependencies: deps,
100
+ terminal_service: terminal_service)
101
+ @coordinators = Coordinators.new(lifecycle: lifecycle,
102
+ pagination_coordinator: nil,
103
+ render_coordinator: nil)
104
+ lifecycle.ensure_background_worker
105
+
106
+ # Load document before creating controllers that depend on it
107
+ @context.doc = preload_document_from_dependencies
108
+ load_document unless doc
109
+ # Expose current book path in state for downstream services/screens
110
+ state.dispatch(Shoko::Application::Actions::UpdateSelectionsAction.new(book_path: path))
111
+
112
+ # Initialize focused controllers with proper dependencies including document
113
+ ui = UIController.new(state, deps)
114
+ sc = StateController.new(state, doc, epub_path, deps)
115
+ input = Adapters::Input::InputController.new(state, deps)
116
+ @controllers = ControllerRefs.new(ui_controller: ui,
117
+ state_controller: sc,
118
+ input_controller: input)
119
+
120
+ # Register controllers in the dependency container for components that resolve them
121
+ deps.register(:ui_controller, ui)
122
+ deps.register(:state_controller, sc)
123
+ deps.register(:input_controller, input)
124
+ # Expose reader controller for components/controllers needing cleanup hooks
125
+ deps.register(:reader_controller, self)
126
+
127
+ frame_coordinator = Shoko::Adapters::Output::Ui::Rendering::FrameCoordinator.new(deps)
128
+ render_pipeline = Shoko::Adapters::Output::Ui::Rendering::RenderPipeline.new(deps)
129
+ pagination = Core::Services::Pagination::PaginationCoordinator.new(
130
+ dependencies: Core::Services::Pagination::PaginationCoordinator::Dependencies.new(
131
+ state: state,
132
+ doc: doc,
133
+ page_calculator: page_calculator,
134
+ layout_service: deps.resolve(:layout_service),
135
+ terminal_service: terminal_service,
136
+ pagination_cache: resolve_optional(:pagination_cache),
137
+ frame_coordinator: frame_coordinator,
138
+ ui_controller: ui,
139
+ render_callback: lambda {
140
+ force_redraw
141
+ draw_screen
142
+ },
143
+ background_worker_provider: -> { background_worker }
144
+ )
145
+ )
146
+ render = Shoko::Adapters::Output::Ui::Rendering::ReaderRenderCoordinator.new(
147
+ dependencies: Shoko::Adapters::Output::Ui::Rendering::ReaderRenderCoordinator::Dependencies.new(
148
+ controller: self,
149
+ state: state,
150
+ dependencies: deps,
151
+ terminal_service: terminal_service,
152
+ frame_coordinator: frame_coordinator,
153
+ render_pipeline: render_pipeline,
154
+ ui_controller: ui,
155
+ wrapping_service: wrapping_service,
156
+ pagination: pagination,
157
+ doc: doc
158
+ )
159
+ )
160
+ @coordinators.pagination_coordinator = pagination
161
+ @coordinators.render_coordinator = render
162
+
163
+ apply_theme_palette
164
+
165
+ # Do not load saved data synchronously to keep first paint fast.
166
+ # Pending jump application will occur after progress load in run.
167
+ apply_pending_jump_if_present
168
+
169
+ # Build UI components
170
+ build_component_layout
171
+ input_controller.setup_input_dispatcher(self)
172
+
173
+ # Observe sidebar visibility changes to rebuild layout
174
+ state.add_observer(self, %i[reader sidebar_visible], %i[config theme],
175
+ %i[config view_mode], %i[config line_spacing],
176
+ %i[config page_numbering_mode],
177
+ %i[config kitty_images])
178
+ end
179
+
180
+ # Observer callback for state changes
181
+ def state_changed(path, _old_value, _new_value)
182
+ case path
183
+ when %i[reader sidebar_visible]
184
+ rebuild_root_layout
185
+ when %i[config theme]
186
+ apply_theme_palette
187
+ when %i[config view_mode], %i[config line_spacing], %i[config page_numbering_mode], %i[config kitty_images]
188
+ begin
189
+ pagination_coordinator.rebuild_after_config_change
190
+ rescue StandardError
191
+ # best-effort rebuild; avoid crashing on layout changes
192
+ end
193
+ force_redraw
194
+ end
195
+ end
196
+
197
+ def perform_first_paint
198
+ instrumentation&.time('render.first_paint') { draw_screen }
199
+ unless metrics_start_time
200
+ instrumentation&.cancel_trace
201
+ return
202
+ end
203
+
204
+ first_paint_completed_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
205
+ ttfp = first_paint_completed_at - metrics_start_time
206
+ instrumentation&.record_metric('render.first_paint.ttfp', ttfp, 0)
207
+ instrumentation&.record_trace('render.first_paint.ttfp', ttfp)
208
+ open_type = if doc.respond_to?(:cached?) && doc.cached?
209
+ 'warm'
210
+ else
211
+ 'cold'
212
+ end
213
+ instrumentation&.complete_trace(open_type:, total_duration: ttfp)
214
+ end
215
+
216
+ def dispatch_input_keys(keys)
217
+ if annotations_overlay_active? && !annotation_editor_visible?
218
+ input_controller.handle_annotations_overlay_input(keys)
219
+ elsif popup_menu_visible?
220
+ input_controller.handle_popup_menu_input(keys)
221
+ else
222
+ keys.each { |key| input_controller.handle_key(key) }
223
+ end
224
+ end
225
+
226
+ def annotations_overlay_active?
227
+ overlay = Shoko::Application::Selectors::ReaderSelectors.annotations_overlay(state)
228
+ overlay.respond_to?(:visible?) && overlay.visible?
229
+ end
230
+
231
+ def annotation_editor_visible?
232
+ editor_overlay = Shoko::Application::Selectors::ReaderSelectors.annotation_editor_overlay(state)
233
+ editor_overlay.respond_to?(:visible?) && editor_overlay.visible?
234
+ end
235
+
236
+ def popup_menu_visible?
237
+ popup_menu = Shoko::Application::Selectors::ReaderSelectors.popup_menu(state)
238
+ popup_menu&.visible
239
+ end
240
+
241
+ # Main application loop
242
+ def main_loop
243
+ ReaderEventLoop.new(self, state, metrics_start_time, instrumentation).run
244
+ end
245
+
246
+ def mark_metrics_start!
247
+ context.metrics_start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
248
+ end
249
+
250
+ # Page calculation and navigation support
251
+ private
252
+
253
+ def resolve_optional(service_name)
254
+ return dependencies.resolve(service_name) if dependencies.registered?(service_name)
255
+
256
+ nil
257
+ rescue StandardError
258
+ nil
259
+ end
260
+
261
+ def memo
262
+ context.memo ||= {}
263
+ end
264
+
265
+ def selection_service
266
+ return memo[:selection_service] if memo.key?(:selection_service)
267
+
268
+ memo[:selection_service] = begin
269
+ dependencies.resolve(:selection_service)
270
+ rescue StandardError
271
+ nil
272
+ end
273
+ end
274
+
275
+ def wrapping_service
276
+ return memo[:wrapping_service] if memo.key?(:wrapping_service)
277
+
278
+ memo[:wrapping_service] = (dependencies.resolve(:wrapping_service) if dependencies.registered?(:wrapping_service))
279
+ rescue StandardError
280
+ memo[:wrapping_service] = nil
281
+ end
282
+
283
+ def preload_document_from_dependencies
284
+ return nil unless dependencies.respond_to?(:registered?) && dependencies.registered?(:document)
285
+
286
+ dependencies.resolve(:document)
287
+ rescue StandardError
288
+ nil
289
+ end
290
+
291
+ def load_document
292
+ return doc if doc
293
+
294
+ factory = dependencies.resolve(:document_service_factory)
295
+ document_service = factory.call(path)
296
+ @context.doc = document_service.load_document
297
+
298
+ # Register document in dependency container for services to access
299
+ dependencies.register(:document, doc)
300
+ # Expose chapter count for navigation service logic
301
+ begin
302
+ state.dispatch(Shoko::Application::Actions::UpdatePaginationStateAction.new(
303
+ total_chapters: doc&.chapter_count || 0
304
+ ))
305
+ rescue StandardError
306
+ # best-effort
307
+ end
308
+ doc
309
+ end
310
+
311
+ def load_data
312
+ state_controller.load_progress
313
+ state_controller.load_bookmarks
314
+ state_controller.refresh_annotations
315
+ end
316
+
317
+ def apply_pending_jump_if_present
318
+ jump_handler.apply
319
+ end
320
+
321
+ def jump_handler
322
+ memo[:jump_handler] ||= PendingJumpHandler.new(state, dependencies, ui_controller)
323
+ end
324
+
325
+ def normalize_selection_for_state(range)
326
+ service = selection_service
327
+ return nil unless service
328
+
329
+ service.normalize_range(state, range)
330
+ end
331
+
332
+ def read_input_keys(timeout: nil)
333
+ terminal_service.read_keys_blocking(limit: 10, timeout: timeout)
334
+ end
335
+
336
+ # Override helper to delegate to the DI-backed wrapping service
337
+ def wrap_lines(lines, width)
338
+ service = wrapping_service
339
+ if service
340
+ chapter_index = state&.get(%i[reader current_chapter]) || 0
341
+ return service.wrap_lines(lines, chapter_index, width)
342
+ end
343
+ # Fallback (tests/dev only)
344
+ lines
345
+ end
346
+
347
+ # Hook for subclasses (MouseableReader) to clear any active selection/popup
348
+ def clear_selection!
349
+ # no-op in base controller
350
+ end
351
+
352
+ def activate_annotation_editor_overlay_session
353
+ return memo[:overlay_session] if memo[:overlay_session]
354
+
355
+ memo[:overlay_session] = Shoko::Application::AnnotationEditorOverlaySession.new(
356
+ state,
357
+ dependencies,
358
+ ui_controller
359
+ )
360
+ end
361
+
362
+ def deactivate_annotation_editor_overlay_session
363
+ memo[:overlay_session] = nil
364
+ end
365
+
366
+ def current_editor_component
367
+ return memo[:overlay_session] if memo[:overlay_session]&.active?
368
+
369
+ deactivate_annotation_editor_overlay_session
370
+ ui_controller.current_mode
371
+ end
372
+
373
+ # Ensure both UI state and any local selection handlers are cleared
374
+ def cleanup_popup_state
375
+ ui_controller.cleanup_popup_state
376
+ clear_selection!
377
+ end
378
+
379
+ # Test helper moved to WrappingService#fetch_window_and_prefetch
380
+
381
+ public :activate_annotation_editor_overlay_session,
382
+ :deactivate_annotation_editor_overlay_session,
383
+ :current_editor_component
384
+
385
+ # Encapsulates the main reader event loop to tame ReaderController complexity.
386
+ class ReaderEventLoop
387
+ NOTIFICATION_POLL_INTERVAL = 0.1
388
+
389
+ def initialize(controller, state, metrics_start_time, instrumentation)
390
+ @controller = controller
391
+ @state = state
392
+ @metrics_start_time = metrics_start_time
393
+ @instrumentation = instrumentation
394
+ @tti_recorded = false
395
+ end
396
+
397
+ def run
398
+ controller.perform_first_paint
399
+ startup_reference = metrics_start_time
400
+
401
+ while running?
402
+ notification_active = toast_message_active?
403
+ keys = if notification_active
404
+ controller.read_input_keys(timeout: NOTIFICATION_POLL_INTERVAL)
405
+ else
406
+ controller.read_input_keys
407
+ end
408
+ record_tti(startup_reference, keys)
409
+ if keys.empty?
410
+ controller.draw_screen if notification_active
411
+ next
412
+ end
413
+
414
+ controller.dispatch_input_keys(keys)
415
+ controller.draw_screen
416
+ end
417
+ end
418
+
419
+ private
420
+
421
+ attr_reader :controller, :state, :metrics_start_time, :instrumentation
422
+
423
+ def running?
424
+ Shoko::Application::Selectors::ReaderSelectors.running?(state)
425
+ end
426
+
427
+ def record_tti(startup_reference, keys)
428
+ return if @tti_recorded
429
+ return unless startup_reference && keys.any?
430
+
431
+ instrumentation&.record_metric(
432
+ 'render.tti',
433
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) - startup_reference,
434
+ 0
435
+ )
436
+ @tti_recorded = true
437
+ end
438
+
439
+ def toast_message_active?
440
+ message = Shoko::Application::Selectors::ReaderSelectors.message(state)
441
+ message && !message.to_s.empty?
442
+ rescue StandardError
443
+ false
444
+ end
445
+ end
446
+ end
447
+ end
448
+ end
449
+ end