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,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Adapters::Output::Ui::Components
5
+ # Standard interface contract for all UI components.
6
+ # Defines the required methods and behavior for component lifecycle.
7
+ module ComponentInterface
8
+ # Called once when component is first rendered
9
+ def mount
10
+ raise NotImplementedError, 'Components must implement #mount'
11
+ end
12
+
13
+ # Called when component is removed from the UI
14
+ def unmount
15
+ raise NotImplementedError, 'Components must implement #unmount'
16
+ end
17
+
18
+ # Render this component into the given surface within bounds
19
+ # @param surface [Surface] Terminal surface wrapper
20
+ # @param bounds [Rect] Local bounds for this component
21
+ def render(surface, bounds)
22
+ raise NotImplementedError, 'Components must implement #render'
23
+ end
24
+
25
+ # Handle input key for this component
26
+ # @param key [String] Input key
27
+ # @return [Symbol] :handled or :pass_through
28
+ def handle_input(_key)
29
+ :pass_through
30
+ end
31
+
32
+ # Component height calculation for layout
33
+ # @param available_height [Integer] Total height available from parent
34
+ # @return [Integer, Symbol] Height requirement
35
+ def preferred_height(_available_height)
36
+ :flexible
37
+ end
38
+
39
+ # Component width calculation for layout
40
+ # @param available_width [Integer] Total width available from parent
41
+ # @return [Integer, Symbol] Width requirement
42
+ def preferred_width(_available_width)
43
+ :flexible
44
+ end
45
+
46
+ # Check if component needs re-rendering
47
+ # @return [Boolean]
48
+ def needs_update?
49
+ true
50
+ end
51
+
52
+ # Mark component as updated (no longer needs re-render)
53
+ def mark_updated
54
+ # Default implementation - override if needed
55
+ end
56
+
57
+ # Validate that a class properly implements the interface
58
+ def self.validate_implementation!(klass)
59
+ required_methods = %i[mount unmount render]
60
+ missing = required_methods.reject { |method| klass.method_defined?(method) }
61
+
62
+ return if missing.empty?
63
+
64
+ raise ArgumentError, "#{klass} missing required methods: #{missing.join(', ')}"
65
+ end
66
+
67
+ # Helper method to ensure a component follows the interface
68
+ def self.included(base)
69
+ base.extend(ClassMethods)
70
+ end
71
+
72
+ # Class-level helpers mixed into components implementing the interface.
73
+ module ClassMethods
74
+ def validate_interface!
75
+ ComponentInterface.validate_implementation!(self)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_component'
4
+ require_relative 'surface'
5
+ require_relative 'reading/view_renderer_factory'
6
+ require_relative 'reading/help_renderer'
7
+
8
+ module Shoko
9
+ module Adapters::Output::Ui::Components
10
+ # ContentComponent coordinates the main reading content area.
11
+ # It switches between help and the active view renderer based on state.
12
+ class ContentComponent < BaseComponent
13
+ def initialize(controller)
14
+ super(controller&.dependencies) # Initialize BaseComponent with dependencies
15
+ @controller = controller
16
+ @view_renderer = nil
17
+ deps = controller&.dependencies
18
+ @help_renderer = Reading::HelpRenderer.new(deps)
19
+
20
+ state = @controller.state
21
+ # Observe core fields that affect content rendering via StateStore paths
22
+ state.add_observer(self, %i[reader current_chapter], %i[reader left_page], %i[reader right_page],
23
+ %i[reader single_page], %i[reader current_page_index], %i[reader mode], %i[config view_mode])
24
+ end
25
+
26
+ # Observer callback triggered by ObserverStateStore
27
+ def state_changed(path, old_value, new_value)
28
+ # Reset renderer for mode changes or view mode changes
29
+ @view_renderer = nil if [%i[reader mode], %i[config view_mode]].include?(path)
30
+
31
+ # Call parent invalidate to properly trigger re-rendering
32
+ super
33
+ end
34
+
35
+ # Fill remaining space after fixed components
36
+ def preferred_height(_available_height)
37
+ :fill
38
+ end
39
+
40
+ def do_render(surface, bounds)
41
+ state = @controller.state
42
+
43
+ case state.get(%i[reader mode])
44
+ when :help
45
+ @help_renderer.render(surface, bounds)
46
+ else
47
+ view_renderer.render(surface, bounds)
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def view_renderer
54
+ return @view_renderer if @view_renderer
55
+
56
+ @view_renderer = Reading::ViewRendererFactory.create(@controller.state,
57
+ @controller.dependencies)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,191 @@
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
+
7
+ module Shoko
8
+ module Adapters::Output::Ui::Components
9
+ # Enhanced popup menu that uses the coordinate service for consistent positioning
10
+ # and integrates with the clipboard service for reliable copy functionality.
11
+ class EnhancedPopupMenu < BaseComponent
12
+ include Adapters::Output::Ui::Constants::UI
13
+
14
+ attr_reader :visible, :selected_index, :x, :y, :width, :height
15
+
16
+ def initialize(selection_range, available_actions = nil, coordinate_service = nil,
17
+ clipboard_service = nil, rendered_lines = nil)
18
+ super()
19
+ @coordinate_service = coordinate_service
20
+ @clipboard_service = clipboard_service
21
+ @rendered_lines = rendered_lines || {}
22
+
23
+ @selection_range = @coordinate_service.normalize_selection_range(selection_range, @rendered_lines)
24
+
25
+ unless @selection_range
26
+ @visible = false
27
+ return
28
+ end
29
+
30
+ @available_actions = available_actions || default_actions
31
+ @items = @available_actions.map { |action| action[:label] }
32
+ @selected_index = 0
33
+ @visible = true
34
+ @width = calculate_width
35
+ @height = @items.length
36
+
37
+ # Ensure we have at least one item before proceeding
38
+ unless @items.any?
39
+ @visible = false
40
+ return
41
+ end
42
+
43
+ # Calculate optimal position using coordinate service
44
+ position = popup_anchor_position(@selection_range[:end])
45
+ @x = position[:x]
46
+ @y = position[:y]
47
+ end
48
+
49
+ def do_render(surface, bounds)
50
+ return unless @visible
51
+
52
+ @items.each_with_index do |item, i|
53
+ render_menu_item(surface, bounds, item, i)
54
+ end
55
+ end
56
+
57
+ def handle_key(key)
58
+ return nil unless @visible
59
+
60
+ if Adapters::Input::KeyDefinitions::NAVIGATION[:up].include?(key)
61
+ move_selection(-1)
62
+ { type: :selection_change }
63
+ elsif Adapters::Input::KeyDefinitions::NAVIGATION[:down].include?(key)
64
+ move_selection(1)
65
+ { type: :selection_change }
66
+ elsif Adapters::Input::KeyDefinitions::ACTIONS[:confirm].include?(key)
67
+ execute_selected_action
68
+ elsif Adapters::Input::KeyDefinitions::ACTIONS[:cancel].include?(key)
69
+ { type: :cancel }
70
+ end
71
+ end
72
+
73
+ # Align with ComponentInterface naming; delegate to existing logic
74
+ def handle_input(key)
75
+ handle_key(key)
76
+ end
77
+
78
+ def handle_click(click_x, click_y)
79
+ return nil unless @visible && contains?(click_x, click_y)
80
+
81
+ clicked_index = click_y - @y
82
+ return nil unless clicked_index >= 0 && clicked_index < @items.length
83
+
84
+ @selected_index = clicked_index
85
+ execute_selected_action
86
+ end
87
+
88
+ def hide
89
+ @visible = false
90
+ end
91
+
92
+ def contains?(col, row)
93
+ bounds = Shoko::Adapters::Output::Ui::Components::Rect.new(x: @x, y: @y, width: @width, height: @height)
94
+ @coordinate_service.within_bounds?(col, row, bounds)
95
+ end
96
+
97
+ private
98
+
99
+ def default_actions
100
+ actions = []
101
+
102
+ # Always offer annotation creation
103
+ actions << {
104
+ label: 'Create Annotation',
105
+ action: :create_annotation,
106
+ icon: '󱓩',
107
+ }
108
+
109
+ # Only offer clipboard if available
110
+ if @clipboard_service&.available?
111
+ actions << {
112
+ label: 'Copy to Clipboard',
113
+ action: :copy_to_clipboard,
114
+ icon: '',
115
+ }
116
+ end
117
+
118
+ actions
119
+ end
120
+
121
+ def calculate_width
122
+ max_label_width = @items.map { |item| Shoko::Adapters::Output::Terminal::TextMetrics.visible_length(item) }.max || 0
123
+ max_label_width + 6 # Padding for icon and spacing
124
+ end
125
+
126
+ def move_selection(direction)
127
+ return if @items.empty?
128
+
129
+ @selected_index = (@selected_index + direction) % @items.length
130
+ end
131
+
132
+ def execute_selected_action
133
+ action = @available_actions[@selected_index]
134
+ return { type: :cancel } unless action
135
+
136
+ {
137
+ type: :action,
138
+ action: action[:action],
139
+ data: {
140
+ selection_range: @selection_range,
141
+ action_config: action,
142
+ },
143
+ }
144
+ end
145
+
146
+ def popup_anchor_position(anchor_hash)
147
+ anchor = Shoko::Core::Models::SelectionAnchor.from(anchor_hash)
148
+ geometry = geometry_for_anchor(anchor)
149
+ return { x: 1, y: 1 } unless geometry
150
+
151
+ x = if anchor.cell_index >= geometry.cells.length
152
+ geometry.column_origin + geometry.visible_width
153
+ else
154
+ cell = geometry.cells[anchor.cell_index]
155
+ geometry.column_origin + cell.screen_x
156
+ end
157
+ y = geometry.row
158
+
159
+ @coordinate_service.calculate_popup_position({ x:, y: }, @width, @height)
160
+ end
161
+
162
+ def geometry_for_anchor(anchor)
163
+ return nil unless anchor
164
+
165
+ return nil unless @rendered_lines
166
+
167
+ entry = @rendered_lines[anchor.geometry_key]
168
+ (entry && entry[:geometry]) || nil
169
+ end
170
+
171
+ def render_menu_item(surface, bounds, item, index)
172
+ item_y = @y + index
173
+ is_selected = (index == @selected_index)
174
+ action = @available_actions[index]
175
+
176
+ # Colors
177
+ bg = is_selected ? TOOLTIP_BG_SELECTED : TOOLTIP_BG_DEFAULT
178
+ fg = is_selected ? TOOLTIP_FG_SELECTED : TOOLTIP_FG_DEFAULT
179
+
180
+ # Background
181
+ surface.write_abs(bounds, item_y, @x, "#{bg}#{' ' * @width}#{Terminal::ANSI::RESET}")
182
+
183
+ # Content with icon
184
+ indicator = is_selected ? '❯' : ' '
185
+ icon = action[:icon] || ' '
186
+ line_text = Shoko::Adapters::Output::Terminal::TextMetrics.pad_right("#{indicator}#{icon} #{item} ", @width)
187
+ surface.write_abs(bounds, item_y, @x, "#{bg}#{fg}#{line_text}#{Terminal::ANSI::RESET}")
188
+ end
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_component'
4
+ require_relative 'surface'
5
+ require_relative '../../terminal/text_metrics.rb'
6
+
7
+ module Shoko
8
+ module Adapters::Output::Ui::Components
9
+ # Renders the bottom status area (page info + transient message).
10
+ class FooterComponent < BaseComponent
11
+ def initialize(view_model_provider = nil)
12
+ super()
13
+ @view_model_provider = view_model_provider
14
+ @cached_view_model = nil
15
+ end
16
+
17
+ def preferred_height(_available_height)
18
+ vm = resolve_view_model
19
+ return 0 unless vm
20
+
21
+ renderable_page_info?(vm) ? 1 : 0
22
+ end
23
+
24
+ def do_render(surface, bounds)
25
+ vm = resolve_view_model
26
+ return unless vm
27
+
28
+ return unless renderable_page_info?(vm)
29
+
30
+ render_page_info(surface, bounds, vm, bounds.height)
31
+ ensure
32
+ @cached_view_model = nil
33
+ end
34
+
35
+ private
36
+
37
+ def resolve_view_model
38
+ return nil unless @view_model_provider
39
+
40
+ @cached_view_model ||= @view_model_provider.call
41
+ rescue StandardError
42
+ nil
43
+ end
44
+
45
+ def renderable_page_info?(view_model)
46
+ disallowed_modes = %i[help]
47
+ return false if view_model.respond_to?(:mode) && disallowed_modes.include?(view_model.mode)
48
+ return false unless view_model.respond_to?(:show_page_numbers) && view_model.show_page_numbers
49
+
50
+ info = view_model.page_info
51
+ info && !info.empty?
52
+ end
53
+
54
+ def render_page_info(surface, bounds, view_model, row)
55
+ ui = Shoko::Adapters::Output::Ui::Constants::UI
56
+ width = bounds.width
57
+ info = view_model.page_info
58
+
59
+ if view_model.view_mode == :split && info[:left]
60
+ render_split_page_info(surface, bounds, info, width, row, ui)
61
+ else
62
+ render_single_page_info(surface, bounds, info, width, row, ui)
63
+ end
64
+ end
65
+
66
+ def render_single_page_info(surface, bounds, info, width, row, ui_constants)
67
+ current = info[:current].to_i
68
+ total = info[:total].to_i
69
+ return if current.zero? && total.zero?
70
+
71
+ label = page_label(current, total)
72
+ col = center_col(width, Shoko::Adapters::Output::Terminal::TextMetrics.visible_length(label))
73
+ write_colored(surface, bounds, row, col, label, ui_constants::COLOR_TEXT_PRIMARY)
74
+ end
75
+
76
+ def render_split_page_info(surface, bounds, info, width, row, ui_constants)
77
+ left = info[:left]
78
+ right = info[:right]
79
+ return unless left
80
+
81
+ left_label = page_label(left[:current].to_i, left[:total].to_i)
82
+ left_col = quarter_center_col(width, Shoko::Adapters::Output::Terminal::TextMetrics.visible_length(left_label), :left)
83
+ unless left_label.empty?
84
+ write_colored(surface, bounds, row, left_col, left_label,
85
+ ui_constants::COLOR_TEXT_PRIMARY)
86
+ end
87
+
88
+ return unless right
89
+
90
+ right_label = page_label(right[:current].to_i, right[:total].to_i)
91
+ right_col = quarter_center_col(width, Shoko::Adapters::Output::Terminal::TextMetrics.visible_length(right_label), :right)
92
+ return if right_label.empty?
93
+
94
+ write_colored(surface, bounds, row, right_col, right_label,
95
+ ui_constants::COLOR_TEXT_PRIMARY)
96
+ end
97
+
98
+ # ----- helpers -----
99
+
100
+ def page_label(current, total)
101
+ total.positive? ? "#{current} / #{total}" : "Page #{current}"
102
+ end
103
+
104
+ def center_col(width, text_len)
105
+ [(width - text_len) / 2, 1].max
106
+ end
107
+
108
+ def quarter_center_col(width, text_len, side)
109
+ half = text_len / 2
110
+ return [(width / 4) - half, 1].max if side == :left
111
+
112
+ [(3 * width / 4) - half, 1].max
113
+ end
114
+
115
+ def write_colored(surface, bounds, row, col, text, color_prefix)
116
+ surface.write(bounds, row, col, color_prefix + text + Terminal::ANSI::RESET)
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_component'
4
+ require_relative 'surface'
5
+ require_relative '../constants/ui_constants'
6
+ require_relative '../../terminal/text_metrics.rb'
7
+
8
+ module Shoko
9
+ module Adapters::Output::Ui::Components
10
+ # Renders the top header row (document title).
11
+ class HeaderComponent < BaseComponent
12
+ include Adapters::Output::Ui::Constants::UI
13
+
14
+ def initialize(view_model_provider = nil, theme = :dark)
15
+ super()
16
+ @view_model_provider = view_model_provider
17
+ @theme = theme
18
+ end
19
+
20
+ def preferred_height(_available_height)
21
+ 1
22
+ end
23
+
24
+ def do_render(surface, bounds)
25
+ return unless @view_model_provider
26
+
27
+ view_model = @view_model_provider.call
28
+ return unless view_model.respond_to?(:document_title)
29
+
30
+ render_centered_title(surface, bounds, view_model.document_title.to_s)
31
+ end
32
+
33
+ private
34
+
35
+ def render_centered_title(surface, bounds, title)
36
+ return if title.empty?
37
+
38
+ reset = Terminal::ANSI::RESET
39
+ width = bounds.width
40
+ title_width = Shoko::Adapters::Output::Terminal::TextMetrics.visible_length(title)
41
+ col = [(width - title_width) / 2, 1].max
42
+ surface.write(bounds, 1, col, "#{COLOR_TEXT_PRIMARY}#{title}#{reset}")
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../base_component'
4
+ require_relative '../rect'
5
+
6
+ module Shoko
7
+ module Adapters::Output::Ui::Components
8
+ module Layouts
9
+ # Horizontal layout that splits children left-to-right
10
+ # Supports collapsible left sidebar with dynamic width allocation
11
+ class Horizontal < BaseComponent
12
+ def initialize(left_child, right_child)
13
+ super(nil)
14
+ @left_child = left_child
15
+ @right_child = right_child
16
+ end
17
+
18
+ def do_render(surface, bounds)
19
+ return unless @left_child && @right_child
20
+
21
+ # Cache frequently used bounds
22
+ x = bounds.x
23
+ y = bounds.y
24
+ w = bounds.width
25
+ h = bounds.height
26
+
27
+ # Calculate widths based on left child's preferred width
28
+ left_width = calculate_left_width(w)
29
+ right_width = w - left_width
30
+
31
+ # Render left child (sidebar)
32
+ if left_width.positive?
33
+ left_bounds = Rect.new(x: x, y: y, width: left_width, height: h)
34
+ @left_child.render(surface, left_bounds)
35
+ end
36
+
37
+ # Render right child (content)
38
+ return unless right_width.positive?
39
+
40
+ right_x = x + left_width
41
+ right_bounds = Rect.new(x: right_x, y: y, width: right_width, height: h)
42
+ @right_child.render(surface, right_bounds)
43
+ end
44
+
45
+ private
46
+
47
+ def calculate_left_width(total_width)
48
+ return 0 unless @left_child.respond_to?(:preferred_width)
49
+
50
+ pref = @left_child.preferred_width(total_width)
51
+ case pref
52
+ when Integer
53
+ [pref, total_width].min
54
+ when :flexible
55
+ total_width / 3 # Default to 1/3 width
56
+ else
57
+ 0
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../base_component'
4
+ require_relative '../rect'
5
+
6
+ module Shoko
7
+ module Adapters::Output::Ui::Components
8
+ module Layouts
9
+ # Simple vertical layout that stacks children top-to-bottom
10
+ # Respects preferred heights; assigns remaining space to first flexible child
11
+ class Vertical < BaseComponent
12
+ def initialize(children)
13
+ super(nil)
14
+ @children = children
15
+ end
16
+
17
+ def do_render(surface, bounds)
18
+ return if @children.nil? || @children.empty?
19
+
20
+ # Calculate heights using new contract
21
+ heights = calculate_child_heights(bounds.height)
22
+
23
+ # Render children
24
+ cursor_y = bounds.y
25
+ @children.each_with_index do |child, i|
26
+ height = heights[i] || 0
27
+ next if height <= 0
28
+
29
+ child_bounds = Rect.new(x: bounds.x, y: cursor_y, width: bounds.width, height: height)
30
+ child.render(surface, child_bounds)
31
+ cursor_y += height
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def calculate_child_heights(total_height)
38
+ heights = []
39
+ remaining = total_height
40
+ fill_children = []
41
+
42
+ # First pass: allocate fixed heights
43
+ @children.each_with_index do |child, i|
44
+ pref = child.respond_to?(:preferred_height) ? child.preferred_height(total_height) : :flexible
45
+
46
+ case pref
47
+ when Integer
48
+ # Fixed height
49
+ height = [pref, remaining].min
50
+ heights[i] = height
51
+ remaining -= height
52
+ when :fill
53
+ # Fill remaining space (calculated in second pass)
54
+ fill_children << i
55
+ heights[i] = nil
56
+ else
57
+ # :flexible and any unknown values default to minimum space
58
+ heights[i] = 0
59
+ end
60
+ end
61
+
62
+ # Second pass: distribute remaining space to fill children
63
+ if fill_children.any?
64
+ target_height = remaining.positive? ? (remaining / fill_children.size) : 0
65
+ fill_children.each { |i| heights[i] = target_height }
66
+ end
67
+
68
+ heights
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end