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,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'time'
5
+ module Shoko
6
+ module Adapters::Monitoring
7
+ # Collects per-open performance timings when DEBUG_PERF=1.
8
+ module PerfTracer
9
+ SESSION_KEY = :shoko_perf_session
10
+ STAGES = [
11
+ 'open.invoke',
12
+ 'cache.pipeline',
13
+ 'cache.lookup',
14
+ 'zip.read',
15
+ 'opf.parse',
16
+ 'xhtml.normalize',
17
+ 'pagination.build',
18
+ 'formatting.ensure',
19
+ 'page_map.hydrate',
20
+ 'render.first_paint.ttfp',
21
+ ].freeze
22
+
23
+ @profile_path = begin
24
+ primary = ENV.fetch('SHOKO_PROFILE_PATH', '').to_s.strip
25
+ primary.empty? ? nil : primary
26
+ end
27
+
28
+ module_function
29
+
30
+ def profile_path=(path)
31
+ @profile_path = path
32
+ end
33
+
34
+ def enabled?
35
+ return @enabled unless @enabled.nil?
36
+
37
+ raw = ENV.fetch('DEBUG_PERF', nil)
38
+ env_enabled = raw && raw.to_s.strip == '1'
39
+ @enabled = env_enabled || @profile_path
40
+ end
41
+
42
+ alias active? enabled?
43
+
44
+ def start_open(path)
45
+ return unless enabled?
46
+
47
+ session = Session.new(path)
48
+ Thread.current[SESSION_KEY] = session
49
+ session
50
+ end
51
+
52
+ def current_session
53
+ Thread.current[SESSION_KEY]
54
+ end
55
+
56
+ def measure(stage, &)
57
+ session = current_session
58
+ return yield unless session
59
+
60
+ session.measure(stage, &)
61
+ end
62
+
63
+ def record(stage, duration)
64
+ session = current_session
65
+ session&.record(stage, duration)
66
+ end
67
+
68
+ def complete(open_type:, total_duration: nil)
69
+ session = current_session
70
+ return unless session
71
+
72
+ session.add_metadata(open_type: open_type)
73
+ session.record('open.invoke', total_duration) if total_duration
74
+ session.open_type = open_type
75
+ session.emit
76
+ ensure
77
+ clear_session
78
+ end
79
+
80
+ def clear_session
81
+ Thread.current[SESSION_KEY] = nil
82
+ end
83
+
84
+ def cancel
85
+ clear_session
86
+ end
87
+
88
+ def annotate(metadata)
89
+ session = current_session
90
+ session&.add_metadata(metadata)
91
+ end
92
+
93
+ # Internal per-open timing tracker.
94
+ class Session
95
+ attr_accessor :open_type
96
+
97
+ def initialize(path)
98
+ @path = path
99
+ @started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
100
+ @timings = Hash.new(0.0)
101
+ @open_type = 'unknown'
102
+ @metadata = { book: path }
103
+ end
104
+
105
+ def measure(stage)
106
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
107
+ yield
108
+ ensure
109
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
110
+ @timings[stage] += duration if duration
111
+ end
112
+
113
+ def record(stage, duration)
114
+ @timings[stage] += duration.to_f
115
+ end
116
+
117
+ def add_metadata(hash)
118
+ return unless hash.is_a?(Hash)
119
+
120
+ hash.each do |k, v|
121
+ @metadata[k.to_sym] = v unless v.nil?
122
+ end
123
+ end
124
+
125
+ def emit
126
+ total = @timings['open.invoke']
127
+ total = elapsed unless total.positive?
128
+ fields = ["perf open=#{open_type_label}"]
129
+ PerfTracer::STAGES.each do |stage|
130
+ duration = stage == 'open.invoke' ? total : @timings[stage]
131
+ fields << "#{stage}=#{format_ms(duration)}"
132
+ end
133
+ line_one = [
134
+ "time=#{timestamp}",
135
+ "book=#{@metadata[:book]}",
136
+ ("cache_hit=#{@metadata[:cache_hit]}" if @metadata.key?(:cache_hit)),
137
+ ("pagination_cache=#{@metadata[:pagination_cache]}" if @metadata.key?(:pagination_cache)),
138
+ ("chapters=#{@metadata[:chapters]}" if @metadata.key?(:chapters)),
139
+ "open_type=#{open_type_label}",
140
+ ].compact.join(' ')
141
+ output = [line_one, "stages #{fields[1..].join(' ')}", '---'].join("\n")
142
+ write_output(output)
143
+ end
144
+
145
+ private
146
+
147
+ def timestamp
148
+ Time.now.utc.iso8601
149
+ rescue StandardError
150
+ Time.now.to_s
151
+ end
152
+
153
+ def open_type_label
154
+ label = @open_type.to_s.strip
155
+ label.empty? ? 'unknown' : label
156
+ end
157
+
158
+ def elapsed
159
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) - @started_at
160
+ end
161
+
162
+ def format_ms(seconds)
163
+ ms = (seconds.to_f * 1000.0)
164
+ "#{ms.round}ms"
165
+ end
166
+
167
+ def write_output(text)
168
+ path = PerfTracer.instance_variable_get(:@profile_path)
169
+ if path && !path.to_s.strip.empty?
170
+ begin
171
+ FileUtils.mkdir_p(File.dirname(path))
172
+ File.open(path, 'a') { |f| f.puts(text) }
173
+ return
174
+ rescue StandardError
175
+ # fall through to stdout
176
+ end
177
+ end
178
+ $stdout.puts(text)
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Adapters::Monitoring
5
+ # Performance monitoring for the application.
6
+ # Tracks execution times, memory usage, and provides profiling capabilities.
7
+ #
8
+ # @example Timing a block
9
+ # PerformanceMonitor.time("epub_parsing") do
10
+ # EPUBDocument.new(path)
11
+ # end
12
+ #
13
+ # @example Getting metrics
14
+ # puts PerformanceMonitor.metrics
15
+ class PerformanceMonitor
16
+ class << self
17
+ # Storage for performance metrics
18
+ def metrics
19
+ @metrics ||= Hash.new { |h, k| h[k] = [] }
20
+ end
21
+
22
+ # Time a block of code
23
+ #
24
+ # @param label [String] Label for the timing
25
+ # @yield Block to time
26
+ # @return [Object] Result of the block
27
+ def time(label)
28
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
29
+ start_memory = current_memory_usage
30
+
31
+ result = yield
32
+
33
+ end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
34
+ end_memory = current_memory_usage
35
+
36
+ duration = end_time - start_time
37
+ memory_delta = end_memory - start_memory
38
+
39
+ record_metric(label, duration, memory_delta)
40
+
41
+ result
42
+ end
43
+
44
+ # Record a metric
45
+ #
46
+ # @param label [String] Metric label
47
+ # @param duration [Float] Duration in seconds
48
+ # @param memory_delta [Integer] Memory change in bytes
49
+ def record_metric(label, duration, memory_delta)
50
+ metrics[label] << ({
51
+ timestamp: Time.now,
52
+ duration:,
53
+ memory_delta:,
54
+ })
55
+
56
+ # Log slow operations
57
+ return unless duration > 1.0
58
+
59
+ Shoko::Adapters::Monitoring::Logger.warn('Slow operation detected',
60
+ label:,
61
+ duration: "#{(duration * 1000).round(2)}ms")
62
+ end
63
+
64
+ # Get statistics for a metric
65
+ #
66
+ # @param label [String] Metric label
67
+ # @return [Hash] Statistics
68
+ def stats(label)
69
+ data = metrics[label]
70
+ return nil if data.empty?
71
+
72
+ calculate_statistics(data)
73
+ end
74
+
75
+ # Clear all metrics
76
+ def clear
77
+ @metrics = nil
78
+ end
79
+
80
+ private
81
+
82
+ def calculate_statistics(data)
83
+ durations = data.map { |m| m[:duration] }
84
+
85
+ {
86
+ count: data.size,
87
+ total: durations.sum,
88
+ average: calculate_average(durations),
89
+ min: durations.min,
90
+ max: durations.max,
91
+ last: durations.last,
92
+ }
93
+ end
94
+
95
+ def calculate_average(durations)
96
+ durations.sum / durations.size
97
+ end
98
+
99
+ # Get current memory usage in bytes
100
+ #
101
+ # @return [Integer] Memory usage
102
+ def current_memory_usage
103
+ # This is a simplified version - in production you might use
104
+ # more sophisticated memory profiling
105
+ GC.stat[:total_allocated_objects] * 40 # Rough estimate
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../core/services/base_service.rb'
4
+ require 'English'
5
+
6
+ module Shoko
7
+ module Adapters
8
+ module Output
9
+ module Clipboard
10
+ # Domain service for clipboard operations with dependency injection.
11
+ # Migrated from legacy Services::ClipboardService to follow DI pattern.
12
+ class ClipboardService < BaseService
13
+ # Error raised when clipboard operations fail
14
+ class ClipboardError < StandardError; end
15
+
16
+ # Copy text to system clipboard
17
+ def copy_text?(text)
18
+ return false if text.nil? || text.strip.empty?
19
+
20
+ command = detect_clipboard_command
21
+ return false unless command
22
+
23
+ success = execute_clipboard_command(command, text)
24
+
25
+ if success
26
+ log_success(text.length)
27
+ true
28
+ else
29
+ log_failure
30
+ false
31
+ end
32
+ end
33
+
34
+ # Copy text with user feedback
35
+ def copy_with_feedback(text)
36
+ if copy_text?(text)
37
+ yield(' Copied to clipboard!') if block_given?
38
+ true
39
+ else
40
+ yield(' Failed to copy to clipboard') if block_given?
41
+ false
42
+ end
43
+ rescue ClipboardError => e
44
+ yield(" Copy failed: #{e.message}") if block_given?
45
+ false
46
+ end
47
+
48
+ # Check if clipboard functionality is available
49
+ def available?
50
+ !detect_clipboard_command.nil?
51
+ end
52
+
53
+ private
54
+
55
+ def detect_clipboard_command
56
+ case RUBY_PLATFORM
57
+ when /darwin/
58
+ command_exists?('pbcopy') ? ['pbcopy'] : nil
59
+ when /linux/
60
+ if command_exists?('xclip')
61
+ ['xclip', '-selection', 'clipboard']
62
+ elsif command_exists?('xsel')
63
+ ['xsel', '--clipboard', '--input']
64
+ elsif command_exists?('wl-copy')
65
+ ['wl-copy']
66
+ end
67
+ when /mswin|mingw|cygwin/
68
+ ['clip']
69
+ end
70
+ end
71
+
72
+ def command_exists?(command)
73
+ name = command.to_s.strip
74
+ return false if name.empty?
75
+
76
+ path_env = ENV.fetch('PATH', '')
77
+ exts = if RUBY_PLATFORM.match?(/mswin|mingw|cygwin/i)
78
+ raw = ENV.fetch('PATHEXT', '.EXE;.BAT;.CMD')
79
+ parsed = raw.split(';').map(&:strip).reject(&:empty?)
80
+ (parsed + parsed.map(&:downcase) + ['']).uniq
81
+ else
82
+ ['']
83
+ end
84
+
85
+ path_env.split(File::PATH_SEPARATOR).any? do |dir|
86
+ exts.any? do |ext|
87
+ candidate = File.join(dir, "#{name}#{ext}")
88
+ File.file?(candidate) && File.executable?(candidate)
89
+ end
90
+ end
91
+ rescue StandardError
92
+ false
93
+ end
94
+
95
+ def execute_clipboard_command(command, text)
96
+ IO.popen(command, 'w') do |pipe|
97
+ pipe.write(text)
98
+ end
99
+ $CHILD_STATUS.success?
100
+ rescue Errno::ENOENT, Errno::EPIPE
101
+ false
102
+ end
103
+
104
+ def log_success(char_count)
105
+ return unless registered?(:logger)
106
+
107
+ resolve(:logger).info('Text copied to clipboard', chars: char_count)
108
+ end
109
+
110
+ def log_failure
111
+ return unless registered?(:logger)
112
+
113
+ resolve(:logger).warn('Failed to copy text to clipboard')
114
+ end
115
+
116
+ protected
117
+
118
+ def required_dependencies
119
+ [] # Logger is optional
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest/sha1'
4
+
5
+ require_relative '../../../../../core/models/content_block.rb'
6
+
7
+ module Shoko
8
+ module Adapters::Output::Formatting
9
+ class FormattingService
10
+ class LineAssembler
11
+ # Builds display lines and metadata for block-level and inline images.
12
+ class ImageBuilder
13
+ include Shoko::Core::Models
14
+
15
+ MAX_ID = 4_294_967_295
16
+ RENDERABLE_IMAGE_EXTENSIONS = %w[.png .jpg .jpeg].freeze
17
+
18
+ def initialize(width:, chapter_index:, chapter_source_path:, max_image_rows: nil)
19
+ @width = width.to_i
20
+ @chapter_index = chapter_index
21
+ @chapter_source_path = chapter_source_path
22
+ @max_image_rows = normalized_max_image_rows(max_image_rows)
23
+ @inline_image_counter = 0
24
+ end
25
+
26
+ def block_lines(block, block_index:, base_metadata:)
27
+ render = base_metadata.merge(image_render: block_render_metadata(block, block_index))
28
+ image_lines(render, rows: render.dig(:image_render, :rows).to_i)
29
+ end
30
+
31
+ def inline_lines(inline, indent_cols)
32
+ @inline_image_counter += 1
33
+ cols_available = [@width - indent_cols.to_i, 1].max
34
+ render = inline_metadata(inline, cols_available, indent_cols.to_i, @inline_image_counter)
35
+ image_lines(render, rows: render.dig(:image_render, :rows).to_i)
36
+ end
37
+
38
+ def renderable_block_image?(block)
39
+ image = (block.metadata || {})[:image] || (block.metadata || {})['image'] || {}
40
+ src = image[:src] || image['src']
41
+ renderable_image_src?(src)
42
+ rescue StandardError
43
+ false
44
+ end
45
+
46
+ def renderable_image_src?(src)
47
+ return false if src.nil? || src.to_s.empty?
48
+
49
+ ext = File.extname(src.to_s.split(/[?#]/, 2).first.to_s).downcase
50
+ RENDERABLE_IMAGE_EXTENSIONS.include?(ext)
51
+ rescue StandardError
52
+ false
53
+ end
54
+
55
+ private
56
+
57
+ def normalized_max_image_rows(value)
58
+ rows = value.to_i
59
+ rows.positive? ? rows : nil
60
+ end
61
+
62
+ def block_render_metadata(block, block_index)
63
+ {
64
+ cols: @width,
65
+ rows: rows_for(@width),
66
+ placement_id: placement_id_for_block(block, block_index),
67
+ }
68
+ end
69
+
70
+ def inline_metadata(inline, cols_available, indent_cols, index)
71
+ src = inline.is_a?(Hash) ? (inline[:src] || inline['src']) : nil
72
+ alt = inline.is_a?(Hash) ? (inline[:alt] || inline['alt']) : nil
73
+ {
74
+ block_type: :image,
75
+ chapter_index: @chapter_index,
76
+ chapter_source_path: @chapter_source_path,
77
+ image: { src: src, alt: alt },
78
+ inline_image: true,
79
+ image_render: {
80
+ cols: cols_available,
81
+ rows: rows_for(cols_available),
82
+ placement_id: placement_id_for_inline(src, index),
83
+ col_offset: indent_cols,
84
+ },
85
+ }
86
+ end
87
+
88
+ def image_lines(metadata, rows:)
89
+ Array.new(rows) do |row_index|
90
+ DisplayLine.new(
91
+ text: '',
92
+ segments: [],
93
+ metadata: metadata.merge(
94
+ image_render_line: row_index.zero?,
95
+ image_line_index: row_index,
96
+ image_spacer: !row_index.zero?
97
+ )
98
+ )
99
+ end
100
+ end
101
+
102
+ def rows_for(cols)
103
+ estimate = (cols.to_i * 0.5).round
104
+ estimate = estimate.clamp(4, 18)
105
+ estimate = [estimate, @max_image_rows].min if @max_image_rows
106
+ [estimate, 1].max
107
+ rescue StandardError
108
+ 8
109
+ end
110
+
111
+ def placement_id_for_block(block, block_index)
112
+ image = (block.metadata || {})[:image] || (block.metadata || {})['image'] || {}
113
+ src = image[:src] || image['src'] || ''
114
+ hashed_id("#{chapter_seed}|#{src}|#{block_index}")
115
+ rescue StandardError
116
+ clamp_id(block_index.to_i + 1)
117
+ end
118
+
119
+ def placement_id_for_inline(src, index)
120
+ hashed_id("#{chapter_seed}|#{src}|inline|#{index}")
121
+ rescue StandardError
122
+ clamp_id(index.to_i + 1)
123
+ end
124
+
125
+ def chapter_seed
126
+ @chapter_source_path.to_s
127
+ end
128
+
129
+ def hashed_id(seed)
130
+ raw = Digest::SHA1.digest(seed.to_s)
131
+ int = raw.unpack1('N')
132
+ int.zero? ? 1 : int
133
+ rescue StandardError
134
+ 1
135
+ end
136
+
137
+ def clamp_id(value)
138
+ int = value.to_i
139
+ int = 1 if int <= 0
140
+ return int if int <= MAX_ID
141
+
142
+ reduced = int % MAX_ID
143
+ reduced.zero? ? 1 : reduced
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end