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,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../pagination'
4
+ require_relative '../../../adapters/output/kitty/kitty_graphics.rb'
5
+
6
+ module Shoko::Core::Services::Pagination
7
+ # Centralises the logic for hydrating dynamic pagination from the cache.
8
+ class PaginationCachePreloader
9
+ # Preload outcome with an optional cache key.
10
+ Result = Struct.new(:status, :key, keyword_init: true)
11
+ # Requested terminal dimensions (before defaults are applied).
12
+ Dimensions = Struct.new(:width, :height, keyword_init: true)
13
+ # Layout metadata used for pagination cache lookups.
14
+ LayoutSpec = Struct.new(:key, :width, :height, :view_mode, :line_spacing, :kitty_images, keyword_init: true)
15
+ private_constant :Result, :Dimensions, :LayoutSpec
16
+
17
+ def initialize(state:, page_calculator:, pagination_cache:)
18
+ @state = state
19
+ @page_calculator = page_calculator
20
+ @pagination_cache = pagination_cache
21
+ end
22
+
23
+ def preload(doc, width:, height:)
24
+ guard = guard_preload(doc)
25
+ return guard if guard
26
+
27
+ dimensions = resolve_dimensions(Dimensions.new(width: width, height: height))
28
+ layout, miss_key = resolve_layout(doc, dimensions)
29
+ return Result.new(status: :miss, key: miss_key) unless layout
30
+
31
+ apply_layout_config(layout)
32
+ cached_pages = load_cached_pages(doc, layout.key)
33
+ return Result.new(status: :miss, key: layout.key) unless cached_pages
34
+
35
+ hydrate_from_cache(cached_pages, dimensions)
36
+ Result.new(status: :hit, key: layout.key)
37
+ rescue StandardError => e
38
+ log_failure(e)
39
+ Result.new(status: :error)
40
+ end
41
+
42
+ private
43
+
44
+ attr_reader :state, :page_calculator, :pagination_cache
45
+
46
+ def guard_preload(doc)
47
+ return Result.new(status: :invalid) unless doc
48
+ return Result.new(status: :unavailable) unless dynamic_mode?
49
+
50
+ Result.new(status: :no_calculator) unless page_calculator
51
+ end
52
+
53
+ def resolve_dimensions(requested)
54
+ width = requested.width || state.get(%i[ui terminal_width]) || 80
55
+ height = requested.height || state.get(%i[ui terminal_height]) || 24
56
+ Dimensions.new(width: width, height: height)
57
+ end
58
+
59
+ def resolve_layout(doc, dimensions)
60
+ layout = build_layout_spec(dimensions)
61
+ miss_key = layout.key
62
+ return [layout, miss_key] if pagination_cache.exists_for_document?(doc, layout.key)
63
+
64
+ [find_fallback_layout(doc, layout), miss_key]
65
+ end
66
+
67
+ def dynamic_mode?
68
+ Shoko::Application::Selectors::ConfigSelectors.page_numbering_mode(state) == :dynamic
69
+ end
70
+
71
+ def build_layout_spec(dimensions)
72
+ view_mode = current_view_mode
73
+ line_spacing = current_line_spacing
74
+ kitty_images = Shoko::Adapters::Output::Kitty::KittyGraphics.enabled_for?(state)
75
+ key = pagination_cache.layout_key(
76
+ dimensions.width,
77
+ dimensions.height,
78
+ view_mode,
79
+ line_spacing,
80
+ kitty_images: kitty_images
81
+ )
82
+ LayoutSpec.new(
83
+ key: key,
84
+ width: dimensions.width,
85
+ height: dimensions.height,
86
+ view_mode: view_mode,
87
+ line_spacing: line_spacing,
88
+ kitty_images: kitty_images
89
+ )
90
+ end
91
+
92
+ def apply_layout_config(layout)
93
+ state.apply_terminal_dimensions(layout.width, layout.height)
94
+ update_config(layout)
95
+ end
96
+
97
+ def update_config(layout)
98
+ updates = {}
99
+ updates[%i[config view_mode]] = layout.view_mode if layout.view_mode
100
+ updates[%i[config line_spacing]] = layout.line_spacing if layout.line_spacing
101
+ state.update(updates) unless updates.empty?
102
+ end
103
+
104
+ def load_cached_pages(doc, key)
105
+ cached_pages = pagination_cache.load_for_document(doc, key)
106
+ cached_pages if cached_pages&.any?
107
+ end
108
+
109
+ def hydrate_from_cache(cached_pages, dimensions)
110
+ page_calculator.hydrate_from_cache(
111
+ cached_pages,
112
+ state: state,
113
+ width: dimensions.width,
114
+ height: dimensions.height
115
+ )
116
+ page_calculator.apply_pending_precise_restore!(state)
117
+ end
118
+
119
+ def log_failure(error)
120
+ logger = begin
121
+ state.resolve(:logger)
122
+ rescue StandardError
123
+ nil
124
+ end
125
+ logger&.debug('PaginationCachePreloader: failed', error: error.message)
126
+ end
127
+
128
+ def find_fallback_layout(doc, layout)
129
+ keys = pagination_cache.layout_keys_for_document(doc)
130
+ return nil if keys.empty?
131
+
132
+ preferred = preferred_key(keys, layout)
133
+ return nil unless preferred
134
+
135
+ layout_from_key(preferred)
136
+ end
137
+
138
+ def preferred_key(keys, layout)
139
+ keys.find { |candidate| layout_key_matches?(candidate, layout) }
140
+ end
141
+
142
+ def layout_key_matches?(candidate, layout)
143
+ parsed = pagination_cache.parse_layout_key(candidate)
144
+ return false unless parsed
145
+
146
+ parsed[:view_mode] == layout.view_mode &&
147
+ parsed[:line_spacing] == layout.line_spacing &&
148
+ parsed[:kitty_images] == layout.kitty_images
149
+ end
150
+
151
+ def layout_from_key(key)
152
+ parsed = pagination_cache.parse_layout_key(key)
153
+ return nil unless parsed
154
+
155
+ LayoutSpec.new(
156
+ key: key,
157
+ width: parsed[:width],
158
+ height: parsed[:height],
159
+ view_mode: parsed[:view_mode],
160
+ line_spacing: parsed[:line_spacing],
161
+ kitty_images: parsed[:kitty_images]
162
+ )
163
+ end
164
+
165
+ def current_view_mode
166
+ Shoko::Application::Selectors::ConfigSelectors.view_mode(state)
167
+ end
168
+
169
+ def current_line_spacing
170
+ Shoko::Application::Selectors::ConfigSelectors.line_spacing(state)
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../pagination'
4
+ require_relative 'page_info_calculator'
5
+ require_relative 'pagination_orchestrator'
6
+
7
+ module Shoko::Core::Services::Pagination
8
+ # Coordinates pagination-related workflows for the reader.
9
+ class PaginationCoordinator
10
+ # Aggregated pagination dependencies.
11
+ Dependencies = Struct.new(
12
+ :state,
13
+ :doc,
14
+ :page_calculator,
15
+ :layout_service,
16
+ :terminal_service,
17
+ :pagination_cache,
18
+ :frame_coordinator,
19
+ :ui_controller,
20
+ :render_callback,
21
+ :background_worker_provider,
22
+ keyword_init: true
23
+ )
24
+
25
+ # State flags for deferred pagination behavior.
26
+ Flags = Struct.new(:pending_initial_calculation, :defer_page_map, keyword_init: true)
27
+
28
+ def initialize(dependencies:)
29
+ @deps = dependencies
30
+ @orchestrator = PaginationOrchestrator.new(
31
+ terminal_service: dependencies.terminal_service,
32
+ pagination_cache: dependencies.pagination_cache,
33
+ frame_coordinator: dependencies.frame_coordinator
34
+ )
35
+ @flags = Flags.new(pending_initial_calculation: true, defer_page_map: false)
36
+ seed_flags
37
+ end
38
+
39
+ def pending_initial_calculation?
40
+ @flags.pending_initial_calculation
41
+ end
42
+
43
+ def defer_page_map?
44
+ @flags.defer_page_map
45
+ end
46
+
47
+ def clear_defer_page_map!
48
+ @flags.defer_page_map = false
49
+ end
50
+
51
+ def perform_initial_calculations_if_needed
52
+ perform_initial_calculations_with_progress if pending_initial_calculation? && !preloaded_page_data?
53
+ @flags.pending_initial_calculation = false
54
+ end
55
+
56
+ def schedule_background_page_map_build
57
+ return unless defer_page_map?
58
+
59
+ submit_background_job { build_page_map_in_background }
60
+ rescue StandardError
61
+ @flags.defer_page_map = false
62
+ end
63
+
64
+ def refresh_after_resize(width:, height:)
65
+ return if defer_page_map?
66
+
67
+ session(dimensions: [width, height])&.refresh_after_resize
68
+ end
69
+
70
+ def rebuild_after_config_change
71
+ session(dimensions: terminal_dimensions)&.rebuild_after_config_change
72
+ rescue StandardError
73
+ nil
74
+ end
75
+
76
+ def rebuild_dynamic
77
+ result = session&.rebuild_dynamic
78
+ render_callback&.call
79
+ result
80
+ end
81
+
82
+ def rebuild_pagination(_key = nil)
83
+ rebuild_dynamic
84
+ end
85
+
86
+ def invalidate_cache
87
+ result = session(dimensions: terminal_dimensions)&.invalidate_cache || :missing
88
+ apply_invalidate_message(result)
89
+ :handled
90
+ end
91
+
92
+ def invalidate_pagination_cache(_key = nil)
93
+ invalidate_cache
94
+ end
95
+
96
+ def page_info
97
+ calculator = PageInfoCalculator.new(
98
+ dependencies: PageInfoCalculator::Dependencies.new(
99
+ state: deps.state,
100
+ doc: deps.doc,
101
+ page_calculator: deps.page_calculator,
102
+ layout_service: deps.layout_service,
103
+ terminal_service: deps.terminal_service,
104
+ pagination_orchestrator: @orchestrator
105
+ ),
106
+ defer_page_map: defer_page_map?
107
+ )
108
+ calculator.calculate
109
+ rescue StandardError
110
+ { type: :single, current: 0, total: 0 }
111
+ end
112
+
113
+ private
114
+
115
+ attr_reader :deps
116
+
117
+ def render_callback
118
+ deps.render_callback
119
+ end
120
+
121
+ def background_worker
122
+ provider = deps.background_worker_provider
123
+ provider&.call
124
+ end
125
+
126
+ def terminal_dimensions
127
+ height, width = deps.terminal_service.size
128
+ [width, height]
129
+ end
130
+
131
+ def session(dimensions: nil)
132
+ @orchestrator.session(
133
+ doc: deps.doc,
134
+ state: deps.state,
135
+ page_calculator: deps.page_calculator,
136
+ dimensions: dimensions
137
+ )
138
+ end
139
+
140
+ def perform_initial_calculations_with_progress
141
+ return unless deps.doc
142
+
143
+ session = session(dimensions: terminal_dimensions)
144
+ return unless session
145
+
146
+ session.initial_build
147
+ render_callback&.call
148
+ end
149
+
150
+ def build_page_map_in_background
151
+ session(dimensions: terminal_dimensions)&.build_full_map
152
+ @flags.defer_page_map = false
153
+ render_callback&.call
154
+ rescue StandardError
155
+ @flags.defer_page_map = false
156
+ end
157
+
158
+ def submit_background_job(&)
159
+ worker = background_worker
160
+ if worker
161
+ worker.submit(&)
162
+ else
163
+ Thread.new do
164
+ yield
165
+ rescue StandardError
166
+ # ignore background failures
167
+ end
168
+ end
169
+ end
170
+
171
+ def preloaded_page_data?
172
+ if Shoko::Application::Selectors::ConfigSelectors.page_numbering_mode(deps.state) == :dynamic
173
+ return deps.page_calculator&.total_pages&.positive?
174
+ end
175
+
176
+ deps.state.get(%i[reader total_pages]).to_i.positive?
177
+ end
178
+
179
+ def seed_flags
180
+ return unless deps.doc.respond_to?(:cached?) && deps.doc.cached?
181
+
182
+ @flags.pending_initial_calculation = false
183
+ @flags.defer_page_map = true
184
+ return unless deps.page_calculator && deps.page_calculator.total_pages.to_i.positive?
185
+
186
+ @flags.defer_page_map = false
187
+ end
188
+
189
+ def apply_invalidate_message(result)
190
+ return unless deps.ui_controller
191
+
192
+ case result
193
+ when :deleted
194
+ deps.ui_controller.set_message('Pagination cache cleared')
195
+ when :missing
196
+ deps.ui_controller.set_message('No pagination cache for this layout')
197
+ else
198
+ deps.ui_controller.set_message('Failed to clear pagination cache')
199
+ end
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,291 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../pagination'
4
+ require_relative '../../../adapters/output/kitty/kitty_graphics.rb'
5
+
6
+ module Shoko::Core::Services::Pagination
7
+ # Handles pagination builds (dynamic/absolute) and progress overlay.
8
+ # Keeps heavy orchestration out of ReaderController while preserving behavior.
9
+ class PaginationOrchestrator
10
+ # Factory for selecting per-mode pagination strategies.
11
+ module StrategyFactory
12
+ module_function
13
+
14
+ def select(session)
15
+ mode = Shoko::Application::Selectors::ConfigSelectors.page_numbering_mode(session.state)
16
+ mode == :dynamic ? DynamicStrategy : AbsoluteStrategy
17
+ end
18
+ end
19
+
20
+ # Base strategy type for pagination operations.
21
+ class Strategy
22
+ def initialize(session)
23
+ @session = session
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :session
29
+ end
30
+
31
+ # Dynamic pagination behavior.
32
+ class DynamicStrategy < Strategy
33
+ def build_full_map(progress: nil)
34
+ session.build_dynamic_map(progress: progress)
35
+ nil
36
+ end
37
+
38
+ def build_initial_map(progress:)
39
+ build_full_map(progress: progress)
40
+ end
41
+
42
+ def refresh_after_resize
43
+ session.build_dynamic_map
44
+ session.clamp_dynamic_index!
45
+ end
46
+
47
+ def rebuild_after_config_change
48
+ payload = session.pending_progress_payload
49
+ session.state.dispatch(Shoko::Application::Actions::UpdateSelectionsAction.new(pending_progress: payload))
50
+ session.build_dynamic_map
51
+ session.clamp_dynamic_index!
52
+ end
53
+
54
+ def rebuild_dynamic(progress:)
55
+ payload = session.pending_progress_payload
56
+ session.with_loading('Rebuilding pagination…') do
57
+ session.state.dispatch(Shoko::Application::Actions::UpdateSelectionsAction.new(pending_progress: payload))
58
+ session.build_dynamic_map(progress: progress)
59
+ end
60
+ :handled
61
+ end
62
+ end
63
+
64
+ # Absolute pagination behavior.
65
+ class AbsoluteStrategy < Strategy
66
+ def build_full_map(progress: nil)
67
+ session.build_absolute_map(progress: progress)
68
+ end
69
+
70
+ def build_initial_map(progress:)
71
+ build_full_map(progress: progress)
72
+ end
73
+
74
+ def refresh_after_resize
75
+ session.build_absolute_map
76
+ end
77
+
78
+ def rebuild_after_config_change
79
+ session.build_absolute_map
80
+ end
81
+
82
+ def rebuild_dynamic(progress:)
83
+ :pass
84
+ end
85
+ end
86
+
87
+ # Aggregates pagination inputs and exposes a per-document session API.
88
+ class PaginationSession
89
+ attr_reader :doc, :state, :page_calculator, :dimensions
90
+
91
+ def initialize(doc:, state:, page_calculator:, dimensions:, pagination_cache:, frame_coordinator:)
92
+ @doc = doc
93
+ @state = state
94
+ @page_calculator = page_calculator
95
+ @dimensions = dimensions
96
+ @pagination_cache = pagination_cache
97
+ @frame_coordinator = frame_coordinator
98
+ end
99
+
100
+ def build_full_map!(progress: nil, &block)
101
+ progress ||= block
102
+ strategy.build_full_map(progress: progress)
103
+ end
104
+
105
+ def build_full_map(progress: nil, &)
106
+ build_full_map!(progress: progress, &)
107
+ end
108
+
109
+ def refresh_after_resize
110
+ strategy.refresh_after_resize
111
+ end
112
+
113
+ def rebuild_after_config_change
114
+ strategy.rebuild_after_config_change
115
+ end
116
+
117
+ # Performs the initial pagination calculation with a loading overlay.
118
+ # Returns a hash with optional :page_map_cache for absolute mode.
119
+ def initial_build
120
+ cache = nil
121
+ with_loading('Calculating pages...') do
122
+ map = strategy.build_initial_map(progress: progress_callback)
123
+ cache = map ? build_absolute_cache_entry(map) : nil
124
+ end
125
+ { page_map_cache: cache }
126
+ end
127
+
128
+ # Rebuilds dynamic pagination with a loading overlay and precise restore.
129
+ def rebuild_dynamic
130
+ strategy.rebuild_dynamic(progress: progress_callback)
131
+ end
132
+
133
+ # Remove the cached pagination entry for the supplied dimensions.
134
+ #
135
+ # @return [Symbol] :deleted when cache entry removed, :missing when no entry existed,
136
+ # :error when removal fails.
137
+ def invalidate_cache
138
+ return :missing unless doc && @pagination_cache
139
+
140
+ key = @pagination_cache.layout_key(width, height, view_mode, line_spacing, kitty_images: kitty_images?)
141
+ return :missing unless key && @pagination_cache.exists_for_document?(doc, key)
142
+
143
+ @pagination_cache.delete_for_document(doc, key)
144
+ :deleted
145
+ rescue StandardError
146
+ :error
147
+ end
148
+
149
+ def width
150
+ dimensions[0]
151
+ end
152
+
153
+ def height
154
+ dimensions[1]
155
+ end
156
+
157
+ def view_mode
158
+ Shoko::Application::Selectors::ConfigSelectors.view_mode(state)
159
+ end
160
+
161
+ def line_spacing
162
+ Shoko::Application::Selectors::ConfigSelectors.line_spacing(state)
163
+ end
164
+
165
+ def kitty_images?
166
+ Shoko::Adapters::Output::Kitty::KittyGraphics.enabled_for?(state)
167
+ end
168
+
169
+ def pending_progress_payload
170
+ current_chapter = state.get(%i[reader current_chapter]) || 0
171
+ current_index = state.get(%i[reader current_page_index]).to_i
172
+ page = page_calculator.get_page(current_index)
173
+ {
174
+ chapter_index: current_chapter,
175
+ line_offset: page ? page[:start_line] : 0,
176
+ }
177
+ end
178
+
179
+ def build_dynamic_map(progress: nil)
180
+ Shoko::Adapters::Monitoring::PerfTracer.measure('pagination.build') do
181
+ page_calculator.build_dynamic_map!(width, height, doc, state) do |done, total|
182
+ progress&.call(done, total)
183
+ end
184
+ end
185
+ page_calculator.apply_pending_precise_restore!(state)
186
+ end
187
+
188
+ def build_absolute_map(progress: nil)
189
+ Shoko::Adapters::Monitoring::PerfTracer.measure('pagination.build') do
190
+ page_calculator.build_absolute_map!(width, height, doc, state) do |done, total|
191
+ progress&.call(done, total)
192
+ end
193
+ end
194
+ end
195
+
196
+ def clamp_dynamic_index!
197
+ total = page_calculator.total_pages.to_i
198
+ return if total <= 0
199
+
200
+ current = state.get(%i[reader current_page_index]).to_i
201
+ clamped = current.clamp(0, total - 1)
202
+ state.dispatch(
203
+ Shoko::Application::Actions::UpdatePageAction.new(current_page_index: clamped)
204
+ )
205
+ end
206
+
207
+ def progress_callback
208
+ ->(done, total) { update_progress(done, total) }
209
+ end
210
+
211
+ def with_loading(message)
212
+ begin_loading(message)
213
+ yield
214
+ ensure
215
+ end_loading
216
+ end
217
+
218
+ def begin_loading(message)
219
+ state.dispatch(Shoko::Application::Actions::UpdateUILoadingAction.new(
220
+ loading_active: true,
221
+ loading_message: message,
222
+ loading_progress: 0.0
223
+ ))
224
+ @frame_coordinator&.render_loading_overlay
225
+ end
226
+
227
+ def end_loading
228
+ state.dispatch(Shoko::Application::Actions::UpdateUILoadingAction.new(
229
+ loading_active: false,
230
+ loading_message: nil
231
+ ))
232
+ end
233
+
234
+ def update_progress(done, total)
235
+ progress = Shoko::Core::Services::ProgressHelper.ratio(done, total)
236
+ state.dispatch(Shoko::Application::Actions::UpdateUILoadingAction.new(
237
+ loading_progress: progress
238
+ ))
239
+ @frame_coordinator&.render_loading_overlay
240
+ end
241
+
242
+ def build_absolute_cache_entry(page_map)
243
+ key = @pagination_cache&.layout_key(
244
+ width,
245
+ height,
246
+ view_mode,
247
+ line_spacing,
248
+ kitty_images: kitty_images?
249
+ )
250
+ {
251
+ key: key,
252
+ map: page_map,
253
+ total: Array(page_map).sum,
254
+ }
255
+ end
256
+
257
+ private
258
+
259
+ def strategy
260
+ @strategy ||= StrategyFactory.select(self).new(self)
261
+ end
262
+ end
263
+
264
+ def initialize(terminal_service:, pagination_cache: nil, frame_coordinator: nil)
265
+ @terminal_service = terminal_service
266
+ @pagination_cache = pagination_cache
267
+ @frame_coordinator = frame_coordinator
268
+ end
269
+
270
+ def session(doc:, state:, page_calculator:, dimensions: nil)
271
+ return nil unless doc && page_calculator
272
+
273
+ dims = dimensions || terminal_dimensions
274
+ PaginationSession.new(
275
+ doc: doc,
276
+ state: state,
277
+ page_calculator: page_calculator,
278
+ dimensions: dims,
279
+ pagination_cache: @pagination_cache,
280
+ frame_coordinator: @frame_coordinator
281
+ )
282
+ end
283
+
284
+ private
285
+
286
+ def terminal_dimensions
287
+ height, width = @terminal_service.size
288
+ [width, height]
289
+ end
290
+ end
291
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Core
5
+ module Services
6
+ module Pagination
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Core
5
+ module Services
6
+ # Utility helpers for normalizing progress metrics across menu and reader flows.
7
+ module ProgressHelper
8
+ module_function
9
+
10
+ # Normalize partial progress against a total, guarding against zero denominators.
11
+ #
12
+ # @param done [Numeric]
13
+ # @param total [Numeric]
14
+ # @return [Float]
15
+ def ratio(done, total)
16
+ denom = [total.to_f, 1.0].max
17
+ done.to_f / denom
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end