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,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Core
5
+ module Models
6
+ # Shared reader layout/formatting defaults used across application and UI layers.
7
+ module ReaderSettings
8
+ DEFAULT_LINE_SPACING = :compact
9
+ LINE_SPACING_VALUES = %i[compact normal relaxed].freeze
10
+ LINE_SPACING_MULTIPLIERS = {
11
+ compact: 1.0,
12
+ normal: 0.75,
13
+ relaxed: 0.5,
14
+ }.freeze
15
+
16
+ SINGLE_VIEW_WIDTH_PERCENT = 0.9
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Core
5
+ module Models
6
+ # Lightweight value object describing an anchor within rendered geometry.
7
+ class SelectionAnchor
8
+ ATTRIBUTES = %i[page_id column_id geometry_key line_offset cell_index row column_origin].freeze
9
+
10
+ attr_reader(*ATTRIBUTES)
11
+
12
+ def initialize(page_id:, column_id:, geometry_key:, line_offset:, cell_index:, row:, column_origin:)
13
+ @page_id = page_id
14
+ @column_id = column_id
15
+ @geometry_key = geometry_key
16
+ @line_offset = line_offset
17
+ @cell_index = cell_index
18
+ @row = row
19
+ @column_origin = column_origin
20
+ end
21
+
22
+ def self.from(anchor)
23
+ return anchor if anchor.is_a?(SelectionAnchor)
24
+ return nil unless anchor.respond_to?(:[]) || anchor.is_a?(Hash)
25
+
26
+ new(
27
+ page_id: anchor[:page_id] || anchor['page_id'],
28
+ column_id: anchor[:column_id] || anchor['column_id'],
29
+ geometry_key: anchor[:geometry_key] || anchor['geometry_key'],
30
+ line_offset: anchor[:line_offset] || anchor['line_offset'] || 0,
31
+ cell_index: anchor[:cell_index] || anchor['cell_index'] || 0,
32
+ row: anchor[:row] || anchor['row'] || 0,
33
+ column_origin: anchor[:column_origin] || anchor['column_origin'] || 0
34
+ )
35
+ end
36
+
37
+ def to_h
38
+ {
39
+ page_id: page_id,
40
+ column_id: column_id,
41
+ geometry_key: geometry_key,
42
+ line_offset: line_offset,
43
+ cell_index: cell_index,
44
+ row: row,
45
+ column_origin: column_origin,
46
+ }
47
+ end
48
+
49
+ def <=>(other)
50
+ return nil unless other.is_a?(SelectionAnchor)
51
+
52
+ compare_tuple <=> other.compare_tuple
53
+ end
54
+
55
+ def compare_tuple
56
+ [page_id || 0, line_offset, column_id || 0, row || 0, cell_index]
57
+ end
58
+
59
+ def with_cell_index(new_index)
60
+ self.class.new(
61
+ page_id: page_id,
62
+ column_id: column_id,
63
+ geometry_key: geometry_key,
64
+ line_offset: line_offset,
65
+ cell_index: new_index,
66
+ row: row,
67
+ column_origin: column_origin
68
+ )
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Core
5
+ module Models
6
+ # Represents a Table-of-Contents entry.
7
+ TOCEntry = Struct.new(:title, :href, :level, :chapter_index, :navigable, keyword_init: true) do
8
+ def initialize(title:, href:, level:, chapter_index: nil, navigable: true)
9
+ super
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_service'
4
+ require_relative '../events/annotation_events.rb'
5
+
6
+ module Shoko
7
+ module Core
8
+ module Services
9
+ # Domain-level service for annotation persistence and state updates.
10
+ # Uses AnnotationRepository for clean separation from infrastructure.
11
+ class AnnotationService < BaseService
12
+ def list_for_book(path)
13
+ return [] unless path && !path.to_s.empty?
14
+
15
+ @annotation_repository.find_by_book_path(path)
16
+ end
17
+
18
+ def list_all
19
+ @annotation_repository.find_all
20
+ end
21
+
22
+ def add(path, text, note, range, chapter_index, page_meta = nil)
23
+ annotation = @annotation_repository.add_for_book(
24
+ path,
25
+ text: text,
26
+ note: note,
27
+ range: range,
28
+ chapter_index: chapter_index,
29
+ page_meta: page_meta
30
+ )
31
+
32
+ @domain_event_bus.publish(Events::AnnotationAdded.new(
33
+ book_path: path,
34
+ annotation: annotation
35
+ ))
36
+
37
+ notify_updated(path)
38
+ annotation
39
+ end
40
+
41
+ def update(path, id, note)
42
+ old_note = ''
43
+ if @annotation_repository.respond_to?(:find_by_id)
44
+ old_annotation = @annotation_repository.find_by_id(path, id)
45
+ old_note = old_annotation ? old_annotation['note'] : ''
46
+ end
47
+
48
+ result = @annotation_repository.update_note(path, id, note)
49
+
50
+ @domain_event_bus.publish(Events::AnnotationUpdated.new(
51
+ book_path: path,
52
+ annotation_id: id,
53
+ old_note: old_note,
54
+ new_note: note
55
+ ))
56
+
57
+ notify_updated(path)
58
+ result
59
+ end
60
+
61
+ def delete(path, id)
62
+ annotation = @annotation_repository.find_by_id(path, id) if @annotation_repository.respond_to?(:find_by_id)
63
+
64
+ result = @annotation_repository.delete_by_id(path, id)
65
+
66
+ @domain_event_bus.publish(Events::AnnotationRemoved.new(
67
+ book_path: path,
68
+ annotation_id: id,
69
+ annotation: annotation
70
+ ))
71
+
72
+ notify_updated(path)
73
+ result
74
+ end
75
+
76
+ protected
77
+
78
+ def required_dependencies
79
+ %i[state_store annotation_repository domain_event_bus]
80
+ end
81
+
82
+ def setup_service_dependencies
83
+ @state_store = resolve(:state_store)
84
+ @annotation_repository = resolve(:annotation_repository)
85
+ @domain_event_bus = resolve(:domain_event_bus)
86
+ end
87
+
88
+ private
89
+
90
+ def notify_updated(path)
91
+ return unless @state_store && path
92
+
93
+ annotations = list_for_book(path)
94
+ @state_store.dispatch(Shoko::Application::Actions::UpdateAnnotationsAction.new(annotations))
95
+ rescue StandardError
96
+ # Best-effort state refresh; persistence already succeeded
97
+ nil
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Core
5
+ module Services
6
+ # Base class for all domain services with dependency injection support.
7
+ # Provides standardized initialization and dependency management.
8
+ class BaseService
9
+ attr_reader :dependencies
10
+
11
+ def initialize(dependencies)
12
+ @dependencies = dependencies
13
+ validate_dependencies
14
+ setup_service_dependencies
15
+ end
16
+
17
+ protected
18
+
19
+ # Override in subclasses to specify required dependencies
20
+ def required_dependencies
21
+ []
22
+ end
23
+
24
+ # Override in subclasses to setup specific service dependencies
25
+ def setup_service_dependencies
26
+ # Default implementation does nothing
27
+ end
28
+
29
+ # Resolve a dependency from the container
30
+ def resolve(dependency_name)
31
+ return unless @dependencies.respond_to?(:resolve)
32
+
33
+ @dependencies.resolve(dependency_name)
34
+ end
35
+
36
+ # Check if a dependency is registered
37
+ def registered?(dependency_name)
38
+ return false unless @dependencies.respond_to?(:registered?)
39
+
40
+ @dependencies.registered?(dependency_name)
41
+ end
42
+
43
+ private
44
+
45
+ def validate_dependencies
46
+ return unless respond_to?(:required_dependencies)
47
+
48
+ missing = required_dependencies.reject { |dep| registered?(dep) }
49
+ return if missing.empty?
50
+
51
+ raise ArgumentError, "Missing required dependencies: #{missing.join(', ')}"
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ module Shoko
59
+ BaseService = Core::Services::BaseService
60
+ end
@@ -0,0 +1,267 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_service'
4
+ require_relative '../events/bookmark_events.rb'
5
+
6
+ module Shoko
7
+ module Core
8
+ module Services
9
+ # Pure business logic for bookmark management.
10
+ # Replaces the tightly coupled BookmarkService with clean domain logic.
11
+ class BookmarkService < BaseService
12
+ # Add bookmark at current position
13
+ #
14
+ # @param text_snippet [String] Optional text snippet for the bookmark
15
+ # @return [Bookmark] Created bookmark
16
+ def add_bookmark(text_snippet = nil)
17
+ book_path = current_book_path
18
+ return nil unless book_path
19
+
20
+ current_state = safe_snapshot
21
+ chapter_index = current_state.dig(:reader, :current_chapter) || 0
22
+ line_offset = get_current_line_offset(current_state)
23
+
24
+ bookmark = @bookmark_repository.add_for_book(
25
+ book_path,
26
+ chapter_index: chapter_index,
27
+ line_offset: line_offset,
28
+ text_snippet: text_snippet || generate_text_snippet(current_state)
29
+ )
30
+
31
+ refresh_bookmarks(book_path)
32
+
33
+ @domain_event_bus.publish(Events::BookmarkAdded.new(
34
+ book_path: book_path,
35
+ bookmark: bookmark
36
+ ))
37
+ @event_bus.emit_event(:bookmark_added, { bookmark: bookmark })
38
+ bookmark
39
+ end
40
+
41
+ # Remove bookmark
42
+ #
43
+ # @param bookmark [Bookmark] Bookmark to remove
44
+ def remove_bookmark(bookmark)
45
+ book_path = current_book_path
46
+ return unless book_path
47
+
48
+ @bookmark_repository.delete_for_book(book_path, bookmark)
49
+ refresh_bookmarks(book_path)
50
+
51
+ @domain_event_bus.publish(Events::BookmarkRemoved.new(
52
+ book_path: book_path,
53
+ bookmark: bookmark
54
+ ))
55
+ @event_bus.emit_event(:bookmark_removed, { bookmark: bookmark })
56
+ end
57
+
58
+ # Get all bookmarks for current book
59
+ #
60
+ # @return [Array<Bookmark>] Array of bookmarks
61
+ def bookmarks
62
+ book_path = current_book_path
63
+ return [] unless book_path
64
+
65
+ @bookmark_repository.find_by_book_path(book_path)
66
+ end
67
+
68
+ # Navigate to bookmark
69
+ #
70
+ # @param bookmark [Bookmark] Bookmark to navigate to
71
+ def jump_to_bookmark(bookmark)
72
+ snapshot = safe_snapshot
73
+ line_offset = bookmark.line_offset.to_i
74
+ updates = {
75
+ %i[reader current_chapter] => bookmark.chapter_index,
76
+ %i[reader current_page] => line_offset,
77
+ }
78
+
79
+ view_mode = snapshot.dig(:config, :view_mode) || :split
80
+ if view_mode == :split
81
+ stride = split_stride_for(snapshot)
82
+ updates[%i[reader left_page]] = line_offset
83
+ updates[%i[reader right_page]] = line_offset + stride
84
+ else
85
+ updates[%i[reader single_page]] = line_offset
86
+ end
87
+
88
+ if dynamic_mode?(snapshot)
89
+ page_index = page_index_for(bookmark.chapter_index, line_offset)
90
+ updates[%i[reader current_page_index]] = page_index if page_index
91
+ end
92
+
93
+ apply_state_updates(updates)
94
+
95
+ # Publish domain event
96
+ @domain_event_bus.publish(Events::BookmarkNavigated.new(
97
+ book_path: current_book_path,
98
+ bookmark: bookmark
99
+ ))
100
+
101
+ # Legacy event bus for backward compatibility
102
+ @event_bus.emit_event(:navigated_to_bookmark, { bookmark: bookmark })
103
+ end
104
+
105
+ # Check if current position has bookmark
106
+ #
107
+ # @return [Boolean]
108
+ def current_position_bookmarked?
109
+ book_path = current_book_path
110
+ return false unless book_path
111
+
112
+ current_state = safe_snapshot
113
+ current_chapter = current_state.dig(:reader, :current_chapter) || 0
114
+ line_offset = get_current_line_offset(current_state)
115
+ @bookmark_repository.exists_at_position?(book_path, current_chapter, line_offset)
116
+ end
117
+
118
+ # Get bookmark at current position (if any)
119
+ #
120
+ # @return [Bookmark, nil]
121
+ def bookmark_at_current_position
122
+ book_path = current_book_path
123
+ return nil unless book_path
124
+
125
+ current_state = safe_snapshot
126
+ current_chapter = current_state.dig(:reader, :current_chapter) || 0
127
+ line_offset = get_current_line_offset(current_state)
128
+ @bookmark_repository.find_at_position(book_path, current_chapter, line_offset)
129
+ end
130
+
131
+ # Toggle bookmark at current position
132
+ #
133
+ # @param text_snippet [String] Text snippet if adding
134
+ # @return [Symbol] :added or :removed
135
+ def toggle_bookmark(text_snippet = nil)
136
+ existing_bookmark = bookmark_at_current_position
137
+
138
+ if existing_bookmark
139
+ remove_bookmark(existing_bookmark)
140
+ :removed
141
+ else
142
+ add_bookmark(text_snippet)
143
+ :added
144
+ end
145
+ end
146
+
147
+ protected
148
+
149
+ def required_dependencies
150
+ %i[state_store event_bus bookmark_repository domain_event_bus]
151
+ end
152
+
153
+ def setup_service_dependencies
154
+ @state_store = resolve(:state_store)
155
+ @event_bus = resolve(:event_bus)
156
+ @bookmark_repository = resolve(:bookmark_repository)
157
+ @domain_event_bus = resolve(:domain_event_bus)
158
+ @page_calculator = resolve(:page_calculator) if registered?(:page_calculator)
159
+ @layout_service = resolve(:layout_service) if registered?(:layout_service)
160
+ @terminal_service = resolve(:terminal_service) if registered?(:terminal_service)
161
+ end
162
+
163
+ private
164
+
165
+ def get_current_line_offset(state)
166
+ if dynamic_mode?(state)
167
+ offset = line_offset_for_dynamic_state(state)
168
+ return offset if offset
169
+ end
170
+
171
+ # Get the current line position depending on view mode
172
+ view_mode = state.dig(:config, :view_mode) || :split
173
+ if view_mode == :split
174
+ state.dig(:reader, :left_page) || 0
175
+ else
176
+ state.dig(:reader, :single_page) || 0
177
+ end
178
+ end
179
+
180
+ def dynamic_mode?(state)
181
+ (state.dig(:config, :page_numbering_mode) || :dynamic) == :dynamic
182
+ end
183
+
184
+ def page_index_for(chapter_index, line_offset)
185
+ return nil unless @page_calculator
186
+
187
+ idx = @page_calculator.find_page_index(chapter_index, line_offset)
188
+ idx && idx >= 0 ? idx : nil
189
+ rescue StandardError
190
+ nil
191
+ end
192
+
193
+ def line_offset_for_dynamic_state(state)
194
+ return nil unless @page_calculator
195
+
196
+ page_index = state.dig(:reader, :current_page_index) || 0
197
+ page = @page_calculator.get_page(page_index)
198
+ offset = page && (page[:start_line] || page['start_line'])
199
+ offset&.to_i
200
+ rescue StandardError
201
+ nil
202
+ end
203
+
204
+ def split_stride_for(state)
205
+ return 1 unless @layout_service
206
+
207
+ width = state.dig(:ui, :terminal_width)
208
+ height = state.dig(:ui, :terminal_height)
209
+ height, width = @terminal_service.size if (!width || !height) && @terminal_service
210
+ width = width.to_i
211
+ height = height.to_i
212
+ width = 80 if width <= 0
213
+ height = 24 if height <= 0
214
+
215
+ _, content_height = @layout_service.calculate_metrics(width, height, :split)
216
+ spacing = state.dig(:config, :line_spacing) || Shoko::Core::Models::ReaderSettings::DEFAULT_LINE_SPACING
217
+ stride = @layout_service.adjust_for_line_spacing(content_height, spacing)
218
+ stride = 1 if stride.to_i <= 0
219
+ stride
220
+ rescue StandardError
221
+ 1
222
+ end
223
+
224
+ def generate_text_snippet(state)
225
+ # This would be implemented based on the current document content
226
+ # For now, return a placeholder
227
+ chapter_index = state.dig(:reader, :current_chapter) || 0
228
+ line_offset = get_current_line_offset(state)
229
+ "Chapter #{chapter_index + 1}, Line #{line_offset + 1}"
230
+ end
231
+
232
+ def current_book_path
233
+ if @state_store.respond_to?(:get)
234
+ @state_store.get(%i[reader book_path])
235
+ elsif @state_store.respond_to?(:current_state)
236
+ (@state_store.current_state || {}).dig(:reader, :book_path)
237
+ end
238
+ end
239
+
240
+ def refresh_bookmarks(book_path = current_book_path)
241
+ return unless book_path
242
+
243
+ bookmarks = @bookmark_repository.find_by_book_path(book_path)
244
+ apply_state_updates({ %i[reader bookmarks] => bookmarks })
245
+ end
246
+
247
+ def apply_state_updates(updates)
248
+ return if updates.nil? || updates.empty?
249
+
250
+ if @state_store.respond_to?(:update)
251
+ @state_store.update(updates)
252
+ elsif @state_store.respond_to?(:set)
253
+ updates.each { |path, value| @state_store.set(path, value) }
254
+ end
255
+ end
256
+
257
+ def safe_snapshot
258
+ return {} unless @state_store.respond_to?(:current_state)
259
+
260
+ @state_store.current_state || {}
261
+ rescue StandardError
262
+ {}
263
+ end
264
+ end
265
+ end
266
+ end
267
+ end