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,410 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Application::Controllers
5
+ # Handles all state management: persistence, bookmarks, progress
6
+ class StateController
7
+ def initialize(state, doc, path, dependencies)
8
+ @state = state
9
+ @doc = doc
10
+ @path = path
11
+ @dependencies = dependencies
12
+ @terminal_service = @dependencies.resolve(:terminal_service)
13
+ # Prefer repositories via DI
14
+ has_resolve = @dependencies.respond_to?(:resolve)
15
+ @progress_repository = @dependencies.resolve(:progress_repository) if has_resolve
16
+ return unless has_resolve
17
+
18
+ @bookmark_repository = @dependencies.resolve(:bookmark_repository)
19
+ end
20
+
21
+ def save_progress
22
+ return unless @path && @doc
23
+
24
+ progress_data = collect_progress_data
25
+ canonical = canonical_path_for_doc
26
+
27
+ @progress_repository.save_for_book(canonical,
28
+ chapter_index: progress_data[:chapter],
29
+ line_offset: progress_data[:line_offset])
30
+ end
31
+
32
+ def load_progress
33
+ canonical = canonical_path_for_doc
34
+ progress = @progress_repository.find_by_book_path(canonical)
35
+ # Fallback: attempt original open path if canonical not found (for legacy records)
36
+ progress = @progress_repository.find_by_book_path(@path) if !progress && @path != canonical
37
+ return unless progress
38
+
39
+ apply_progress_data(progress)
40
+ end
41
+
42
+ def load_bookmarks
43
+ canonical = canonical_path_for_doc
44
+ bookmarks = @bookmark_repository.find_by_book_path(canonical)
45
+ @state.dispatch(Shoko::Application::Actions::UpdateBookmarksAction.new(bookmarks))
46
+ end
47
+
48
+ def add_bookmark
49
+ bookmark_service = resolve_bookmark_service
50
+ if bookmark_service
51
+ bookmark_service.add_bookmark
52
+ else
53
+ position = current_bookmark_position
54
+ canonical = canonical_path_for_doc
55
+ begin
56
+ @bookmark_repository.add_for_book(canonical,
57
+ chapter_index: position[:chapter],
58
+ line_offset: position[:line_offset],
59
+ text_snippet: '')
60
+ bookmarks = @bookmark_repository.find_by_book_path(canonical)
61
+ rescue StandardError
62
+ bookmarks = @state.get(%i[reader bookmarks]) || []
63
+ end
64
+ @state.dispatch(Shoko::Application::Actions::UpdateBookmarksAction.new(bookmarks))
65
+ end
66
+
67
+ curr_ch = @state.get(%i[reader current_chapter]) || 0
68
+ curr_page = current_page_label
69
+ set_message("Bookmark added at Chapter #{curr_ch + 1}, Page #{curr_page}")
70
+ end
71
+
72
+ def jump_to_bookmark
73
+ bookmarks = bookmarks_list
74
+ selected_idx = @state.get(%i[reader sidebar_bookmarks_selected]) || 0
75
+ bookmark = bookmarks[selected_idx]
76
+ return unless bookmark
77
+
78
+ has_resolve = @dependencies.respond_to?(:resolve)
79
+ navigation = if has_resolve && @dependencies.registered?(:navigation_service)
80
+ @dependencies.resolve(:navigation_service)
81
+ end
82
+
83
+ chapter_index = bookmark.chapter_index
84
+ if navigation
85
+ navigation.jump_to_chapter(chapter_index)
86
+ else
87
+ @state.dispatch(Shoko::Application::Actions::UpdateChapterAction.new(chapter_index))
88
+ end
89
+
90
+ offset = bookmark.line_offset.to_i
91
+ stride = split_stride_for_state
92
+ payload = {
93
+ single_page: offset,
94
+ left_page: offset,
95
+ right_page: offset + stride,
96
+ current_page: offset,
97
+ }
98
+
99
+ if Shoko::Application::Selectors::ConfigSelectors.page_numbering_mode(@state) == :dynamic &&
100
+ has_resolve && @dependencies.registered?(:page_calculator)
101
+ page_index = @dependencies.resolve(:page_calculator)&.find_page_index(chapter_index,
102
+ offset)
103
+ payload[:current_page_index] = page_index if page_index
104
+ end
105
+
106
+ @state.dispatch(Shoko::Application::Actions::UpdatePageAction.new(payload))
107
+ save_progress
108
+ @state.dispatch(Shoko::Application::Actions::UpdateReaderModeAction.new(:read))
109
+ end
110
+
111
+ def delete_selected_bookmark
112
+ bookmarks = bookmarks_list
113
+ selected_idx = @state.get(%i[reader sidebar_bookmarks_selected]) || 0
114
+ bookmark = bookmarks[selected_idx]
115
+ return unless bookmark
116
+
117
+ canonical = canonical_path_for_doc
118
+ @bookmark_repository.delete_for_book(canonical, bookmark)
119
+ load_bookmarks
120
+ current_bookmarks = bookmarks_list
121
+ max_selected = if current_bookmarks.any?
122
+ [selected_idx, current_bookmarks.length - 1].min
123
+ else
124
+ 0
125
+ end
126
+ @state.dispatch(
127
+ Shoko::Application::Actions::UpdateSidebarAction.new(bookmarks_selected: max_selected)
128
+ )
129
+ set_message(Adapters::Output::Ui::Constants::Messages::BOOKMARK_DELETED)
130
+ end
131
+
132
+ def refresh_annotations
133
+ annotations = []
134
+ begin
135
+ svc = @dependencies.resolve(:annotation_service) if @dependencies.respond_to?(:resolve)
136
+ annotations = svc ? svc.list_for_book(@path) : []
137
+ rescue StandardError => e
138
+ # Log the error and keep annotations empty
139
+ begin
140
+ @dependencies.resolve(:logger).error('Failed to refresh annotations', error: e.message,
141
+ path: @path)
142
+ rescue StandardError
143
+ # no-op
144
+ end
145
+ ensure
146
+ @state.dispatch(Application::Actions::UpdateAnnotationsAction.new(annotations))
147
+ end
148
+ end
149
+
150
+ def split_stride_for_state
151
+ return 1 unless @dependencies.respond_to?(:registered?) && @dependencies.registered?(:layout_service)
152
+
153
+ layout_service = @dependencies.resolve(:layout_service)
154
+ width = @state.get(%i[ui terminal_width])
155
+ height = @state.get(%i[ui terminal_height])
156
+ if (!width || !height) && @dependencies.registered?(:terminal_service)
157
+ height, width = @dependencies.resolve(:terminal_service).size
158
+ end
159
+ width = width.to_i
160
+ height = height.to_i
161
+ width = 80 if width <= 0
162
+ height = 24 if height <= 0
163
+
164
+ _, content_height = layout_service.calculate_metrics(width, height, :split)
165
+ spacing = @state.get(%i[config line_spacing]) || Shoko::Core::Models::ReaderSettings::DEFAULT_LINE_SPACING
166
+ stride = layout_service.adjust_for_line_spacing(content_height, spacing)
167
+ stride = 1 if stride.to_i <= 0
168
+ stride
169
+ rescue StandardError
170
+ 1
171
+ end
172
+
173
+ def resolve_bookmark_service
174
+ return nil unless @dependencies.respond_to?(:registered?) && @dependencies.registered?(:bookmark_service)
175
+
176
+ @dependencies.resolve(:bookmark_service)
177
+ rescue StandardError
178
+ nil
179
+ end
180
+
181
+ def current_bookmark_position
182
+ chapter = @state.get(%i[reader current_chapter]) || 0
183
+ if dynamic_page_numbering? &&
184
+ @dependencies.respond_to?(:registered?) &&
185
+ @dependencies.registered?(:page_calculator)
186
+ page_index = @state.get(%i[reader current_page_index]) || 0
187
+ page = @dependencies.resolve(:page_calculator)&.get_page(page_index)
188
+ if page
189
+ chapter = page[:chapter_index] || chapter
190
+ line = page[:start_line] || page['start_line']
191
+ return { chapter: chapter, line_offset: line.to_i }
192
+ end
193
+ end
194
+
195
+ view_mode = Application::Selectors::ConfigSelectors.view_mode(@state)
196
+ line_offset = view_mode == :split ? @state.get(%i[reader left_page]) : @state.get(%i[reader single_page])
197
+ { chapter: chapter, line_offset: line_offset || 0 }
198
+ rescue StandardError
199
+ { chapter: chapter, line_offset: 0 }
200
+ end
201
+
202
+ def current_page_label
203
+ if dynamic_page_numbering?
204
+ ((@state.get(%i[reader current_page_index]) || 0).to_i + 1)
205
+ else
206
+ @state.get(%i[reader current_page]) || 0
207
+ end
208
+ rescue StandardError
209
+ 0
210
+ end
211
+
212
+ def dynamic_page_numbering?
213
+ Application::Selectors::ConfigSelectors.page_numbering_mode(@state) == :dynamic
214
+ rescue StandardError
215
+ false
216
+ end
217
+
218
+ def jump_to_annotation(annotation)
219
+ normalized = normalize_annotation(annotation)
220
+ return unless normalized
221
+
222
+ chapter_index = normalized[:chapter_index]
223
+ range = normalized[:range]
224
+ navigation = if @dependencies.respond_to?(:resolve) && @dependencies.registered?(:navigation_service)
225
+ @dependencies.resolve(:navigation_service)
226
+ end
227
+ navigation&.jump_to_chapter(chapter_index) if chapter_index
228
+
229
+ if range
230
+ selection = normalize_selection_for_state(range)
231
+ @state.dispatch(Shoko::Application::Actions::UpdateSelectionAction.new(selection)) if selection
232
+ end
233
+
234
+ @state.dispatch(Shoko::Application::Actions::UpdateReaderModeAction.new(:read))
235
+ end
236
+
237
+ def delete_annotation_by_id(annotation)
238
+ current_index = @state.get(%i[reader sidebar_annotations_selected]) || 0
239
+ normalized = normalize_annotation(annotation)
240
+ annotation_id = normalized[:id]
241
+
242
+ svc = if @dependencies.respond_to?(:resolve) && @dependencies.registered?(:annotation_service)
243
+ @dependencies.resolve(:annotation_service)
244
+ end
245
+ return current_index unless svc && annotation_id
246
+
247
+ svc.delete(@path, annotation_id)
248
+ annotations = svc.list_for_book(@path)
249
+ @state.dispatch(Shoko::Application::Actions::UpdateAnnotationsAction.new(annotations))
250
+
251
+ new_index = [current_index, annotations.length - 1].min
252
+ new_index = 0 if new_index.negative?
253
+ @state.dispatch(Shoko::Application::Actions::UpdateSidebarAction.new(
254
+ annotations_selected: new_index,
255
+ sidebar_annotations_selected: new_index
256
+ ))
257
+ new_index
258
+ rescue StandardError
259
+ current_index
260
+ end
261
+
262
+ def quit_to_menu
263
+ save_progress
264
+ @state.dispatch(Shoko::Application::Actions::QuitToMenuAction.new)
265
+ end
266
+
267
+ def quit_application
268
+ save_progress
269
+ @terminal_service.cleanup
270
+ exit 0
271
+ end
272
+
273
+ private
274
+
275
+ private :split_stride_for_state
276
+
277
+ def canonical_path_for_doc
278
+ @doc.respond_to?(:canonical_path) ? @doc.canonical_path : @path
279
+ end
280
+
281
+ def normalize_selection_for_state(range)
282
+ return nil unless range
283
+
284
+ return range if anchor_range?(range)
285
+
286
+ coord = resolve_coordinate_service
287
+ return nil unless coord
288
+
289
+ rendered = Shoko::Application::Selectors::ReaderSelectors.rendered_lines(@state)
290
+ coord.normalize_selection_range(range, rendered)
291
+ rescue StandardError
292
+ nil
293
+ end
294
+
295
+ def anchor_range?(range)
296
+ return false unless range.is_a?(Hash)
297
+
298
+ start_anchor = range[:start] || range['start']
299
+ start_anchor.is_a?(Hash) && (start_anchor.key?(:geometry_key) || start_anchor.key?('geometry_key'))
300
+ end
301
+
302
+ def resolve_coordinate_service
303
+ return nil unless @dependencies.respond_to?(:resolve)
304
+
305
+ @dependencies.resolve(:coordinate_service)
306
+ rescue StandardError
307
+ nil
308
+ end
309
+
310
+ def bookmarks_list
311
+ @state.get(%i[reader bookmarks])
312
+ end
313
+
314
+ def collect_progress_data
315
+ page_calculator = @dependencies.resolve(:page_calculator)
316
+ if Application::Selectors::ConfigSelectors.page_numbering_mode(@state) == :dynamic && page_calculator
317
+ collect_dynamic_progress(page_calculator)
318
+ else
319
+ collect_absolute_progress
320
+ end
321
+ end
322
+
323
+ def normalize_annotation(annotation)
324
+ return {} unless annotation.is_a?(Hash)
325
+
326
+ annotation.transform_keys do |key|
327
+ key.is_a?(String) ? key.to_sym : key
328
+ end
329
+ end
330
+
331
+ def collect_dynamic_progress(page_calculator)
332
+ page_data = page_calculator.get_page(@state.get(%i[reader current_page_index]))
333
+ return { chapter: 0, line_offset: 0 } unless page_data
334
+
335
+ {
336
+ chapter: page_data[:chapter_index],
337
+ line_offset: page_data[:start_line],
338
+ }
339
+ end
340
+
341
+ def collect_absolute_progress
342
+ line_offset = if Application::Selectors::ConfigSelectors.view_mode(@state) == :split
343
+ @state.get(%i[reader left_page])
344
+ else
345
+ @state.get(%i[reader single_page])
346
+ end
347
+
348
+ {
349
+ chapter: @state.get(%i[reader current_chapter]),
350
+ line_offset: line_offset,
351
+ }
352
+ end
353
+
354
+ def apply_progress_data(progress)
355
+ # Set chapter (with validation)
356
+ chapter = if progress.respond_to?(:chapter_index)
357
+ progress.chapter_index
358
+ else
359
+ progress['chapter'] || progress[:chapter] || 0
360
+ end
361
+ @state.dispatch(Shoko::Application::Actions::UpdateChapterAction.new(chapter >= @doc.chapter_count ? 0 : chapter))
362
+
363
+ # Set page offset
364
+ line_offset = if progress.respond_to?(:line_offset)
365
+ progress.line_offset
366
+ else
367
+ progress['line_offset'] || progress[:line_offset] || 0
368
+ end
369
+ page_calculator = @dependencies.resolve(:page_calculator)
370
+
371
+ if Application::Selectors::ConfigSelectors.page_numbering_mode(@state) == :dynamic && page_calculator
372
+ # Dynamic page mode (lazy): estimate index now; compute precisely after background build
373
+ begin
374
+ # Use state-known terminal dimensions to avoid relying on TerminalService setup timing
375
+ width = (@state.get(%i[ui terminal_width]) || 80).to_i
376
+ height = (@state.get(%i[ui terminal_height]) || 24).to_i
377
+ layout = @dependencies.resolve(:layout_service)
378
+ _, content_height = layout.calculate_metrics(width, height,
379
+ Application::Selectors::ConfigSelectors.view_mode(@state))
380
+ lines_per_page = layout.adjust_for_line_spacing(content_height,
381
+ Application::Selectors::ConfigSelectors.line_spacing(@state))
382
+ est_index = lines_per_page.positive? ? (line_offset.to_f / lines_per_page).floor : 0
383
+ @state.dispatch(Shoko::Application::Actions::UpdatePageAction.new(current_page_index: est_index))
384
+ rescue StandardError
385
+ # best-effort; leave index as-is if estimation fails
386
+ end
387
+ # Store pending precise restore to be applied after background map build
388
+ @state.dispatch(Shoko::Application::Actions::UpdateSelectionsAction.new(
389
+ pending_progress: {
390
+ chapter_index: @state.get(%i[reader current_chapter]),
391
+ line_offset: line_offset,
392
+ }
393
+ ))
394
+ else
395
+ # Absolute page mode
396
+ page_offsets = line_offset
397
+ @state.dispatch(Shoko::Application::Actions::UpdatePageAction.new(single_page: page_offsets,
398
+ left_page: page_offsets))
399
+ end
400
+ end
401
+
402
+ def set_message(text, duration = 2)
403
+ notifier = @dependencies.resolve(:notification_service)
404
+ notifier.set_message(@state, text, duration)
405
+ rescue StandardError
406
+ @state.dispatch(Shoko::Application::Actions::UpdateMessageAction.new(text))
407
+ end
408
+ end
409
+ end
410
+ end