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,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Application
5
+ # Orchestrates reader startup steps: terminal prep, progress restore,
6
+ # pagination preload, and background data loads.
7
+ class ReaderStartupOrchestrator
8
+ def initialize(dependencies)
9
+ @dependencies = dependencies
10
+ @terminal_service = @dependencies.resolve(:terminal_service)
11
+ end
12
+
13
+ # Execute startup sequence using the controller as context
14
+ # @param controller [Shoko::ReaderController]
15
+ def start(controller)
16
+ instrumentation = instrumentation_service
17
+ wrap_with_instrumentation(instrumentation, 'startup.reader') do
18
+ state = controller.state
19
+ page_calculator = controller.page_calculator
20
+ doc = controller.doc
21
+
22
+ # Query terminal size (FrameCoordinator will update state during rendering)
23
+ height, width = begin
24
+ @terminal_service.size
25
+ rescue StandardError
26
+ [nil, nil]
27
+ end
28
+
29
+ # Load progress after terminal is ready
30
+ sc = safe_resolve_state_controller
31
+ sc&.load_progress
32
+
33
+ if doc.respond_to?(:cached?) && doc.cached?
34
+ preloader = resolve_pagination_preloader(state, page_calculator)
35
+ result = preloader&.preload(doc, width:, height:)
36
+ controller.clear_defer_page_map! if result && result.status == :hit
37
+ end
38
+
39
+ # Perform initial calculations if needed
40
+ controller.perform_initial_calculations_if_needed if controller.pending_initial_calculation?
41
+
42
+ # Schedule background page-map build for instant-open path
43
+ controller.schedule_background_page_map_build if controller.defer_page_map?
44
+
45
+ # Background load bookmarks and annotations
46
+ submit_background_job(sc) do
47
+ svc = sc || safe_resolve_state_controller
48
+ if svc
49
+ svc.load_bookmarks
50
+ svc.refresh_annotations
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def wrap_with_instrumentation(instrumentation, metric, &)
59
+ if instrumentation
60
+ instrumentation.time(metric, &)
61
+ else
62
+ yield
63
+ end
64
+ end
65
+
66
+ def instrumentation_service
67
+ @instrumentation_service ||= begin
68
+ @dependencies.resolve(:instrumentation_service)
69
+ rescue StandardError
70
+ nil
71
+ end
72
+ end
73
+
74
+ def safe_resolve_state_controller
75
+ @dependencies.resolve(:state_controller)
76
+ rescue StandardError
77
+ nil
78
+ end
79
+
80
+ def submit_background_job(_initial_state_controller, &)
81
+ worker = resolve_background_worker
82
+ if worker
83
+ worker.submit(&)
84
+ else
85
+ Thread.new do
86
+ yield
87
+ rescue StandardError
88
+ # ignore background failures
89
+ end
90
+ end
91
+ rescue StandardError
92
+ # ignore background failures
93
+ nil
94
+ end
95
+
96
+ def resolve_background_worker
97
+ return nil unless @dependencies.respond_to?(:resolve)
98
+
99
+ @dependencies.resolve(:background_worker)
100
+ rescue StandardError
101
+ nil
102
+ end
103
+
104
+ def resolve_pagination_preloader(_state, _page_calculator)
105
+ return nil unless @dependencies.respond_to?(:resolve)
106
+
107
+ @dependencies.resolve(:pagination_cache_preloader)
108
+ rescue StandardError
109
+ nil
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Application
5
+ module Selectors
6
+ # Selectors for configuration state
7
+ module ConfigSelectors
8
+ def self.view_mode(state)
9
+ state.get(%i[config view_mode])
10
+ end
11
+
12
+ def self.line_spacing(state)
13
+ state.get(%i[config line_spacing])
14
+ end
15
+
16
+ def self.page_numbering_mode(state)
17
+ state.get(%i[config page_numbering_mode])
18
+ end
19
+
20
+ def self.theme(state)
21
+ state.get(%i[config theme])
22
+ end
23
+
24
+ def self.show_page_numbers(state)
25
+ state.get(%i[config show_page_numbers])
26
+ end
27
+
28
+ def self.show_page_numbers?(state)
29
+ show_page_numbers(state)
30
+ end
31
+
32
+ def self.highlight_quotes(state)
33
+ state.get(%i[config highlight_quotes])
34
+ end
35
+
36
+ def self.highlight_quotes?(state)
37
+ highlight_quotes(state)
38
+ end
39
+
40
+ def self.highlight_keywords(state)
41
+ state.get(%i[config highlight_keywords])
42
+ end
43
+
44
+ def self.highlight_keywords?(state)
45
+ highlight_keywords(state)
46
+ end
47
+
48
+ def self.kitty_images(state)
49
+ state.get(%i[config kitty_images])
50
+ end
51
+
52
+ def self.kitty_images?(state)
53
+ !!kitty_images(state)
54
+ end
55
+
56
+ def self.config_hash(state)
57
+ state.get([:config])
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Application
5
+ module Selectors
6
+ # Selectors for menu state - provides read-only access to state
7
+ module MenuSelectors
8
+ def self.selected(state)
9
+ state.get(%i[menu selected])
10
+ end
11
+
12
+ def self.selected_item(state)
13
+ selected(state)
14
+ end
15
+
16
+ def self.mode(state)
17
+ state.get(%i[menu mode])
18
+ end
19
+
20
+ def self.browse_selected(state)
21
+ state.get(%i[menu browse_selected])
22
+ end
23
+
24
+ def self.search_query(state)
25
+ state.get(%i[menu search_query]) || ''
26
+ end
27
+
28
+ def self.search_cursor(state)
29
+ state.get(%i[menu search_cursor])
30
+ end
31
+
32
+ def self.search_active(state)
33
+ state.get(%i[menu search_active])
34
+ end
35
+
36
+ def self.search_active?(state)
37
+ search_active(state)
38
+ end
39
+
40
+ def self.download_query(state)
41
+ state.get(%i[menu download_query]) || ''
42
+ end
43
+
44
+ def self.download_cursor(state)
45
+ state.get(%i[menu download_cursor])
46
+ end
47
+
48
+ def self.download_selected(state)
49
+ state.get(%i[menu download_selected])
50
+ end
51
+
52
+ def self.download_status(state)
53
+ state.get(%i[menu download_status])
54
+ end
55
+
56
+ def self.download_progress(state)
57
+ state.get(%i[menu download_progress])
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../adapters/output/render_registry.rb'
4
+
5
+ module Shoko
6
+ module Application
7
+ module Selectors
8
+ # Selectors for reader state - provides read-only access to state
9
+ # Replaces direct state access and convenience methods
10
+ module ReaderSelectors
11
+ # Chapter and page selectors
12
+ def self.current_chapter(state)
13
+ state.get(%i[reader current_chapter])
14
+ end
15
+
16
+ def self.current_page_index(state)
17
+ state.get(%i[reader current_page_index])
18
+ end
19
+
20
+ def self.current_page(state)
21
+ current_page_index(state) + 1
22
+ end
23
+
24
+ def self.left_page(state)
25
+ state.get(%i[reader left_page])
26
+ end
27
+
28
+ def self.right_page(state)
29
+ state.get(%i[reader right_page])
30
+ end
31
+
32
+ def self.single_page(state)
33
+ state.get(%i[reader single_page])
34
+ end
35
+
36
+ # Mode and UI state selectors
37
+ def self.mode(state)
38
+ state.get(%i[reader mode])
39
+ end
40
+
41
+ def self.selection(state)
42
+ state.get(%i[reader selection])
43
+ end
44
+
45
+ def self.message(state)
46
+ state.get(%i[reader message])
47
+ end
48
+
49
+ def self.running(state)
50
+ state.get(%i[reader running])
51
+ end
52
+
53
+ def self.running?(state)
54
+ running(state)
55
+ end
56
+
57
+ # List selectors
58
+ def self.bookmarks(state)
59
+ state.get(%i[reader bookmarks]) || []
60
+ end
61
+
62
+ def self.annotations(state)
63
+ state.get(%i[reader annotations]) || []
64
+ end
65
+
66
+ # Pagination selectors
67
+ def self.page_map(state)
68
+ state.get(%i[reader page_map]) || []
69
+ end
70
+
71
+ def self.total_pages(state)
72
+ state.get(%i[reader total_pages])
73
+ end
74
+
75
+ def self.pages_per_chapter(state)
76
+ state.get(%i[reader pages_per_chapter]) || []
77
+ end
78
+
79
+ # Dynamic pagination selectors
80
+ def self.dynamic_page_map(state)
81
+ state.get(%i[reader dynamic_page_map])
82
+ end
83
+
84
+ def self.dynamic_total_pages(state)
85
+ state.get(%i[reader dynamic_total_pages])
86
+ end
87
+
88
+ def self.dynamic_chapter_starts(state)
89
+ state.get(%i[reader dynamic_chapter_starts]) || []
90
+ end
91
+
92
+ # Terminal sizing selectors
93
+ def self.last_width(state)
94
+ state.get(%i[reader last_width])
95
+ end
96
+
97
+ def self.last_height(state)
98
+ state.get(%i[reader last_height])
99
+ end
100
+
101
+ def self.last_dynamic_width(state)
102
+ state.get(%i[reader last_dynamic_width])
103
+ end
104
+
105
+ def self.last_dynamic_height(state)
106
+ state.get(%i[reader last_dynamic_height])
107
+ end
108
+
109
+ def self.terminal_size_changed?(state, width, height)
110
+ width != last_width(state) || height != last_height(state)
111
+ end
112
+
113
+ # UI state selectors
114
+ def self.rendered_lines(state)
115
+ registry = begin
116
+ state.resolve(:render_registry)
117
+ rescue StandardError
118
+ nil
119
+ end
120
+ registry ||= begin
121
+ Shoko::Adapters::Output::RenderRegistry.current
122
+ rescue StandardError
123
+ nil
124
+ end
125
+ lines = registry&.lines
126
+ return lines if lines && !lines.empty?
127
+
128
+ state.get(%i[reader rendered_lines]) || {}
129
+ end
130
+
131
+ def self.popup_menu(state)
132
+ state.get(%i[reader popup_menu])
133
+ end
134
+
135
+ def self.annotations_overlay(state)
136
+ state.get(%i[reader annotations_overlay])
137
+ end
138
+
139
+ def self.annotation_editor_overlay(state)
140
+ state.get(%i[reader annotation_editor_overlay])
141
+ end
142
+
143
+ # Sidebar selectors
144
+ def self.sidebar_visible(state)
145
+ state.get(%i[reader sidebar_visible])
146
+ end
147
+
148
+ def self.sidebar_visible?(state)
149
+ sidebar_visible(state)
150
+ end
151
+
152
+ def self.sidebar_active_tab(state)
153
+ state.get(%i[reader sidebar_active_tab])
154
+ end
155
+
156
+ def self.sidebar_toc_selected(state)
157
+ state.get(%i[reader sidebar_toc_selected])
158
+ end
159
+
160
+ def self.sidebar_toc_collapsed(state)
161
+ state.get(%i[reader sidebar_toc_collapsed])
162
+ end
163
+
164
+ def self.sidebar_annotations_selected(state)
165
+ state.get(%i[reader sidebar_annotations_selected])
166
+ end
167
+
168
+ def self.sidebar_bookmarks_selected(state)
169
+ state.get(%i[reader sidebar_bookmarks_selected])
170
+ end
171
+
172
+ def self.sidebar_toc_filter(state)
173
+ state.get(%i[reader sidebar_toc_filter])
174
+ end
175
+
176
+ def self.sidebar_toc_filter_active(state)
177
+ state.get(%i[reader sidebar_toc_filter_active])
178
+ end
179
+
180
+ def self.sidebar_toc_filter_active?(state)
181
+ sidebar_toc_filter_active(state)
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Application
5
+ module Actions
6
+ # Base action for immutable-like state updates via StateStore#dispatch
7
+ class BaseAction
8
+ def initialize(payload = {})
9
+ @payload = payload
10
+ end
11
+
12
+ # Apply the action to the given StateStore instance
13
+ # Implement in subclasses
14
+ def apply(_state)
15
+ raise NotImplementedError, 'Action subclasses must implement #apply(state)'
16
+ end
17
+
18
+ protected
19
+
20
+ attr_reader :payload
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_action'
4
+
5
+ module Shoko
6
+ module Application
7
+ module Actions
8
+ # Stop the reader loop (used to return to menu)
9
+ class QuitToMenuAction < BaseAction
10
+ def apply(state)
11
+ state.update({ %i[reader running] => false })
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_action'
4
+
5
+ module Shoko
6
+ module Application
7
+ module Actions
8
+ # Switch reader mode: :read, :help
9
+ class SwitchReaderModeAction < BaseAction
10
+ VALID = %i[read help].freeze
11
+
12
+ def apply(state)
13
+ mode = (payload[:mode] || :read).to_sym
14
+ return state.get(%i[reader mode]) unless VALID.include?(mode)
15
+
16
+ state.update({ %i[reader mode] => mode })
17
+ mode
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_action'
4
+
5
+ module Shoko
6
+ module Application
7
+ module Actions
8
+ # Toggle reader view between :split and :single
9
+ class ToggleViewModeAction < BaseAction
10
+ def apply(state)
11
+ current = state.get(%i[config view_mode]) || :split
12
+ new_mode = current == :split ? :single : :split
13
+
14
+ # Update all state changes atomically
15
+ state.update({
16
+ %i[config view_mode] => new_mode,
17
+ %i[reader last_width] => 0,
18
+ %i[reader last_height] => 0,
19
+ %i[reader dynamic_page_map] => nil,
20
+ %i[reader dynamic_total_pages] => 0,
21
+ %i[reader last_dynamic_width] => 0,
22
+ %i[reader last_dynamic_height] => 0,
23
+ })
24
+ state.save_config if state.respond_to?(:save_config)
25
+
26
+ new_mode
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_action'
4
+
5
+ module Shoko
6
+ module Application
7
+ module Actions
8
+ # Stores the annotation editor overlay component in state.
9
+ class UpdateAnnotationEditorOverlayAction < BaseAction
10
+ def initialize(overlay)
11
+ super(overlay: overlay)
12
+ end
13
+
14
+ def apply(state)
15
+ state.update({ %i[reader annotation_editor_overlay] => payload[:overlay] })
16
+ end
17
+ end
18
+
19
+ # Clears any active annotation editor overlay from state.
20
+ class ClearAnnotationEditorOverlayAction < UpdateAnnotationEditorOverlayAction
21
+ def initialize
22
+ super(nil)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_action'
4
+
5
+ module Shoko
6
+ module Application
7
+ module Actions
8
+ # Action for updating annotations list
9
+ class UpdateAnnotationsAction < BaseAction
10
+ def initialize(annotations)
11
+ super(annotations: annotations)
12
+ end
13
+
14
+ def apply(state)
15
+ state.update({ %i[reader annotations] => payload[:annotations] })
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_action'
4
+
5
+ module Shoko
6
+ module Application
7
+ module Actions
8
+ # Updates the reader annotations overlay component stored in state.
9
+ class UpdateAnnotationsOverlayAction < BaseAction
10
+ def initialize(overlay)
11
+ super(overlay: overlay)
12
+ end
13
+
14
+ def apply(state)
15
+ state.update({ %i[reader annotations_overlay] => payload[:overlay] })
16
+ end
17
+ end
18
+
19
+ # Clears any active annotations overlay.
20
+ class ClearAnnotationsOverlayAction < UpdateAnnotationsOverlayAction
21
+ def initialize
22
+ super(nil)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_action'
4
+
5
+ module Shoko
6
+ module Application
7
+ module Actions
8
+ # Action for updating the bookmarks list
9
+ class UpdateBookmarksAction < BaseAction
10
+ def initialize(bookmarks)
11
+ super(bookmarks: bookmarks)
12
+ end
13
+
14
+ def apply(state)
15
+ state.update({ %i[reader bookmarks] => payload[:bookmarks] })
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_action'
4
+
5
+ module Shoko
6
+ module Application
7
+ module Actions
8
+ # Action for updating the current chapter
9
+ class UpdateChapterAction < BaseAction
10
+ def initialize(chapter_index)
11
+ super(chapter_index: chapter_index)
12
+ end
13
+
14
+ def apply(state)
15
+ idx = payload[:chapter_index]
16
+ updates = {
17
+ %i[reader current_chapter] => idx,
18
+ }
19
+ state.update(updates)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_action'
4
+
5
+ module Shoko
6
+ module Application
7
+ module Actions
8
+ # Action for updating configuration values
9
+ class UpdateConfigAction < BaseAction
10
+ def apply(state)
11
+ # Build update hash for atomic state update
12
+ updates = {}
13
+ payload.each do |config_field, value|
14
+ updates[[:config, config_field]] = value
15
+ end
16
+ state.update(updates)
17
+ state.save_config if state.respond_to?(:save_config)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_action'
4
+
5
+ module Shoko
6
+ module Application
7
+ module Actions
8
+ # Shared helpers for applying whitelisted state update fields.
9
+ module UpdateFieldHelpers
10
+ module_function
11
+
12
+ # Apply only allowed fields under a namespaced state path
13
+ # e.g., namespace = :reader results in updates to [:reader, field]
14
+ def apply_allowed(state, payload, allowed, namespace: :reader)
15
+ updates = {}
16
+ payload.each do |field, value|
17
+ next unless allowed.include?(field)
18
+
19
+ updates[[namespace, field]] = value
20
+ end
21
+ state.update(updates) unless updates.empty?
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end