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,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_domain_event'
4
+
5
+ module Shoko
6
+ module Core
7
+ module Events
8
+ # Domain event for annotation creation
9
+ class AnnotationAdded < BaseDomainEvent
10
+ required_attributes :book_path, :annotation
11
+ typed_attributes book_path: String
12
+
13
+ def initialize(book_path:, annotation:, **)
14
+ super(
15
+ aggregate_id: book_path,
16
+ book_path: book_path,
17
+ annotation: annotation,
18
+ **
19
+ )
20
+ end
21
+
22
+ def book_path
23
+ get_attribute(:book_path)
24
+ end
25
+
26
+ def annotation
27
+ get_attribute(:annotation)
28
+ end
29
+ end
30
+
31
+ # Domain event for annotation updates
32
+ class AnnotationUpdated < BaseDomainEvent
33
+ required_attributes :book_path, :annotation_id, :old_note, :new_note
34
+ typed_attributes book_path: String, annotation_id: String, old_note: String,
35
+ new_note: String
36
+
37
+ def initialize(book_path:, annotation_id:, old_note:, new_note:, **)
38
+ super(
39
+ aggregate_id: book_path,
40
+ book_path: book_path,
41
+ annotation_id: annotation_id,
42
+ old_note: old_note,
43
+ new_note: new_note,
44
+ **
45
+ )
46
+ end
47
+
48
+ def book_path
49
+ get_attribute(:book_path)
50
+ end
51
+
52
+ def annotation_id
53
+ get_attribute(:annotation_id)
54
+ end
55
+
56
+ def old_note
57
+ get_attribute(:old_note)
58
+ end
59
+
60
+ def new_note
61
+ get_attribute(:new_note)
62
+ end
63
+ end
64
+
65
+ # Domain event for annotation removal
66
+ class AnnotationRemoved < BaseDomainEvent
67
+ required_attributes :book_path, :annotation_id
68
+ typed_attributes book_path: String, annotation_id: String
69
+
70
+ def initialize(book_path:, annotation_id:, annotation: nil, **)
71
+ super(
72
+ aggregate_id: book_path,
73
+ book_path: book_path,
74
+ annotation_id: annotation_id,
75
+ annotation: annotation,
76
+ **
77
+ )
78
+ end
79
+
80
+ def book_path
81
+ get_attribute(:book_path)
82
+ end
83
+
84
+ def annotation_id
85
+ get_attribute(:annotation_id)
86
+ end
87
+
88
+ def annotation
89
+ get_attribute(:annotation)
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require 'time'
5
+
6
+ module Shoko
7
+ module Core
8
+ module Events
9
+ # Base class for all domain events.
10
+ #
11
+ # Domain events represent something that has happened in the domain
12
+ # and are used to trigger side effects and maintain loose coupling
13
+ # between different parts of the system.
14
+ #
15
+ # @example Creating a domain event
16
+ # class BookmarkAdded < BaseDomainEvent
17
+ # attribute :book_path, String
18
+ # attribute :bookmark, Bookmark
19
+ # end
20
+ #
21
+ # @example Using a domain event
22
+ # event = BookmarkAdded.new(book_path: '/path/to/book.epub', bookmark: bookmark)
23
+ # event_bus.publish(event)
24
+ class BaseDomainEvent
25
+ # Event metadata
26
+ attr_reader :event_id, :occurred_at, :aggregate_id, :version
27
+
28
+ def initialize(aggregate_id: nil, version: 1, **attributes)
29
+ @event_id = SecureRandom.uuid
30
+ @occurred_at = Time.now.utc
31
+ @aggregate_id = aggregate_id
32
+ @version = version
33
+ @attributes = attributes
34
+
35
+ validate_required_attributes
36
+ validate_attribute_types
37
+ end
38
+
39
+ # Get event type name
40
+ #
41
+ # @return [String] Event type name
42
+ def event_type
43
+ self.class.name.split('::').last
44
+ end
45
+
46
+ # Get event data as hash
47
+ #
48
+ # @return [Hash] Event data
49
+ def event_data
50
+ @attributes.dup
51
+ end
52
+
53
+ # Get specific attribute value
54
+ #
55
+ # @param name [Symbol] Attribute name
56
+ # @return [Object] Attribute value
57
+ def get_attribute(name)
58
+ @attributes[name]
59
+ end
60
+
61
+ # Convert event to hash for serialization
62
+ #
63
+ # @return [Hash] Serialized event
64
+ def to_h
65
+ {
66
+ event_id: @event_id,
67
+ event_type: event_type,
68
+ occurred_at: @occurred_at.iso8601,
69
+ aggregate_id: @aggregate_id,
70
+ version: @version,
71
+ data: @attributes,
72
+ }
73
+ end
74
+
75
+ # Create event from hash
76
+ #
77
+ # @param hash [Hash] Serialized event data
78
+ # @return [BaseDomainEvent] Reconstructed event
79
+ def self.from_h(hash)
80
+ event = allocate
81
+ event.instance_variable_set(:@event_id, hash[:event_id] || hash['event_id'])
82
+ event.instance_variable_set(:@occurred_at,
83
+ Time.parse(hash[:occurred_at] || hash['occurred_at']))
84
+ event.instance_variable_set(:@aggregate_id, hash[:aggregate_id] || hash['aggregate_id'])
85
+ event.instance_variable_set(:@version, hash[:version] || hash['version'] || 1)
86
+ event.instance_variable_set(:@attributes, hash[:data] || hash['data'] || {})
87
+ event
88
+ end
89
+
90
+ # Define required attributes for the event
91
+ #
92
+ # @example
93
+ # class MyEvent < BaseDomainEvent
94
+ # required_attributes :user_id, :action
95
+ # end
96
+ def self.required_attributes(*attrs)
97
+ @required_attributes ||= []
98
+ @required_attributes.concat(attrs) if attrs.any?
99
+ @required_attributes
100
+ end
101
+
102
+ # Define typed attributes for the event
103
+ #
104
+ # @example
105
+ # class MyEvent < BaseDomainEvent
106
+ # typed_attributes user_id: String, count: Integer
107
+ # end
108
+ def self.typed_attributes(types = {})
109
+ @typed_attributes ||= {}
110
+ @typed_attributes.merge!(types) if types.any?
111
+ @typed_attributes
112
+ end
113
+
114
+ # Check if event is of specific type
115
+ #
116
+ # @param type [String, Symbol] Event type to check
117
+ # @return [Boolean] True if event is of specified type
118
+ def of_type?(type)
119
+ event_type == type.to_s
120
+ end
121
+
122
+ # String representation of the event
123
+ #
124
+ # @return [String] String representation
125
+ def to_s
126
+ "#{event_type}(#{@event_id})[#{@occurred_at}]"
127
+ end
128
+
129
+ # Equality comparison
130
+ #
131
+ # @param other [BaseDomainEvent] Other event to compare
132
+ # @return [Boolean] True if events are equal
133
+ def ==(other)
134
+ return false unless other.is_a?(BaseDomainEvent)
135
+
136
+ @event_id == other.event_id &&
137
+ event_type == other.event_type &&
138
+ @attributes == other.event_data
139
+ end
140
+
141
+ private
142
+
143
+ def validate_required_attributes
144
+ klass = self.class
145
+ required = klass.required_attributes
146
+ return unless required
147
+
148
+ missing = required - @attributes.keys
149
+ return if missing.empty?
150
+
151
+ raise ArgumentError, "Missing required attributes: #{missing.join(', ')}"
152
+ end
153
+
154
+ def validate_attribute_types
155
+ klass = self.class
156
+ typed = klass.typed_attributes
157
+ return unless typed
158
+
159
+ typed.each do |attr, type|
160
+ value = @attributes[attr]
161
+ next if value.nil? # Allow nil values
162
+
163
+ raise TypeError, "Attribute #{attr} must be of type #{type}, got #{value.class}" unless value.is_a?(type)
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_domain_event'
4
+
5
+ module Shoko
6
+ module Core
7
+ module Events
8
+ # Common base for bookmark-related events
9
+ class BookmarkEventBase < BaseDomainEvent
10
+ required_attributes :book_path, :bookmark
11
+ typed_attributes book_path: String
12
+
13
+ def initialize(book_path:, bookmark:, **)
14
+ super(
15
+ aggregate_id: book_path,
16
+ book_path: book_path,
17
+ bookmark: bookmark,
18
+ **
19
+ )
20
+ end
21
+
22
+ def book_path
23
+ get_attribute(:book_path)
24
+ end
25
+
26
+ def bookmark
27
+ get_attribute(:bookmark)
28
+ end
29
+ end
30
+
31
+ # Domain event for bookmark creation
32
+ class BookmarkAdded < BookmarkEventBase; end
33
+
34
+ # Domain event for bookmark removal
35
+ class BookmarkRemoved < BookmarkEventBase; end
36
+
37
+ # Domain event for bookmark navigation
38
+ class BookmarkNavigated < BookmarkEventBase; end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_domain_event.rb'
4
+
5
+ module Shoko
6
+ module Core
7
+ module Events
8
+ # Domain event bus for publishing and subscribing to domain events.
9
+ #
10
+ # This provides a domain-specific event bus that sits on top of the
11
+ # infrastructure event bus and handles domain event serialization,
12
+ # routing, and subscription management.
13
+ #
14
+ # @example Publishing an event
15
+ # event_bus.publish(BookmarkAdded.new(book_path: path, bookmark: bookmark))
16
+ #
17
+ # @example Subscribing to events
18
+ # event_bus.subscribe(BookmarkAdded) { |event| handle_bookmark_added(event) }
19
+ class DomainEventBus
20
+ def initialize(infrastructure_event_bus)
21
+ @infrastructure_bus = infrastructure_event_bus
22
+ @subscribers = Hash.new { |h, k| h[k] = [] }
23
+ @middleware = []
24
+ end
25
+
26
+ # Publish a domain event
27
+ #
28
+ # @param event [Core::Events::BaseDomainEvent] Domain event to publish
29
+ def publish(event)
30
+ unless event.is_a?(Core::Events::BaseDomainEvent)
31
+ raise ArgumentError, 'Event must be a BaseDomainEvent'
32
+ end
33
+
34
+ # Apply middleware chain
35
+ processed_event = apply_middleware(event)
36
+ return if processed_event.nil? # Event was filtered out
37
+
38
+ # Publish through infrastructure bus
39
+ @infrastructure_bus.emit_event(
40
+ processed_event.event_type.to_sym,
41
+ event_data: processed_event.to_h
42
+ )
43
+
44
+ # Notify domain subscribers directly
45
+ notify_subscribers(processed_event)
46
+ end
47
+
48
+ # Subscribe to a specific event type
49
+ #
50
+ # @param event_type [Class] Event class to subscribe to
51
+ # @param handler [Proc] Handler block or callable
52
+ # @yield [event] Block to handle the event
53
+ def subscribe(event_type, handler = nil, &)
54
+ handler = resolve_callable!(handler, &)
55
+ @subscribers[event_type] << handler
56
+ end
57
+
58
+ # Subscribe to multiple event types with the same handler
59
+ #
60
+ # @param event_types [Array<Class>] Event classes to subscribe to
61
+ # @param handler [Proc] Handler block or callable
62
+ # @yield [event] Block to handle the event
63
+ def subscribe_to_many(event_types, handler = nil, &)
64
+ handler = resolve_callable!(handler, &)
65
+ event_types.each { |type| subscribe(type, handler) }
66
+ end
67
+
68
+ # Unsubscribe from an event type
69
+ #
70
+ # @param event_type [Class] Event class to unsubscribe from
71
+ # @param handler [Proc] Specific handler to remove (optional)
72
+ def unsubscribe(event_type, handler = nil)
73
+ if handler
74
+ @subscribers[event_type].delete(handler)
75
+ else
76
+ @subscribers.delete(event_type)
77
+ end
78
+ end
79
+
80
+ # Add middleware to the event processing pipeline
81
+ #
82
+ # @param middleware [Proc] Middleware that processes events
83
+ # @yield [event] Block that processes the event and returns modified event or nil
84
+ def add_middleware(middleware = nil, &block)
85
+ middleware = block if block_given?
86
+ raise ArgumentError, 'Middleware must be provided' unless middleware
87
+
88
+ @middleware << middleware
89
+ end
90
+
91
+ def resolve_callable!(callable, &block)
92
+ callable = block if block_given?
93
+ raise ArgumentError, 'Handler must be provided' unless callable
94
+
95
+ callable
96
+ end
97
+
98
+ # Get all subscribers for an event type
99
+ #
100
+ # @param event_type [Class] Event class
101
+ # @return [Array<Proc>] List of subscribers
102
+ def subscribers_for(event_type)
103
+ @subscribers[event_type].dup
104
+ end
105
+
106
+ # Check if there are subscribers for an event type
107
+ #
108
+ # @param event_type [Class] Event class
109
+ # @return [Boolean] True if there are subscribers
110
+ def subscribers?(event_type)
111
+ @subscribers[event_type].any?
112
+ end
113
+
114
+ # Clear all subscribers (useful for testing)
115
+ def clear_subscribers
116
+ @subscribers.clear
117
+ end
118
+
119
+ # Get subscriber count for an event type
120
+ #
121
+ # @param event_type [Class] Event class
122
+ # @return [Integer] Number of subscribers
123
+ def subscriber_count(event_type)
124
+ @subscribers[event_type].size
125
+ end
126
+
127
+ # Get total number of subscribers across all event types
128
+ #
129
+ # @return [Integer] Total subscriber count
130
+ def total_subscribers
131
+ @subscribers.values.sum(&:size)
132
+ end
133
+
134
+ private
135
+
136
+ def apply_middleware(event)
137
+ @middleware.reduce(event) do |current_event, middleware|
138
+ next current_event if current_event.nil?
139
+
140
+ begin
141
+ middleware.call(current_event)
142
+ rescue StandardError => e
143
+ # Log middleware error but don't stop event processing
144
+ warn "Domain event middleware error: #{e.message}"
145
+ current_event
146
+ end
147
+ end
148
+ end
149
+
150
+ def notify_subscribers(event)
151
+ event_type = event.class
152
+
153
+ @subscribers[event_type].each do |handler|
154
+ handler.call(event)
155
+ rescue StandardError => e
156
+ # Log subscriber error but continue with other subscribers
157
+ warn "Domain event subscriber error for #{event_type}: #{e.message}"
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_domain_event'
4
+
5
+ module Shoko
6
+ module Core
7
+ module Events
8
+ # Domain event for reading progress updates
9
+ class ProgressUpdated < BaseDomainEvent
10
+ required_attributes :book_path, :chapter_index, :line_offset
11
+ typed_attributes book_path: String, chapter_index: Integer, line_offset: Integer
12
+
13
+ def initialize(book_path:, chapter_index:, line_offset:, previous_chapter: nil,
14
+ previous_line: nil, **)
15
+ super(
16
+ aggregate_id: book_path,
17
+ book_path: book_path,
18
+ chapter_index: chapter_index,
19
+ line_offset: line_offset,
20
+ previous_chapter: previous_chapter,
21
+ previous_line: previous_line,
22
+ **
23
+ )
24
+ end
25
+
26
+ def book_path
27
+ get_attribute(:book_path)
28
+ end
29
+
30
+ def chapter_index
31
+ get_attribute(:chapter_index)
32
+ end
33
+
34
+ def line_offset
35
+ get_attribute(:line_offset)
36
+ end
37
+
38
+ def previous_chapter
39
+ get_attribute(:previous_chapter)
40
+ end
41
+
42
+ def previous_line
43
+ get_attribute(:previous_line)
44
+ end
45
+
46
+ # Check if this represents forward progress
47
+ def forward_progress?
48
+ return true if previous_chapter.nil? || previous_line.nil?
49
+
50
+ chapter_index > previous_chapter ||
51
+ (chapter_index == previous_chapter && line_offset > previous_line)
52
+ end
53
+ end
54
+
55
+ # Domain event for reading session start
56
+ class ReadingSessionStarted < BaseDomainEvent
57
+ required_attributes :book_path
58
+ typed_attributes book_path: String
59
+
60
+ def initialize(book_path:, **)
61
+ super(
62
+ aggregate_id: book_path,
63
+ book_path: book_path,
64
+ **
65
+ )
66
+ end
67
+
68
+ def book_path
69
+ get_attribute(:book_path)
70
+ end
71
+ end
72
+
73
+ # Domain event for reading session end
74
+ class ReadingSessionEnded < BaseDomainEvent
75
+ required_attributes :book_path, :duration_seconds
76
+ typed_attributes book_path: String, duration_seconds: Integer
77
+
78
+ def initialize(book_path:, duration_seconds:, final_chapter: nil, final_line: nil,
79
+ **)
80
+ super(
81
+ aggregate_id: book_path,
82
+ book_path: book_path,
83
+ duration_seconds: duration_seconds,
84
+ final_chapter: final_chapter,
85
+ final_line: final_line,
86
+ **
87
+ )
88
+ end
89
+
90
+ def book_path
91
+ get_attribute(:book_path)
92
+ end
93
+
94
+ def duration_seconds
95
+ get_attribute(:duration_seconds)
96
+ end
97
+
98
+ def final_chapter
99
+ get_attribute(:final_chapter)
100
+ end
101
+
102
+ def final_line
103
+ get_attribute(:final_line)
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+
5
+ module Shoko
6
+ module Core
7
+ module Models
8
+ # Represents a bookmark within a document.
9
+ Bookmark = Struct.new(:chapter_index, :line_offset, :text_snippet, :created_at,
10
+ keyword_init: true) do
11
+ # Build from hash loaded from disk
12
+ # @param hash [Hash]
13
+ # @return [Bookmark]
14
+ def self.from_h(hash)
15
+ new(
16
+ chapter_index: hash['chapter'],
17
+ line_offset: hash['line_offset'],
18
+ text_snippet: hash['text'],
19
+ created_at: Time.parse(hash['timestamp'])
20
+ )
21
+ end
22
+
23
+ # Convert to hash for persistence
24
+ # @return [Hash]
25
+ def to_h
26
+ {
27
+ 'chapter' => chapter_index,
28
+ 'line_offset' => line_offset,
29
+ 'text' => text_snippet,
30
+ 'timestamp' => created_at.iso8601,
31
+ }
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Core
5
+ module Models
6
+ # Data object for adding bookmarks
7
+ BookmarkData = Struct.new(:path, :chapter, :line_offset, :text, keyword_init: true)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Core
5
+ module Models
6
+ # Represents a chapter within an EPUB document.
7
+ Chapter = Struct.new(:number, :title, :lines, :metadata, :blocks, :raw_content,
8
+ keyword_init: true) do
9
+ # Number of lines in the chapter
10
+ # @return [Integer]
11
+ def line_count
12
+ lines.size
13
+ end
14
+
15
+ # Estimated reading time in minutes
16
+ # @param wpm [Integer] words per minute
17
+ # @return [Integer]
18
+ def estimated_reading_time(wpm = 250)
19
+ word_count = lines.join(' ').split.size
20
+ (word_count / wpm.to_f).ceil
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Core
5
+ module Models
6
+ # Represents a unit of formatted content (heading, paragraph, list item, etc.).
7
+ ContentBlock = Struct.new(:type, :segments, :level, :metadata, keyword_init: true) do
8
+ def text
9
+ segments.to_a.map { |segment| segment&.text.to_s }.join
10
+ end
11
+
12
+ def heading_level
13
+ (metadata && metadata[:level]) || level
14
+ end
15
+ end
16
+
17
+ # Represents a contiguous run of text with associated inline styles.
18
+ TextSegment = Struct.new(:text, :styles, keyword_init: true) do
19
+ def initialize(text:, styles: nil)
20
+ super(text: text.to_s, styles: (styles || {}).freeze)
21
+ end
22
+
23
+ def length
24
+ text.to_s.length
25
+ end
26
+ end
27
+
28
+ # Represents a display-ready line produced by the formatting pipeline.
29
+ DisplayLine = Struct.new(:text, :segments, :metadata, keyword_init: true) do
30
+ def initialize(text:, segments:, metadata: nil)
31
+ super(text: text.to_s, segments: segments || [], metadata: metadata || {})
32
+ end
33
+
34
+ def length
35
+ text.length
36
+ end
37
+
38
+ def empty?
39
+ text.strip.empty?
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end