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,21 @@
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 menu-related state under [:menu, *]
9
+ class UpdateMenuAction < BaseAction
10
+ # Payload is a hash of menu_field => value
11
+ def apply(state)
12
+ updates = {}
13
+ payload.each do |field, value|
14
+ updates[[:menu, field]] = value
15
+ end
16
+ state.update(updates)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_action'
4
+ require_relative '../../../adapters/output/terminal/terminal_sanitizer.rb'
5
+
6
+ module Shoko
7
+ module Application
8
+ module Actions
9
+ # Action for updating the status message
10
+ class UpdateMessageAction < BaseAction
11
+ def initialize(message)
12
+ super(message: message)
13
+ end
14
+
15
+ def apply(state)
16
+ msg = payload[:message]
17
+ safe = if msg.nil?
18
+ nil
19
+ else
20
+ Shoko::Adapters::Output::Terminal::TerminalSanitizer.sanitize(msg.to_s, preserve_newlines: false,
21
+ preserve_tabs: false)
22
+ end
23
+ state.update({ %i[reader message] => safe })
24
+ end
25
+ end
26
+
27
+ # Convenience action for clearing message
28
+ class ClearMessageAction < UpdateMessageAction
29
+ def initialize
30
+ super(nil)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,21 @@
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 page positions (current_page_index, left_page, right_page, single_page)
9
+ class UpdatePageAction < BaseAction
10
+ def apply(state)
11
+ # Build update hash for atomic state update
12
+ updates = {}
13
+ payload.each do |page_field, value|
14
+ updates[[:reader, page_field]] = value
15
+ end
16
+ state.update(updates)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_action'
4
+
5
+ require_relative 'update_field_helpers'
6
+
7
+ module Shoko
8
+ module Application
9
+ module Actions
10
+ # Action to update pagination-related reader state in a single, consistent way.
11
+ # Allowed fields: :page_map, :total_pages, :last_width, :last_height, :total_chapters
12
+ class UpdatePaginationStateAction < BaseAction
13
+ ALLOWED = %i[page_map total_pages last_width last_height total_chapters].freeze
14
+
15
+ def apply(state)
16
+ UpdateFieldHelpers.apply_allowed(state, payload, ALLOWED, namespace: :reader)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ 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
+ # Action for updating popup menu state
9
+ class UpdatePopupMenuAction < BaseAction
10
+ def initialize(popup_menu)
11
+ super(popup_menu: popup_menu)
12
+ end
13
+
14
+ def apply(state)
15
+ state.update({ %i[reader popup_menu] => payload[:popup_menu] })
16
+ end
17
+ end
18
+
19
+ # Convenience action for clearing popup menu
20
+ class ClearPopupMenuAction < UpdatePopupMenuAction
21
+ def initialize
22
+ super(nil)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_action'
4
+
5
+ require_relative 'update_field_helpers'
6
+
7
+ module Shoko
8
+ module Application
9
+ module Actions
10
+ # Action to update reader meta fields that are not pagination specific
11
+ # Allowed fields: :book_path, :running
12
+ class UpdateReaderMetaAction < BaseAction
13
+ ALLOWED = %i[book_path running].freeze
14
+
15
+ def apply(state)
16
+ UpdateFieldHelpers.apply_allowed(state, payload, ALLOWED, namespace: :reader)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ 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 reader mode (read/help/annotation editor).
9
+ class UpdateReaderModeAction < BaseAction
10
+ def initialize(new_mode)
11
+ super(mode: new_mode)
12
+ end
13
+
14
+ def apply(state)
15
+ state.update({ %i[reader mode] => payload[:mode] })
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_action'
4
+ require_relative '../../../adapters/output/render_registry.rb'
5
+
6
+ module Shoko
7
+ module Application
8
+ module Actions
9
+ # Action for updating rendered lines cache
10
+ class UpdateRenderedLinesAction < BaseAction
11
+ def initialize(rendered_lines)
12
+ super(rendered_lines: rendered_lines)
13
+ end
14
+
15
+ def apply(state)
16
+ registry = begin
17
+ state.resolve(:render_registry) if state.respond_to?(:resolve)
18
+ rescue StandardError
19
+ nil
20
+ end
21
+ registry ||= begin
22
+ Shoko::Adapters::Output::RenderRegistry.current
23
+ rescue StandardError
24
+ nil
25
+ end
26
+ registry&.write(payload[:rendered_lines])
27
+ # Keep state entry lightweight for observers; avoid storing the large hash
28
+ state.update({ %i[reader rendered_lines] => :render_registry })
29
+ end
30
+ end
31
+
32
+ # Convenience action for clearing rendered lines
33
+ class ClearRenderedLinesAction < UpdateRenderedLinesAction
34
+ def initialize
35
+ super({})
36
+ end
37
+ end
38
+ end
39
+ end
40
+ 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
+ # Action for updating text selection state
9
+ class UpdateSelectionAction < BaseAction
10
+ def initialize(selection)
11
+ super(selection: selection)
12
+ end
13
+
14
+ def apply(state)
15
+ state.update({ %i[reader selection] => payload[:selection] })
16
+ end
17
+ end
18
+
19
+ # Convenience action for clearing selection
20
+ class ClearSelectionAction < UpdateSelectionAction
21
+ def initialize
22
+ super(nil)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,21 @@
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 various selection states
9
+ class UpdateSelectionsAction < BaseAction
10
+ def apply(state)
11
+ # Build update hash for atomic state update
12
+ updates = {}
13
+ payload.each do |field, value|
14
+ updates[[:reader, field]] = value
15
+ end
16
+ state.update(updates)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,34 @@
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 sidebar state
9
+ class UpdateSidebarAction < BaseAction
10
+ def apply(state)
11
+ # Build update hash for atomic state update
12
+ updates = {}
13
+ payload.each do |field, value|
14
+ case field
15
+ when :visible
16
+ updates[%i[reader sidebar_visible]] = value
17
+ when :active_tab
18
+ updates[%i[reader sidebar_active_tab]] = value
19
+ when :toc_selected
20
+ updates[%i[reader sidebar_toc_selected]] = value
21
+ when :toc_collapsed
22
+ updates[%i[reader sidebar_toc_collapsed]] = value
23
+ when :annotations_selected
24
+ updates[%i[reader sidebar_annotations_selected]] = value
25
+ when :bookmarks_selected
26
+ updates[%i[reader sidebar_bookmarks_selected]] = value
27
+ end
28
+ end
29
+ state.update(updates)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,23 @@
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 UI loading indicators
9
+ # Accepts any of: :loading_active, :loading_message, :loading_progress
10
+ class UpdateUILoadingAction < BaseAction
11
+ def apply(state)
12
+ updates = {}
13
+ payload.each do |field, value|
14
+ next unless %i[loading_active loading_message loading_progress].include?(field)
15
+
16
+ updates[[:ui, field]] = value
17
+ end
18
+ state.update(updates) unless updates.empty?
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Application
5
+ module UI
6
+ # Builds ReaderViewModel from state and document, keeping controller lean.
7
+ class ReaderViewModelBuilder
8
+ def initialize(state, doc)
9
+ @state = state
10
+ @doc = doc
11
+ end
12
+
13
+ def build(page_info)
14
+ ViewModels::ReaderViewModel.new(**attributes(page_info))
15
+ end
16
+
17
+ private
18
+
19
+ def attributes(page_info)
20
+ base_attributes.merge(page_info: page_info)
21
+ end
22
+
23
+ def base_attributes
24
+ {
25
+ current_chapter: current_chapter_index,
26
+ total_chapters: total_chapter_count,
27
+ current_page: state_value(%i[reader current_page]),
28
+ total_pages: state_value(%i[reader total_pages]),
29
+ chapter_title: chapter_title(current_chapter_index),
30
+ document_title: @doc&.title || '',
31
+ view_mode: state_value(%i[config view_mode], :split),
32
+ sidebar_visible: state_value(%i[reader sidebar_visible]),
33
+ mode: state_value(%i[reader mode]),
34
+ message: state_value(%i[reader message]),
35
+ bookmarks: state_value(%i[reader bookmarks], []),
36
+ toc_entries: doc_toc_entries,
37
+ show_page_numbers: state_value(%i[config show_page_numbers], true),
38
+ page_numbering_mode: state_value(%i[config page_numbering_mode], :dynamic),
39
+ line_spacing: state_value(%i[config line_spacing], Shoko::Core::Models::ReaderSettings::DEFAULT_LINE_SPACING),
40
+ language: @doc&.language || 'en',
41
+ }
42
+ end
43
+
44
+ def current_chapter_index
45
+ state_value(%i[reader current_chapter])
46
+ end
47
+
48
+ def total_chapter_count
49
+ Array(@doc&.chapters).length
50
+ end
51
+
52
+ def chapter_title(index)
53
+ chapter = @doc&.get_chapter(index)
54
+ chapter&.title || ''
55
+ rescue StandardError
56
+ ''
57
+ end
58
+
59
+ def doc_toc_entries
60
+ return [] unless @doc.respond_to?(:toc_entries)
61
+
62
+ Array(@doc.toc_entries)
63
+ rescue StandardError
64
+ []
65
+ end
66
+
67
+ def state_value(path, default = nil)
68
+ value = @state.get(path)
69
+ value.nil? ? default : value
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Application::UI
5
+ module ViewModels
6
+ # Pure data structure for reader view rendering.
7
+ # Eliminates component coupling to controllers and services.
8
+ class ReaderViewModel
9
+ attr_reader :current_chapter, :total_chapters, :current_page, :total_pages,
10
+ :chapter_title, :document_title, :view_mode, :sidebar_visible, :mode, :message,
11
+ :bookmarks, :toc_entries, :content_lines, :page_info, :show_page_numbers,
12
+ :page_numbering_mode, :line_spacing, :language
13
+
14
+ def initialize(
15
+ current_chapter: 0,
16
+ total_chapters: 0,
17
+ current_page: 0,
18
+ total_pages: 0,
19
+ chapter_title: '',
20
+ document_title: '',
21
+ view_mode: :split,
22
+ sidebar_visible: false,
23
+ mode: :read,
24
+ message: nil,
25
+ bookmarks: [],
26
+ toc_entries: [],
27
+ content_lines: [],
28
+ page_info: {},
29
+ show_page_numbers: true,
30
+ page_numbering_mode: :dynamic,
31
+ line_spacing: :compact,
32
+ language: 'en'
33
+ )
34
+ @current_chapter = current_chapter
35
+ @total_chapters = total_chapters
36
+ @current_page = current_page
37
+ @total_pages = total_pages
38
+ @chapter_title = chapter_title
39
+ @document_title = document_title
40
+ @view_mode = view_mode
41
+ @sidebar_visible = sidebar_visible
42
+ @mode = mode
43
+ @message = message
44
+ @bookmarks = bookmarks
45
+ @toc_entries = toc_entries
46
+ @content_lines = content_lines
47
+ @page_info = page_info
48
+ @show_page_numbers = show_page_numbers
49
+ @page_numbering_mode = page_numbering_mode
50
+ @line_spacing = line_spacing
51
+ @language = language
52
+ freeze
53
+ end
54
+
55
+ # Derived properties
56
+ def progress_percentage
57
+ return 0 if total_pages.zero?
58
+
59
+ ((current_page.to_f / total_pages) * 100).round(1)
60
+ end
61
+
62
+ def chapter_progress
63
+ return '0/0' if total_chapters.zero?
64
+
65
+ "#{current_chapter + 1}/#{total_chapters}"
66
+ end
67
+
68
+ def page_progress
69
+ return '0/0' if total_pages.zero?
70
+
71
+ "#{current_page + 1}/#{total_pages}"
72
+ end
73
+
74
+ def split_mode?
75
+ view_mode == :split
76
+ end
77
+
78
+ def single_mode?
79
+ view_mode == :single
80
+ end
81
+
82
+ def message?
83
+ !message.nil? && !message.empty?
84
+ end
85
+
86
+ def bookmarks?
87
+ !bookmarks.empty?
88
+ end
89
+
90
+ def toc?
91
+ !toc_entries.empty?
92
+ end
93
+
94
+ # Create new instance with updates
95
+ def with(**changes)
96
+ current_attributes = {
97
+ current_chapter: current_chapter,
98
+ total_chapters: total_chapters,
99
+ current_page: current_page,
100
+ total_pages: total_pages,
101
+ chapter_title: chapter_title,
102
+ document_title: document_title,
103
+ view_mode: view_mode,
104
+ sidebar_visible: sidebar_visible,
105
+ mode: mode,
106
+ message: message,
107
+ bookmarks: bookmarks,
108
+ toc_entries: toc_entries,
109
+ content_lines: content_lines,
110
+ page_info: page_info,
111
+ show_page_numbers: show_page_numbers,
112
+ page_numbering_mode: page_numbering_mode,
113
+ line_spacing: line_spacing,
114
+ language: language,
115
+ }
116
+
117
+ self.class.new(**current_attributes, **changes)
118
+ end
119
+ end
120
+
121
+ # View model for menu screens (main/browse/settings/annotations).
122
+ class MenuViewModel
123
+ attr_reader :mode, :selected_index, :items, :search_query, :search_active,
124
+ :message, :title
125
+
126
+ def initialize(
127
+ mode: :main,
128
+ selected_index: 0,
129
+ items: [],
130
+ search_query: '',
131
+ search_active: false,
132
+ message: nil,
133
+ title: 'Shoko'
134
+ )
135
+ @mode = mode
136
+ @selected_index = selected_index
137
+ @items = items
138
+ @search_query = search_query
139
+ @search_active = search_active
140
+ @message = message
141
+ @title = title
142
+ freeze
143
+ end
144
+
145
+ def selected_item
146
+ items[selected_index]
147
+ end
148
+
149
+ def items?
150
+ !items.empty?
151
+ end
152
+
153
+ def message?
154
+ !message.nil? && !message.empty?
155
+ end
156
+
157
+ def searching?
158
+ search_active
159
+ end
160
+
161
+ def with(**changes)
162
+ current_attributes = {
163
+ mode: mode,
164
+ selected_index: selected_index,
165
+ items: items,
166
+ search_query: search_query,
167
+ search_active: search_active,
168
+ message: message,
169
+ title: title,
170
+ }
171
+
172
+ self.class.new(**current_attributes, **changes)
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Application
5
+ # Unified application entry point that handles both file and menu scenarios
6
+ class UnifiedApplication
7
+ def initialize(epub_path = nil)
8
+ @epub_path = epub_path
9
+ @dependencies = Shoko::Application::ContainerFactory.create_default_container
10
+ @instrumentation = begin
11
+ @dependencies.resolve(:instrumentation_service)
12
+ rescue StandardError
13
+ nil
14
+ end
15
+ end
16
+
17
+ def run
18
+ if @epub_path
19
+ reader_mode
20
+ else
21
+ menu_mode
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def reader_mode
28
+ # Ensure alternate screen is entered before any heavy work for instant-open UX
29
+ term = @dependencies.resolve(:terminal_service)
30
+ term.setup
31
+ @instrumentation&.start_trace(@epub_path)
32
+ begin
33
+ # Pass dependencies to MouseableReader
34
+ Controllers::MouseableReader.new(@epub_path, nil, @dependencies).run
35
+ ensure
36
+ # Balance setup to avoid lingering session depth
37
+ term.cleanup
38
+ @instrumentation&.cancel_trace
39
+ end
40
+ end
41
+
42
+ def menu_mode
43
+ # Pass dependencies to MenuController
44
+ Controllers::MenuController.new(@dependencies).run
45
+ end
46
+ end
47
+ end
48
+ end