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,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_service'
4
+ require_relative '../models/selection_anchor'
5
+ require_relative '../../application/selectors/reader_selectors'
6
+
7
+ module Shoko
8
+ module Core
9
+ module Services
10
+ # Service to normalize selection ranges and extract text from rendered_lines
11
+ # Centralizes logic used by UIController and MouseableReader
12
+ class SelectionService < BaseService
13
+ # Convenience helper: extract selection text directly from state.
14
+ # If selection_range is nil, uses state[:reader][:selection].
15
+ def extract_from_state(state, selection_range = nil)
16
+ return '' unless state
17
+
18
+ range = selection_range || state.get(%i[reader selection])
19
+ rendered = Shoko::Application::Selectors::ReaderSelectors.rendered_lines(state)
20
+ extract_text(range, rendered)
21
+ end
22
+
23
+ # Extract selected text from selection_range using rendered_lines in state
24
+ # @param selection_range [Hash] {:start=>{x:,y:}, :end=>{x:,y:}}
25
+ # @param rendered_lines [Hash<Integer, Hash>] mapping of line_id => {row:, col:, col_end:, width:, text:}
26
+ # @return [String]
27
+ def extract_text(selection_range, rendered_lines)
28
+ return '' unless selection_range && rendered_lines && !rendered_lines.empty?
29
+
30
+ coordinate_service = resolve(:coordinate_service)
31
+ normalized = coordinate_service.normalize_selection_range(selection_range, rendered_lines)
32
+ return '' unless normalized
33
+
34
+ start_anchor = Shoko::Core::Models::SelectionAnchor.from(normalized[:start])
35
+ end_anchor = Shoko::Core::Models::SelectionAnchor.from(normalized[:end])
36
+ return '' unless start_anchor && end_anchor
37
+
38
+ geometry_index = build_geometry_index(rendered_lines)
39
+ return '' if geometry_index.empty?
40
+
41
+ ordered = order_geometry(geometry_index.values)
42
+ start_idx = ordered.find_index { |geo| geo.key == start_anchor.geometry_key }
43
+ end_idx = ordered.find_index { |geo| geo.key == end_anchor.geometry_key }
44
+ return '' unless start_idx && end_idx
45
+
46
+ text_lines = []
47
+
48
+ ordered[start_idx..end_idx].each do |geometry|
49
+ start_cell = geometry.key == start_anchor.geometry_key ? start_anchor.cell_index : 0
50
+ end_cell = geometry.key == end_anchor.geometry_key ? end_anchor.cell_index : geometry.cells.length
51
+
52
+ next if end_cell < start_cell
53
+
54
+ start_char = char_index_for_cell(geometry, start_cell)
55
+ end_char = char_index_for_cell(geometry, end_cell)
56
+ segment = geometry.plain_text[start_char...end_char]
57
+ next if segment.nil?
58
+
59
+ text_lines << segment
60
+ end
61
+
62
+ text_lines.join("\n")
63
+ end
64
+
65
+ # Normalize a selection range using the coordinate service and rendered_lines
66
+ #
67
+ # @param state [Application::Infrastructure::ObserverStateStore]
68
+ # @param selection_range [Hash]
69
+ # @return [Hash, nil] normalized range or nil when normalization fails
70
+ def normalize_range(state, selection_range)
71
+ return nil unless selection_range
72
+ return selection_range if anchor_range?(selection_range)
73
+
74
+ coordinate_service = resolve(:coordinate_service)
75
+ rendered = Shoko::Application::Selectors::ReaderSelectors.rendered_lines(state)
76
+ coordinate_service.normalize_selection_range(selection_range, rendered)
77
+ rescue StandardError
78
+ nil
79
+ end
80
+
81
+ protected
82
+
83
+ def required_dependencies
84
+ [:coordinate_service]
85
+ end
86
+
87
+ private
88
+
89
+ def anchor_range?(range)
90
+ return false unless range.is_a?(Hash)
91
+
92
+ start_anchor = range[:start] || range['start']
93
+ start_anchor.is_a?(Hash) && (start_anchor.key?(:geometry_key) || start_anchor.key?('geometry_key'))
94
+ end
95
+
96
+ def build_geometry_index(rendered_lines)
97
+ rendered_lines.each_with_object({}) do |(_key, info), acc|
98
+ geometry = info[:geometry]
99
+ next unless geometry
100
+
101
+ acc[geometry.key] = geometry
102
+ end
103
+ end
104
+
105
+ def order_geometry(geometries)
106
+ geometries.sort_by do |geo|
107
+ [geo.page_id || 0, geo.line_offset || 0, geo.column_id || 0, geo.row || 0, geo.column_origin || 0]
108
+ end
109
+ end
110
+
111
+ def char_index_for_cell(geometry, cell_index)
112
+ cells = geometry.cells
113
+ return 0 if cells.empty?
114
+
115
+ if cell_index <= 0
116
+ 0
117
+ elsif cell_index >= cells.length
118
+ geometry.plain_text.length
119
+ else
120
+ cells[cell_index].char_start
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Core
5
+ # Base validator class providing common validation patterns.
6
+ # Subclasses should implement specific validation logic.
7
+ #
8
+ # @abstract
9
+ class Validator
10
+ # Validation errors collection
11
+ attr_reader :errors
12
+
13
+ def initialize
14
+ @errors = []
15
+ end
16
+
17
+ # Check if validation passed
18
+ #
19
+ # @return [Boolean] true if no errors
20
+ def valid?
21
+ @errors.empty?
22
+ end
23
+
24
+ # Add an error message
25
+ #
26
+ # @param field [Symbol] Field name
27
+ # @param message [String] Error message
28
+ def add_error(field, message)
29
+ @errors << { field:, message: }
30
+ end
31
+
32
+ # Clear all errors
33
+ def clear_errors
34
+ @errors = []
35
+ end
36
+
37
+ # Validate presence of a value
38
+ #
39
+ # @param value [Object] Value to check
40
+ # @param field [Symbol] Field name for error reporting
41
+ # @param message [String] Custom error message
42
+ # @return [Boolean] Validation result
43
+ def presence_valid?(value, field, message = "can't be blank")
44
+ return true if value && !value.to_s.strip.empty?
45
+
46
+ add_error(field, message)
47
+ false
48
+ end
49
+
50
+ RangeValidationContext = Struct.new(:value, :range, :field, :message)
51
+ FormatValidationContext = Struct.new(:value, :pattern, :field, :message)
52
+
53
+ # Validate that a value falls within a range
54
+ #
55
+ # @param context [RangeValidationContext] Validation parameters
56
+ # @return [Boolean] Validation result
57
+ def range_valid?(context)
58
+ return true if context.range.include?(context.value)
59
+
60
+ add_error(context.field, context.message || 'must be between')
61
+ false
62
+ end
63
+
64
+ # Validate that a value matches a regular expression pattern
65
+ #
66
+ # @param context [FormatValidationContext] Validation parameters
67
+ # @return [Boolean] Validation result
68
+ def format_valid?(context)
69
+ return true if context.value&.match?(context.pattern)
70
+
71
+ add_error(context.field, context.message || 'has invalid format')
72
+ false
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ # Base error class for Shoko
5
+ class Error < StandardError; end
6
+
7
+ # Raised when EPUB file cannot be parsed
8
+ class EPUBParseError < Error
9
+ attr_reader :file_path
10
+
11
+ def initialize(message, file_path)
12
+ super("Failed to parse EPUB at #{file_path}: #{message}")
13
+ @file_path = file_path
14
+ end
15
+ end
16
+
17
+ # Raised when required file is not found
18
+ class FileNotFoundError < Error
19
+ attr_reader :file_path
20
+
21
+ def initialize(file_path)
22
+ super("File not found: #{file_path}")
23
+ @file_path = file_path
24
+ end
25
+ end
26
+
27
+ # Raised when configuration is invalid
28
+ class ConfigurationError < Error; end
29
+
30
+ # Raised when no interactive terminal is available
31
+ class TerminalUnavailableError < Error
32
+ def initialize
33
+ super('Interactive terminal not available')
34
+ end
35
+ end
36
+
37
+ # Raised when reader state is invalid
38
+ class InvalidStateError < Error
39
+ attr_reader :state
40
+
41
+ def initialize(message, state)
42
+ super("Invalid reader state: #{message}")
43
+ @state = state
44
+ end
45
+ end
46
+
47
+ # Raised when navigation is not possible
48
+ class NavigationError < Error
49
+ attr_reader :direction, :reason
50
+
51
+ def initialize(direction, reason)
52
+ super("Cannot navigate #{direction}: #{reason}")
53
+ @direction = direction
54
+ @reason = reason
55
+ end
56
+ end
57
+
58
+ # Raised when bookmark operation fails
59
+ class BookmarkError < Error
60
+ attr_reader :operation
61
+
62
+ def initialize(operation, message)
63
+ super("Bookmark #{operation} failed: #{message}")
64
+ @operation = operation
65
+ end
66
+ end
67
+
68
+ # Raised when rendering fails
69
+ class RenderError < Error
70
+ attr_reader :component
71
+
72
+ def initialize(component, message)
73
+ super("Rendering failed in #{component}: #{message}")
74
+ @component = component
75
+ end
76
+ end
77
+
78
+ # Raised when content normalization produces no semantic blocks
79
+ class FormattingError < Error
80
+ attr_reader :source
81
+
82
+ def initialize(source, message)
83
+ super("Formatting failed for #{source}: #{message}")
84
+ @source = source
85
+ end
86
+ end
87
+
88
+ # Raised when cached book data cannot be loaded or is incompatible.
89
+ class CacheLoadError < Error
90
+ attr_reader :path
91
+
92
+ def initialize(path, message = 'Cache is corrupt or incompatible')
93
+ super("Cache load failed for #{path}: #{message}")
94
+ @path = path
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ # Test-only helpers and doubles for terminal interactions.
5
+ module TestSupport
6
+ # Lightweight replacement for the production Terminal facade that keeps
7
+ # tests deterministic and side-effect free. All terminal operations become
8
+ # in-memory recordings so specs can make assertions without touching an
9
+ # actual TTY.
10
+ class TerminalDouble
11
+ ANSI = Shoko::Adapters::Output::Terminal::TerminalOutput::ANSI
12
+
13
+ class << self
14
+ attr_accessor :default_height, :default_width
15
+
16
+ def reset!
17
+ @writes = []
18
+ @printed = []
19
+ @moves = []
20
+ @clears = []
21
+ @mouse_events = []
22
+ @input_queue = Queue.new
23
+ @default_height = 24
24
+ @default_width = 80
25
+ end
26
+
27
+ def writes
28
+ @writes ||= []
29
+ end
30
+
31
+ def printed
32
+ @printed ||= []
33
+ end
34
+
35
+ def moves
36
+ @moves ||= []
37
+ end
38
+
39
+ def clears
40
+ @clears ||= []
41
+ end
42
+
43
+ def mouse_events
44
+ @mouse_events ||= []
45
+ end
46
+
47
+ def push_input(*keys)
48
+ ensure_input_queue
49
+ keys.flatten.each { |k| @input_queue << k }
50
+ end
51
+
52
+ def drain_input
53
+ ensure_input_queue
54
+ drained = []
55
+ loop do
56
+ drained << @input_queue.pop(true)
57
+ rescue ThreadError
58
+ break
59
+ end
60
+ drained
61
+ end
62
+
63
+ def size=(tuple)
64
+ @default_height, @default_width = tuple
65
+ end
66
+
67
+ def size
68
+ [@default_height || 24, @default_width || 80]
69
+ end
70
+
71
+ def setup
72
+ @setup_calls = setup_calls + 1
73
+ end
74
+
75
+ def cleanup
76
+ @cleanup_calls = cleanup_calls + 1
77
+ end
78
+
79
+ def setup_calls
80
+ @setup_calls ||= 0
81
+ end
82
+
83
+ def cleanup_calls
84
+ @cleanup_calls ||= 0
85
+ end
86
+
87
+ def start_frame(**_kwargs)
88
+ # no-op, but record for assertions if needed
89
+ @frame_started = true
90
+ end
91
+
92
+ def end_frame
93
+ @frame_started = false
94
+ end
95
+
96
+ def frame_started?
97
+ !!@frame_started
98
+ end
99
+
100
+ def clear
101
+ clears << :clear
102
+ end
103
+
104
+ def move(row, col)
105
+ moves << [row, col]
106
+ end
107
+
108
+ def write(row, col, text)
109
+ writes << { row:, col:, text: text.to_s }
110
+ end
111
+
112
+ def write_differential(row, col, text)
113
+ write(row, col, text)
114
+ end
115
+
116
+ def clear_buffer_cache
117
+ # Nothing cached in the double; keep interface parity.
118
+ end
119
+
120
+ def batch_write
121
+ yield if block_given?
122
+ end
123
+
124
+ def print(str)
125
+ printed << str.to_s
126
+ end
127
+
128
+ def flush
129
+ # no-op
130
+ end
131
+
132
+ def enable_mouse
133
+ mouse_events << :enabled
134
+ end
135
+
136
+ def disable_mouse
137
+ mouse_events << :disabled
138
+ end
139
+
140
+ def read_key
141
+ pop_key(non_block: true)
142
+ end
143
+
144
+ def read_key_blocking(timeout: nil)
145
+ pop_key(timeout: timeout || 0.1)
146
+ end
147
+
148
+ def read_input_with_mouse(timeout: nil)
149
+ read_key_blocking(timeout: timeout)
150
+ end
151
+
152
+ private
153
+
154
+ def ensure_input_queue
155
+ @ensure_input_queue ||= Queue.new
156
+ end
157
+
158
+ def pop_key(non_block: false, timeout: nil)
159
+ ensure_input_queue
160
+ if non_block
161
+ @input_queue.pop(true)
162
+ elsif timeout
163
+ @input_queue.pop(timeout: timeout)
164
+ else
165
+ @input_queue.pop
166
+ end
167
+ rescue ThreadError
168
+ nil
169
+ end
170
+ end
171
+ end
172
+
173
+ TerminalDouble.reset!
174
+ end
175
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'terminal_double'
4
+
5
+ module Shoko
6
+ module TestSupport
7
+ # Central hook that enables deterministic test behaviour by swapping in
8
+ # lightweight adapters and updating the dependency container.
9
+ module TestMode
10
+ module_function
11
+
12
+ def active?
13
+ primary = ENV.fetch('SHOKO_TEST_MODE', '').to_s.strip
14
+ primary == '1'
15
+ end
16
+
17
+ def activate!
18
+ return unless active?
19
+
20
+ install_terminal_double!
21
+ silence_logger!
22
+ end
23
+
24
+ def configure_container(container)
25
+ return unless active?
26
+
27
+ container.register_singleton(:terminal_service) do |c|
28
+ TestTerminalService.new(c)
29
+ end
30
+ end
31
+
32
+ # Test-specialised terminal service that exposes helpers for enqueuing
33
+ # deterministic input sequences while delegating behaviour to the base
34
+ # implementation (now backed by the TerminalDouble).
35
+ class TestTerminalService < Shoko::Adapters::Output::Terminal::TerminalService
36
+ def queue_input(*keys)
37
+ Shoko::TestSupport::TerminalDouble.push_input(*keys)
38
+ end
39
+
40
+ def drain_input
41
+ Shoko::TestSupport::TerminalDouble.drain_input
42
+ end
43
+
44
+ def configure_size(height:, width:)
45
+ Shoko::TestSupport::TerminalDouble.size = [height, width]
46
+ end
47
+ end
48
+
49
+ def install_terminal_double!
50
+ return if @terminal_installed
51
+
52
+ real_terminal = (Shoko.const_get(:Terminal) if Shoko.const_defined?(:Terminal, false))
53
+ Shoko.const_set(:RealTerminal, real_terminal) unless Shoko.const_defined?(:RealTerminal)
54
+ Shoko.send(:remove_const, :Terminal) if Shoko.const_defined?(:Terminal, false)
55
+ Shoko.const_set(:Terminal, Shoko::TestSupport::TerminalDouble)
56
+ Shoko::TestSupport::TerminalDouble.reset!
57
+ @terminal_installed = true
58
+ end
59
+
60
+ def silence_logger!
61
+ return unless defined?(Shoko::Adapters::Monitoring::Logger)
62
+
63
+ begin
64
+ Shoko::Adapters::Monitoring::Logger.output = logger_null_io
65
+ Shoko::Adapters::Monitoring::Logger.level = :fatal
66
+ rescue StandardError
67
+ nil
68
+ end
69
+ end
70
+
71
+ def logger_null_io
72
+ @null_logger_io ||= File.open(File::NULL, 'w')
73
+ rescue StandardError
74
+ nil
75
+ end
76
+ end
77
+ end
78
+ end