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,469 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'mouseable_reader'
4
+ require_relative '../../adapters/input/dispatcher.rb'
5
+ require_relative '../../adapters/output/ui/components/main_menu_component.rb'
6
+ require_relative '../../adapters/output/ui/components/surface.rb'
7
+ require_relative '../../adapters/output/ui/components/rect.rb'
8
+ require_relative '../../adapters/output/ui/rendering/frame_coordinator.rb'
9
+ require_relative '../../adapters/output/ui/rendering/render_pipeline.rb'
10
+ require_relative '../main_menu/menu_progress_presenter'
11
+ require_relative 'menu/state_controller'
12
+ require_relative 'menu/input_controller'
13
+
14
+ module Shoko
15
+ module Application::Controllers
16
+ # Controller responsible for the menu orchestration loop.
17
+ class MenuController
18
+ include Adapters::Input::KeyDefinitions::Helpers
19
+
20
+ attr_accessor :filtered_epubs
21
+ attr_reader :state, :main_menu_component, :catalog, :dependencies,
22
+ :terminal_service, :frame_coordinator, :render_pipeline,
23
+ :state_controller, :input_controller
24
+
25
+ def initialize(dependencies = nil)
26
+ @dependencies = dependencies || Application::ContainerFactory.create_default_container
27
+ setup_state
28
+ setup_services
29
+ setup_components
30
+ @state_controller = Menu::StateController.new(self)
31
+ @input_controller = Menu::InputController.new(self)
32
+ @dispatcher = @input_controller.dispatcher
33
+ end
34
+
35
+ def run
36
+ @terminal_service.setup
37
+ @catalog.load_cached
38
+ epubs = @catalog.entries || []
39
+ @filtered_epubs = epubs
40
+ @main_menu_component.browse_screen.filtered_epubs = epubs
41
+ @catalog.start_scan if epubs.empty?
42
+
43
+ main_loop
44
+ rescue Interrupt
45
+ cleanup_and_exit(0, "\nGoodbye!")
46
+ rescue StandardError => e
47
+ cleanup_and_exit(1, "Error: #{e.message}", e)
48
+ ensure
49
+ begin
50
+ if @terminal_service.respond_to?(:force_cleanup)
51
+ @terminal_service.force_cleanup
52
+ elsif @terminal_service.respond_to?(:cleanup)
53
+ @terminal_service.cleanup
54
+ end
55
+ rescue StandardError
56
+ # best effort; leave terminal as-is if cleanup fails here
57
+ end
58
+ @catalog.cleanup if @catalog.respond_to?(:cleanup)
59
+ end
60
+
61
+ def handle_menu_selection
62
+ case selectors.selected(state)
63
+ when 0 then switch_to_browse
64
+ when 1 then switch_to_mode(:library)
65
+ when 2 then switch_to_mode(:annotations)
66
+ when 3 then open_download_screen
67
+ when 4 then switch_to_mode(:settings)
68
+ when 5 then cleanup_and_exit(0, '')
69
+ end
70
+ end
71
+
72
+ def handle_navigation(direction)
73
+ current = selectors.selected(state)
74
+ max_val = 5
75
+
76
+ new_selected = case direction
77
+ when :up then [current - 1, 0].max
78
+ when :down then [current + 1, max_val].min
79
+ else current
80
+ end
81
+ state.dispatch(menu_action(selected: new_selected))
82
+ end
83
+
84
+ def switch_to_browse
85
+ state.dispatch(menu_action(mode: :browse, search_active: false))
86
+ input_controller.activate(selectors.mode(state))
87
+ end
88
+
89
+ def switch_to_search
90
+ state.dispatch(menu_action(mode: :search, search_active: true))
91
+ input_controller.activate(selectors.mode(state))
92
+ end
93
+
94
+ def switch_to_mode(mode)
95
+ payload = { mode: mode, browse_selected: 0 }
96
+ payload[:settings_selected] = 1 if mode == :settings
97
+ state.dispatch(menu_action(payload))
98
+ preload_annotations if mode == :annotations
99
+ input_controller.activate(selectors.mode(state))
100
+ end
101
+
102
+ def open_download_screen
103
+ reset_download_state
104
+ state.dispatch(menu_action(mode: :download))
105
+ input_controller.activate(selectors.mode(state))
106
+ end
107
+
108
+ def download_start_search
109
+ query = (state.get(%i[menu download_query]) || '').to_s
110
+ state.dispatch(menu_action(mode: :download_search, download_cursor: query.length))
111
+ input_controller.activate(selectors.mode(state))
112
+ end
113
+
114
+ def download_exit_search
115
+ state.dispatch(menu_action(mode: :download))
116
+ input_controller.activate(selectors.mode(state))
117
+ end
118
+
119
+ def download_submit_search
120
+ query = (state.get(%i[menu download_query]) || '').to_s
121
+ state_controller.search_downloads(query: query)
122
+ download_exit_search
123
+ end
124
+
125
+ def download_refresh
126
+ query = (state.get(%i[menu download_query]) || '').to_s
127
+ state_controller.search_downloads(query: query)
128
+ end
129
+
130
+ def download_next_page
131
+ next_url = state.get(%i[menu download_next])
132
+ return unless next_url
133
+
134
+ query = (state.get(%i[menu download_query]) || '').to_s
135
+ state_controller.search_downloads(query: query, page_url: next_url)
136
+ end
137
+
138
+ def download_prev_page
139
+ prev_url = state.get(%i[menu download_prev])
140
+ return unless prev_url
141
+
142
+ query = (state.get(%i[menu download_query]) || '').to_s
143
+ state_controller.search_downloads(query: query, page_url: prev_url)
144
+ end
145
+
146
+ def download_up
147
+ update_download_selection(-1)
148
+ end
149
+
150
+ def download_down
151
+ update_download_selection(1)
152
+ end
153
+
154
+ def download_confirm
155
+ book = selected_download_book
156
+ return unless book
157
+
158
+ state_controller.download_book(book)
159
+ end
160
+
161
+ def cleanup_and_exit(code, message, error = nil)
162
+ cleanup_terminal
163
+
164
+ log_exit(message, error)
165
+ exit code
166
+ end
167
+
168
+ def refresh_scan(force: true)
169
+ state_controller.refresh_scan(force: force)
170
+ end
171
+
172
+ # Settings are handled directly via dispatcher bindings
173
+ def toggle_view_mode(_key = nil)
174
+ settings_service.toggle_view_mode
175
+ end
176
+
177
+ def toggle_page_numbers(_key = nil)
178
+ settings_service.toggle_page_numbers
179
+ end
180
+
181
+ def cycle_line_spacing(_key = nil)
182
+ settings_service.cycle_line_spacing
183
+ end
184
+
185
+ def toggle_highlight_quotes(_key = nil)
186
+ settings_service.toggle_highlight_quotes
187
+ end
188
+
189
+ def toggle_kitty_images(_key = nil)
190
+ settings_service.toggle_kitty_images
191
+ end
192
+
193
+ def toggle_page_numbering_mode(_key = nil)
194
+ settings_service.toggle_page_numbering_mode
195
+ end
196
+
197
+ def wipe_cache(_key = nil)
198
+ message = settings_service.wipe_cache(catalog: @catalog)
199
+ @filtered_epubs = []
200
+ @catalog.scan_message = message if @catalog.respond_to?(:scan_message)
201
+ message
202
+ end
203
+
204
+ private
205
+
206
+ def setup_state
207
+ @state = @dependencies.resolve(:global_state)
208
+ @filtered_epubs = []
209
+ end
210
+
211
+ def setup_services
212
+ # Use dependency injection for services
213
+ @catalog = @dependencies.resolve(:catalog_service)
214
+ @terminal_service = @dependencies.resolve(:terminal_service)
215
+ @frame_coordinator = Adapters::Output::Ui::Rendering::FrameCoordinator.new(@dependencies)
216
+ @render_pipeline = Adapters::Output::Ui::Rendering::RenderPipeline.new(@dependencies)
217
+ end
218
+
219
+ def setup_components
220
+ @main_menu_component = Shoko::Adapters::Output::Ui::Components::MainMenuComponent.new(self, @dependencies)
221
+ end
222
+
223
+ public
224
+
225
+ # Library mode helpers
226
+ def library_up
227
+ current = selectors.browse_selected(state) || 0
228
+ state.dispatch(menu_action(browse_selected: (current - 1).clamp(0, current)))
229
+ end
230
+
231
+ def library_down
232
+ items = if main_menu_component&.current_screen.respond_to?(:items)
233
+ main_menu_component.current_screen.items
234
+ else
235
+ []
236
+ end
237
+ max_index = [items.length - 1, 0].max
238
+ current = selectors.browse_selected(state) || 0
239
+ state.dispatch(menu_action(browse_selected: (current + 1).clamp(0, max_index)))
240
+ end
241
+
242
+ def library_select
243
+ item = selected_library_item
244
+ return unless item
245
+
246
+ target_path = resolve_library_path(item)
247
+ return state_controller.file_not_found unless target_path
248
+
249
+ state_controller.run_reader(target_path)
250
+ end
251
+
252
+ def open_selected_book
253
+ state_controller.open_selected_book
254
+ end
255
+
256
+ # Legacy compatibility methods
257
+ def browse_screen
258
+ @main_menu_component.browse_screen
259
+ end
260
+
261
+ # recent_screen removed
262
+
263
+ def settings_screen
264
+ @main_menu_component.settings_screen
265
+ end
266
+
267
+ def download_books_screen
268
+ @main_menu_component.download_books_screen
269
+ end
270
+
271
+ def annotations_screen
272
+ @main_menu_component.annotations_screen
273
+ end
274
+
275
+ def annotation_editor_screen
276
+ @main_menu_component.annotation_edit_screen
277
+ end
278
+
279
+ def menu_screen
280
+ @main_menu_component.current_screen
281
+ end
282
+
283
+ def selected_book
284
+ @main_menu_component.browse_screen.selected_book
285
+ end
286
+
287
+ def main_loop
288
+ draw_screen
289
+ loop do
290
+ process_scan_results_if_available
291
+ handle_user_input
292
+ draw_screen
293
+ end
294
+ end
295
+
296
+ def handle_user_input
297
+ keys = read_input_keys
298
+ input_controller.handle_keys(keys)
299
+ end
300
+
301
+ def read_input_keys
302
+ @terminal_service.read_keys_blocking(limit: 10)
303
+ end
304
+
305
+ def process_scan_results_if_available
306
+ return unless (epubs = @catalog.process_results)
307
+
308
+ @filtered_epubs = epubs
309
+ @main_menu_component.browse_screen.filtered_epubs = epubs
310
+ @main_menu_component.library_screen.invalidate_cache!
311
+ end
312
+
313
+ def draw_screen
314
+ notification_service&.tick(@state)
315
+ @frame_coordinator.with_frame do |surface, bounds, _w, _h|
316
+ @render_pipeline.render_component(surface, bounds, @main_menu_component)
317
+ end
318
+ end
319
+
320
+ # Annotation helpers (public so dispatcher can invoke explicitly)
321
+ def open_selected_annotation
322
+ state_controller.open_selected_annotation
323
+ end
324
+
325
+ def open_selected_annotation_for_edit
326
+ state_controller.open_selected_annotation_for_edit
327
+ end
328
+
329
+ def delete_selected_annotation
330
+ state_controller.delete_selected_annotation
331
+ end
332
+
333
+ def save_current_annotation_edit
334
+ state_controller.save_current_annotation_edit
335
+ end
336
+
337
+ private
338
+
339
+ # Provide current editor component for application commands in menu context
340
+ def current_editor_component
341
+ return nil unless Shoko::Application::Selectors::MenuSelectors.mode(@state) == :annotation_editor
342
+
343
+ @main_menu_component&.annotation_edit_screen
344
+ end
345
+
346
+ def notification_service
347
+ @notification_service ||= begin
348
+ @dependencies.resolve(:notification_service)
349
+ rescue StandardError
350
+ nil
351
+ end
352
+ end
353
+
354
+ def logger
355
+ @logger ||= begin
356
+ @dependencies.resolve(:logger)
357
+ rescue StandardError
358
+ nil
359
+ end
360
+ end
361
+
362
+ def settings_service
363
+ @settings_service ||= @dependencies.resolve(:settings_service)
364
+ end
365
+
366
+ def selectors
367
+ Shoko::Application::Selectors::MenuSelectors
368
+ end
369
+
370
+ def menu_action(payload)
371
+ Shoko::Application::Actions::UpdateMenuAction.new(payload)
372
+ end
373
+
374
+ def preload_annotations
375
+ service = dependencies.resolve(:annotation_service)
376
+ state.dispatch(menu_action(annotations_all: service.list_all))
377
+ rescue StandardError
378
+ state.dispatch(menu_action(annotations_all: {}))
379
+ end
380
+
381
+ def reset_download_state
382
+ state.dispatch(menu_action(
383
+ download_query: '',
384
+ download_cursor: 0,
385
+ download_selected: 0,
386
+ download_results: [],
387
+ download_count: 0,
388
+ download_next: nil,
389
+ download_prev: nil,
390
+ download_status: :idle,
391
+ download_message: '',
392
+ download_progress: 0.0
393
+ ))
394
+ end
395
+
396
+ def update_download_selection(delta)
397
+ results = Array(state.get(%i[menu download_results]))
398
+ max_index = [results.length - 1, 0].max
399
+ current = (state.get(%i[menu download_selected]) || 0).to_i
400
+ new_val = (current + delta).clamp(0, max_index)
401
+ state.dispatch(menu_action(download_selected: new_val))
402
+ end
403
+
404
+ def selected_download_book
405
+ results = Array(state.get(%i[menu download_results]))
406
+ index = (state.get(%i[menu download_selected]) || 0).to_i
407
+ results[index]
408
+ end
409
+
410
+ def selected_library_item
411
+ screen = main_menu_component&.current_screen
412
+ items = screen.respond_to?(:items) ? screen.items : []
413
+ index = selectors.browse_selected(state) || 0
414
+ items[index]
415
+ end
416
+
417
+ def resolve_library_path(item)
418
+ primary = item.respond_to?(:open_path) ? item.open_path : nil
419
+ return primary if state_controller.valid_cache_path?(primary)
420
+
421
+ fallback = item.respond_to?(:epub_path) ? item.epub_path : nil
422
+ return fallback if fallback && !fallback.empty? && File.exist?(fallback)
423
+
424
+ nil
425
+ end
426
+
427
+ def cleanup_terminal
428
+ terminal = terminal_service
429
+ return unless terminal
430
+
431
+ cleanup_error = nil
432
+ begin
433
+ terminal.cleanup
434
+ rescue StandardError => e
435
+ cleanup_error = e
436
+ resolve_logger&.error('Menu terminal cleanup failed', error: e.message)
437
+ ensure
438
+ force_cleanup_if_needed(terminal, cleanup_error)
439
+ end
440
+ end
441
+
442
+ def force_cleanup_if_needed(terminal, cleanup_error)
443
+ return unless terminal.respond_to?(:force_cleanup)
444
+
445
+ remaining_depth = Shoko::Adapters::Output::Terminal::TerminalService.session_depth || 0
446
+ needs_force = cleanup_error || remaining_depth.positive?
447
+ return unless needs_force
448
+
449
+ terminal.force_cleanup
450
+ rescue StandardError => e
451
+ resolve_logger&.error('Menu terminal force cleanup failed', error: e.message)
452
+ end
453
+
454
+ def log_exit(message, error)
455
+ logger = resolve_logger
456
+ logger&.info('Exiting menu', message: message, status: error ? 'error' : 'ok')
457
+ return unless error
458
+
459
+ logger&.error('Menu exit error', error: error.message, backtrace: Array(error.backtrace))
460
+ end
461
+
462
+ def resolve_logger
463
+ dependencies.resolve(:logger)
464
+ rescue StandardError
465
+ nil
466
+ end
467
+ end
468
+ end
469
+ end