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,301 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+ require_relative '../adapters/storage/background_worker.rb'
5
+ require_relative '../adapters/storage/atomic_file_writer.rb'
6
+ require_relative '../adapters/monitoring/performance_monitor.rb'
7
+ require_relative '../adapters/monitoring/perf_tracer.rb'
8
+ require_relative '../adapters/storage/pagination_cache.rb'
9
+ require_relative '../adapters/storage/cache_paths.rb'
10
+ require_relative '../adapters/storage/epub_cache.rb'
11
+ require_relative '../adapters/output/kitty/kitty_image_renderer.rb'
12
+ require_relative '../adapters/book_sources/gutendex_client.rb'
13
+ require_relative '../adapters/storage/repositories/cached_library_repository.rb'
14
+ require_relative '../adapters/book_sources/epub/parsers/xhtml_content_parser.rb'
15
+ require_relative '../adapters/output/render_registry.rb'
16
+ require_relative '../core/events/domain_event_bus'
17
+ require_relative '../adapters/storage/file_writer_service'
18
+ require_relative '../adapters/output/instrumentation_service'
19
+ require_relative '../adapters/book_sources/download_service'
20
+
21
+ module Shoko
22
+ module Application
23
+ # Dependency injection container for managing service dependencies.
24
+ # Replaces the broken ServiceRegistry with proper lifecycle management.
25
+ class DependencyContainer
26
+ class DependencyError < StandardError; end
27
+ class CircularDependencyError < DependencyError; end
28
+
29
+ def initialize
30
+ @services = {}
31
+ @factories = {}
32
+ @singletons = {}
33
+ @resolving = Set.new
34
+ end
35
+
36
+ # Register a singleton service
37
+ #
38
+ # @param name [Symbol] Service name
39
+ # @param service [Object] Service instance
40
+ def register(name, service)
41
+ @services[name] = service
42
+ end
43
+
44
+ # Register a factory for lazy instantiation
45
+ #
46
+ # @param name [Symbol] Service name
47
+ # @param factory [Proc] Factory proc that creates the service
48
+ def register_factory(name, &factory)
49
+ @factories[name] = factory
50
+ end
51
+
52
+ # Register a singleton factory
53
+ #
54
+ # @param name [Symbol] Service name
55
+ # @param factory [Proc] Factory proc
56
+ def register_singleton(name, &factory)
57
+ @singletons[name] = factory
58
+ end
59
+
60
+ # Resolve a service by name
61
+ #
62
+ # @param name [Symbol] Service name
63
+ # @return [Object] Service instance
64
+ def resolve(name)
65
+ return @services[name] if @services.key?(name)
66
+
67
+ detect_circular_dependency(name) do
68
+ resolve_from_factories(name)
69
+ end
70
+ end
71
+
72
+ # Resolve multiple services
73
+ #
74
+ # @param names [Array<Symbol>] Service names
75
+ # @return [Hash<Symbol, Object>] Hash of name => service
76
+ def resolve_many(*names)
77
+ names.to_h { |name| [name, resolve(name)] }
78
+ end
79
+
80
+ # Check if service is registered
81
+ #
82
+ # @param name [Symbol] Service name
83
+ # @return [Boolean]
84
+ def registered?(name)
85
+ @services.key?(name) || @factories.key?(name) || @singletons.key?(name)
86
+ end
87
+
88
+ # List all registered service names
89
+ #
90
+ # @return [Array<Symbol>]
91
+ def service_names
92
+ (@services.keys + @factories.keys + @singletons.keys).uniq
93
+ end
94
+
95
+ # Create child container with inherited services
96
+ #
97
+ # @return [DependencyContainer]
98
+ def create_child
99
+ child = self.class.new
100
+ child.instance_variable_set(:@services, @services.dup)
101
+ child.instance_variable_set(:@factories, @factories.dup)
102
+ child.instance_variable_set(:@singletons, @singletons.dup)
103
+ child
104
+ end
105
+
106
+ # Clear all registrations (for testing)
107
+ def clear!
108
+ @services.clear
109
+ @factories.clear
110
+ @singletons.clear
111
+ end
112
+
113
+ private
114
+
115
+ def resolve_from_factories(name)
116
+ if @singletons.key?(name)
117
+ @services[name] ||= @singletons[name].call(self)
118
+ elsif @factories.key?(name)
119
+ @factories[name].call(self)
120
+ else
121
+ raise DependencyError, "Service '#{name}' not registered"
122
+ end
123
+ end
124
+
125
+ def detect_circular_dependency(name)
126
+ if @resolving.include?(name)
127
+ raise CircularDependencyError,
128
+ "Circular dependency detected for '#{name}'"
129
+ end
130
+
131
+ @resolving.add(name)
132
+ begin
133
+ yield
134
+ ensure
135
+ @resolving.delete(name)
136
+ end
137
+ end
138
+ end
139
+
140
+ # Factory methods for common service configurations
141
+ module ContainerFactory
142
+ def self.create_default_container
143
+ container = DependencyContainer.new
144
+
145
+ # Infrastructure services
146
+ container.register_singleton(:event_bus) { Application::Infrastructure::EventBus.new }
147
+ container.register_singleton(:logger) { Adapters::Monitoring::Logger }
148
+ container.register(:performance_monitor, Adapters::Monitoring::PerformanceMonitor)
149
+ container.register(:perf_tracer, Adapters::Monitoring::PerfTracer)
150
+ container.register(:pagination_cache, Adapters::Storage::PaginationCache)
151
+ container.register(:cache_paths, Adapters::Storage::CachePaths)
152
+ container.register(:atomic_file_writer, Adapters::Storage::AtomicFileWriter)
153
+ container.register(:epub_cache_factory, ->(path) { Adapters::Storage::EpubCache.new(path) })
154
+ container.register(:epub_cache_predicate, ->(path) { Adapters::Storage::EpubCache.cache_file?(path) })
155
+ container.register_singleton(:gutendex_client) do |c|
156
+ Adapters::BookSources::GutendexClient.new(logger: c.resolve(:logger))
157
+ end
158
+ container.register(:background_worker_factory,
159
+ lambda do |name: 'shoko-worker'|
160
+ Adapters::Storage::BackgroundWorker.new(name:)
161
+ end)
162
+ container.register(:xhtml_parser_factory,
163
+ lambda do |raw|
164
+ Adapters::BookSources::Epub::Parsers::XHTMLContentParser.new(raw)
165
+ end)
166
+
167
+ # Domain event bus (eagerly capture event_bus to avoid repeated resolves)
168
+ eb = container.resolve(:event_bus)
169
+ container.register_singleton(:domain_event_bus) do |_c|
170
+ Core::Events::DomainEventBus.new(eb)
171
+ end
172
+
173
+ # Repository implementations (infrastructure)
174
+ container.register_factory(:bookmark_repository) { |c| Adapters::Storage::Repositories::BookmarkRepository.new(c) }
175
+ container.register_factory(:annotation_repository) { |c| Adapters::Storage::Repositories::AnnotationRepository.new(c) }
176
+ container.register_factory(:progress_repository) { |c| Adapters::Storage::Repositories::ProgressRepository.new(c) }
177
+ container.register_factory(:config_repository) { |c| Adapters::Storage::Repositories::ConfigRepository.new(c) }
178
+ # Domain services with dependency injection
179
+ container.register_factory(:navigation_service) { |c| Core::Services::NavigationService.new(c) }
180
+ container.register_factory(:bookmark_service) { |c| Core::Services::BookmarkService.new(c) }
181
+ container.register_singleton(:page_calculator) { |c| Core::Services::PageCalculatorService.new(c) }
182
+ container.register_factory(:coordinate_service) { |c| Core::Services::CoordinateService.new(c) }
183
+ container.register_factory(:selection_service) { |c| Core::Services::SelectionService.new(c) }
184
+ container.register_factory(:layout_service) { |c| Core::Services::LayoutService.new(c) }
185
+ container.register_factory(:clipboard_service) { |c| Adapters::Output::Clipboard::ClipboardService.new(c) }
186
+ # TerminalService wraps a global Terminal; use a singleton to keep lifecycle consistent
187
+ container.register_singleton(:terminal_service) { |c| Adapters::Output::Terminal::TerminalService.new(c) }
188
+ container.register_factory(:annotation_service) { |c| Core::Services::AnnotationService.new(c) }
189
+ container.register_factory(:catalog_service) { |c| Application::UseCases::CatalogService.new(c) }
190
+ container.register_factory(:download_service) { |c| Adapters::BookSources::DownloadService.new(c) }
191
+ # WrappingService caches windows/chapters; make it a singleton to share cache
192
+ container.register_singleton(:wrapping_service) { |c| Adapters::Output::Formatting::WrappingService.new(c) }
193
+ container.register_singleton(:formatting_service) { |c| Adapters::Output::Formatting::FormattingService.new(c) }
194
+ container.register_factory(:settings_service) { |c| Application::UseCases::SettingsService.new(c) }
195
+ container.register_singleton(:kitty_image_renderer) { |_c| Adapters::Output::Kitty::KittyImageRenderer.new }
196
+
197
+ container.register_singleton(:file_writer) { |c| Adapters::Storage::FileWriterService.new(c) }
198
+ container.register_singleton(:instrumentation_service) { |c| Adapters::Output::InstrumentationService.new(c) }
199
+
200
+ container.register_factory(:pagination_cache_preloader) do |c|
201
+ Shoko::Core::Services::Pagination::PaginationCachePreloader.new(
202
+ state: c.resolve(:global_state),
203
+ page_calculator: c.resolve(:page_calculator),
204
+ pagination_cache: c.resolve(:pagination_cache)
205
+ )
206
+ end
207
+
208
+ # Notifications
209
+ container.register_singleton(:notification_service) { |c| Adapters::Output::NotificationService.new(c) }
210
+
211
+ # Document service factory (per-book instance)
212
+ container.register_factory(:document_service_factory) do |c|
213
+ lambda do |path, progress_reporter: nil|
214
+ wrapper = c.resolve(:wrapping_service)
215
+ formatting = c.resolve(:formatting_service)
216
+ worker = c.registered?(:background_worker) ? c.resolve(:background_worker) : nil
217
+ klass = Adapters::BookSources::DocumentService
218
+ instantiate_document_service(klass, path, wrapper, formatting, worker, progress_reporter)
219
+ end
220
+ end
221
+
222
+ # Render registry keeps large per-frame geometry out of state store
223
+ container.register_singleton(:render_registry) { |_c| Adapters::Output::RenderRegistry.current }
224
+
225
+ # Focused controllers replacing god class
226
+
227
+ # Unified state management
228
+ container.register_singleton(:global_state) { |_c| Application::Infrastructure::ObserverStateStore.new(eb) }
229
+
230
+ # IMPORTANT: state_store must resolve to the same ObserverStateStore instance as :global_state
231
+ container.register_factory(:state_store) { |c| c.resolve(:global_state) }
232
+
233
+ # Library scanner service (infrastructure)
234
+ container.register_singleton(:cached_library_repository) do |_c|
235
+ Shoko::Adapters::Storage::Repositories::CachedLibraryRepository.new
236
+ end
237
+
238
+ container.register_factory(:library_scanner) do |_c|
239
+ Shoko::Adapters::BookSources::LibraryScanner.new
240
+ end
241
+
242
+ if defined?(Shoko::TestSupport::TestMode)
243
+ Shoko::TestSupport::TestMode.configure_container(container)
244
+ end
245
+
246
+ container
247
+ end
248
+
249
+ def self.create_test_container
250
+ require 'rspec/mocks'
251
+
252
+ container = DependencyContainer.new
253
+
254
+ # Mock services for testing
255
+ container.register(:event_bus,
256
+ RSpec::Mocks::Double.new('EventBus', subscribe: nil, emit_event: nil))
257
+ container.register(:state_store,
258
+ RSpec::Mocks::Double.new('StateStore', get: nil, set: nil,
259
+ current_state: {}))
260
+ container.register(:logger,
261
+ RSpec::Mocks::Double.new('Logger', info: nil, error: nil, debug: nil))
262
+ container.register(:atomic_file_writer, Adapters::Storage::AtomicFileWriter)
263
+ container.register(:cache_paths, Adapters::Storage::CachePaths)
264
+ container.register(:epub_cache_factory, ->(path) { Adapters::Storage::EpubCache.new(path) })
265
+ container.register(:epub_cache_predicate, ->(path) { Adapters::Storage::EpubCache.cache_file?(path) })
266
+ container.register(:file_writer, Adapters::Storage::FileWriterService.new(container))
267
+ container.register(:instrumentation_service, Adapters::Output::InstrumentationService.new(container))
268
+
269
+ # Provide a domain event bus backed by the mocked infrastructure bus
270
+ container.register(:domain_event_bus,
271
+ Core::Events::DomainEventBus.new(container.resolve(:event_bus)))
272
+
273
+ if defined?(Shoko::TestSupport::TestMode)
274
+ Shoko::TestSupport::TestMode.configure_container(container)
275
+ end
276
+
277
+ container
278
+ end
279
+
280
+ def self.instantiate_document_service(klass, path, wrapper, formatting, worker, progress_reporter = nil)
281
+ klass.new(
282
+ path,
283
+ wrapper,
284
+ formatting_service: formatting,
285
+ background_worker: worker,
286
+ progress_reporter: progress_reporter
287
+ )
288
+ rescue ArgumentError
289
+ begin
290
+ klass.new(path, wrapper, formatting_service: formatting, progress_reporter: progress_reporter)
291
+ rescue ArgumentError
292
+ begin
293
+ klass.new(path, wrapper, progress_reporter: progress_reporter)
294
+ rescue ArgumentError
295
+ klass.new(path)
296
+ end
297
+ end
298
+ end
299
+ end
300
+ end
301
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Application::Infrastructure
5
+ # Thread-safe event bus for application-wide event handling.
6
+ # Complements ObserverStateStore by broadcasting application events.
7
+ class EventBus
8
+ def initialize
9
+ @subscribers = Hash.new { |h, k| h[k] = [] }
10
+ @mutex = Mutex.new
11
+ end
12
+
13
+ # Subscribe to specific event types
14
+ #
15
+ # @param subscriber [Object] Object responding to #handle_event(event)
16
+ # @param *event_types [Array<Symbol>] Event types to subscribe to
17
+ def subscribe(subscriber, *event_types)
18
+ @mutex.synchronize do
19
+ event_types.each do |event_type|
20
+ list = @subscribers[event_type]
21
+ list << subscriber unless list.include?(subscriber)
22
+ end
23
+ end
24
+ end
25
+
26
+ # Unsubscribe from all events
27
+ #
28
+ # @param subscriber [Object] Subscriber to remove
29
+ def unsubscribe(subscriber)
30
+ @mutex.synchronize do
31
+ @subscribers.each_value { |list| list.delete(subscriber) }
32
+ end
33
+ end
34
+
35
+ # Emit an event to all subscribers
36
+ #
37
+ # @param event [Event] Event to emit
38
+ def emit(event)
39
+ subscribers = @mutex.synchronize { @subscribers[event.type].dup }
40
+
41
+ subscribers.each do |subscriber|
42
+ safely_notify(subscriber, event)
43
+ end
44
+ end
45
+
46
+ # Create and emit an event
47
+ #
48
+ # @param type [Symbol] Event type
49
+ # @param data [Hash] Event data
50
+ def emit_event(type, data = {})
51
+ event = Event.new(type: type, data: data, timestamp: Time.now)
52
+ emit(event)
53
+ end
54
+
55
+ private
56
+
57
+ def safely_notify(subscriber, event)
58
+ subscriber.handle_event(event)
59
+ rescue StandardError => e
60
+ Adapters::Monitoring::Logger.error(
61
+ 'Event subscriber error',
62
+ subscriber: subscriber.class.name,
63
+ event_type: event.type,
64
+ error: e.message
65
+ )
66
+ # Log errors from subscribers for visibility during development and tests
67
+ # Only re-raise in tests if explicitly expected
68
+ raise e if defined?(RSpec) && !Thread.current[:suppress_event_errors]
69
+ end
70
+ end
71
+
72
+ # Immutable event object
73
+ Event = Struct.new(:type, :data, :timestamp, keyword_init: true) do
74
+ def initialize(**args)
75
+ super
76
+ freeze
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'state_store'
4
+
5
+ module Shoko
6
+ module Application::Infrastructure
7
+ # StateStore with observer pattern support; central app state with observer notifications
8
+ class ObserverStateStore < StateStore
9
+ def initialize(event_bus = EventBus.new)
10
+ super
11
+ @observers_by_path = Hash.new { |h, k| h[k] = [] }
12
+ @observers_all = []
13
+ config_file = self.class.config_file
14
+ config_missing = !File.exist?(config_file)
15
+ load_config_from_file
16
+ save_config if config_missing && respond_to?(:save_config)
17
+ end
18
+
19
+ # Register an observer for specific state paths
20
+ # Observer should respond to `state_changed(path, old_value, new_value)`
21
+ #
22
+ # @param observer [Object] Object implementing state_changed method
23
+ # @param *paths [Array<Symbol|Array>] State paths to observe
24
+ def add_observer(observer, *paths)
25
+ if paths.empty?
26
+ @observers_all << observer unless @observers_all.include?(observer)
27
+ else
28
+ paths.each do |path|
29
+ normalized_path = normalize_path(path)
30
+ unless @observers_by_path[normalized_path].include?(observer)
31
+ @observers_by_path[normalized_path] << observer
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ # Remove observer from all paths
38
+ #
39
+ # @param observer [Object] Observer to remove
40
+ def remove_observer(observer)
41
+ @observers_all.delete(observer)
42
+ @observers_by_path.each_value { |list| list.delete(observer) }
43
+ end
44
+
45
+ # Override update to include observer notifications
46
+ # Supports both update(path, value) and update({path => value}) formats for compatibility
47
+ def update(path_or_updates, value = nil)
48
+ if value.nil?
49
+ # New format: update({path => value, path2 => value2})
50
+ updates = path_or_updates
51
+ old_state = current_state
52
+ super(updates)
53
+ notify_observers_for_updates(old_state, updates)
54
+ else
55
+ # Legacy format: update(path, value)
56
+ path = path_or_updates
57
+ normalized_path = normalize_path(path)
58
+ old_value = get(normalized_path)
59
+ super({ normalized_path => value })
60
+ notify_observers(normalized_path, old_value, value) unless old_value == value
61
+ end
62
+ end
63
+
64
+ # Override set to include observer notifications
65
+ def set(path, value)
66
+ old_value = get(path)
67
+ super
68
+ return value if old_value == value
69
+
70
+ normalized_path = normalize_path(path)
71
+ notify_observers(normalized_path, old_value, value)
72
+ value
73
+ end
74
+
75
+ private
76
+
77
+ def notify_observers_for_updates(old_state, updates)
78
+ updates.each do |path, new_value|
79
+ arr = Array(path)
80
+ old_value = get_nested_value(old_state, arr)
81
+ next if old_value == new_value
82
+
83
+ normalized_path = normalize_path(arr)
84
+ notify_observers(normalized_path, old_value, new_value)
85
+ end
86
+ end
87
+
88
+ def notify_observers(path, old_value, new_value)
89
+ # Notify path-specific observers
90
+ @observers_by_path[path].each do |observer|
91
+ safe_notify(observer, path, old_value, new_value)
92
+ end
93
+
94
+ # Notify observers watching parent paths
95
+ notify_parent_path_observers(path, old_value, new_value)
96
+
97
+ # Notify global observers
98
+ @observers_all.each do |observer|
99
+ safe_notify(observer, path, old_value, new_value)
100
+ end
101
+ end
102
+
103
+ # Notify observers watching parent paths (e.g., [:reader] when [:reader, :mode] changes)
104
+ def notify_parent_path_observers(path, old_value, new_value)
105
+ len = path.length
106
+ return if len <= 1
107
+
108
+ (1...len).each do |i|
109
+ parent_path = path[0, i]
110
+ @observers_by_path[parent_path].each do |observer|
111
+ safe_notify(observer, path, old_value, new_value)
112
+ end
113
+ end
114
+ end
115
+
116
+ # Safely notify observer, catching any exceptions
117
+ def safe_notify(observer, path, old_value, new_value)
118
+ return unless observer.respond_to?(:state_changed)
119
+
120
+ observer.state_changed(path, old_value, new_value)
121
+ rescue StandardError
122
+ # Silently ignore observer errors to prevent breaking application flow
123
+ nil
124
+ end
125
+
126
+ # Normalize path to array format
127
+ def normalize_path(path)
128
+ case path
129
+ when Array then path
130
+ when Symbol then [path]
131
+ else [path.to_sym]
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end