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,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../application/use_cases/commands/application_commands.rb'
4
+ require_relative '../../application/use_cases/commands/navigation_commands.rb'
5
+ require_relative '../../application/use_cases/commands/sidebar_commands.rb'
6
+ require_relative '../../application/use_cases/commands/conditional_navigation_commands.rb'
7
+ require_relative '../../application/use_cases/commands/menu_commands.rb'
8
+ require_relative '../../application/use_cases/commands/bookmark_commands.rb'
9
+ require_relative '../../application/use_cases/commands/reader_commands.rb'
10
+
11
+ module Shoko
12
+ module Adapters::Input
13
+ # Bridge to create Application commands for Input system usage.
14
+ # Provides a clean interface for the Input system to use Application commands
15
+ # while maintaining backward compatibility during the migration.
16
+ class CommandBridge
17
+ class << self
18
+ # Create navigation commands for reader movement
19
+ #
20
+ # @param action [Symbol] Navigation action (:next_page, :prev_page, etc.)
21
+ # @return [Application::Commands::NavigationCommand]
22
+ def navigation_command(action)
23
+ Application::Commands::NavigationCommand.new(action)
24
+ end
25
+
26
+ # Create application lifecycle commands
27
+ #
28
+ # @param action [Symbol] Application action (:quit, :switch_mode, etc.)
29
+ # @return [Application::Commands::ApplicationCommand]
30
+ def application_command(action)
31
+ Application::Commands::ApplicationCommand.new(action)
32
+ end
33
+
34
+ # Create bookmark operation commands
35
+ #
36
+ # @param action [Symbol] Bookmark action (:add, :remove, :navigate, etc.)
37
+ # @return [Application::Commands::BookmarkCommand]
38
+ def bookmark_command(action)
39
+ Application::Commands::BookmarkCommand.new(action)
40
+ end
41
+
42
+ # Create sidebar navigation commands
43
+ #
44
+ # @param action [Symbol] Sidebar action (:up, :down, :select)
45
+ # @return [Application::Commands::SidebarCommand]
46
+ def sidebar_command(action)
47
+ Application::Commands::SidebarCommand.new(action)
48
+ end
49
+
50
+ # Create conditional navigation commands
51
+ #
52
+ # @param type [Symbol] Type of conditional navigation
53
+ # @return [Application::Commands::ConditionalNavigationCommand]
54
+ def conditional_navigation_command(type)
55
+ case type
56
+ when :up_or_sidebar then Application::Commands::ConditionalNavigationCommand.up_or_sidebar
57
+ when :down_or_sidebar then Application::Commands::ConditionalNavigationCommand.down_or_sidebar
58
+ when :select_or_sidebar then Application::Commands::ConditionalNavigationCommand.select_or_sidebar
59
+ else
60
+ raise ArgumentError, "Unknown conditional navigation type: #{type}"
61
+ end
62
+ end
63
+
64
+ # Convert Input system symbols to appropriate Application commands
65
+ # This method helps during migration by automatically routing
66
+ # common input symbols to their Application command equivalents.
67
+ #
68
+ # @param symbol [Symbol] Input symbol
69
+ # @param context [Object] Execution context
70
+ # @return [Application::Commands::BaseCommand, nil] Command or nil if no mapping
71
+ def symbol_to_command(symbol, _context = nil)
72
+ case symbol
73
+ # Navigation commands - now use Application commands
74
+ when :next_page then navigation_command(:next_page)
75
+ when :prev_page then navigation_command(:prev_page)
76
+ when :next_chapter then navigation_command(:next_chapter)
77
+ when :prev_chapter then navigation_command(:prev_chapter)
78
+ when :scroll_up then Application::Commands::NavigationCommandFactory.scroll_up
79
+ when :scroll_down then Application::Commands::NavigationCommandFactory.scroll_down
80
+ when :go_to_start then navigation_command(:go_to_start)
81
+ when :go_to_end then navigation_command(:go_to_end)
82
+ # Application commands
83
+ when :show_help then application_command(:show_help)
84
+ when :open_toc then application_command(:show_toc)
85
+ when :open_bookmarks then application_command(:show_bookmarks)
86
+ when :open_annotations then application_command(:show_annotations)
87
+ when :quit_to_menu then application_command(:quit_to_menu)
88
+ when :add_bookmark then Application::Commands::BookmarkCommandFactory.add_bookmark
89
+ # Conditional navigation commands
90
+ when :conditional_up then conditional_navigation_command(:up_or_sidebar)
91
+ when :conditional_down then conditional_navigation_command(:down_or_sidebar)
92
+ when :conditional_select then conditional_navigation_command(:select_or_sidebar)
93
+ # Direct sidebar commands
94
+ when :sidebar_up then sidebar_command(:up)
95
+ when :sidebar_down then sidebar_command(:down)
96
+ when :sidebar_select then sidebar_command(:select)
97
+ # Menu commands
98
+ when :menu_up then Application::Commands::MenuCommand.new(:menu_up)
99
+ when :menu_down then Application::Commands::MenuCommand.new(:menu_down)
100
+ when :menu_select then Application::Commands::MenuCommand.new(:menu_select)
101
+ when :menu_quit then Application::Commands::MenuCommand.new(:menu_quit)
102
+ when :back_to_menu then Application::Commands::MenuCommand.new(:back_to_menu)
103
+ when :browse_up then Application::Commands::MenuCommand.new(:browse_up)
104
+ when :browse_down then Application::Commands::MenuCommand.new(:browse_down)
105
+ when :browse_select then Application::Commands::MenuCommand.new(:browse_select)
106
+ when :library_up then Application::Commands::MenuCommand.new(:library_up)
107
+ when :library_down then Application::Commands::MenuCommand.new(:library_down)
108
+ when :library_select then Application::Commands::MenuCommand.new(:library_select)
109
+ when :settings_up then Application::Commands::MenuCommand.new(:settings_up)
110
+ when :settings_down then Application::Commands::MenuCommand.new(:settings_down)
111
+ when :settings_select then Application::Commands::MenuCommand.new(:settings_select)
112
+ # recent_* commands removed
113
+ when :start_search then Application::Commands::MenuCommand.new(:start_search)
114
+ when :exit_search then Application::Commands::MenuCommand.new(:exit_search)
115
+ # Annotations-related menu commands
116
+ when :annotations_up then Application::Commands::MenuCommand.new(:annotations_up)
117
+ when :annotations_down then Application::Commands::MenuCommand.new(:annotations_down)
118
+ when :annotations_select then Application::Commands::MenuCommand.new(:annotations_select)
119
+ when :annotations_edit then Application::Commands::MenuCommand.new(:annotations_edit)
120
+ when :annotations_delete then Application::Commands::MenuCommand.new(:annotations_delete)
121
+ when :annotation_detail_open then Application::Commands::MenuCommand.new(:annotation_detail_open)
122
+ when :annotation_detail_edit then Application::Commands::MenuCommand.new(:annotation_detail_edit)
123
+ when :annotation_detail_delete then Application::Commands::MenuCommand.new(:annotation_detail_delete)
124
+ when :annotation_detail_back then Application::Commands::MenuCommand.new(:annotation_detail_back)
125
+ # Settings actions
126
+ when :toggle_view_mode then Application::Commands::MenuCommand.new(:toggle_view_mode)
127
+ when :cycle_line_spacing then Application::Commands::MenuCommand.new(:cycle_line_spacing)
128
+ when :toggle_page_numbers then Application::Commands::MenuCommand.new(:toggle_page_numbers)
129
+ when :toggle_page_numbering_mode then Application::Commands::MenuCommand.new(:toggle_page_numbering_mode)
130
+ when :toggle_highlight_quotes then Application::Commands::MenuCommand.new(:toggle_highlight_quotes)
131
+ when :toggle_kitty_images then Application::Commands::MenuCommand.new(:toggle_kitty_images)
132
+ when :wipe_cache then Application::Commands::MenuCommand.new(:wipe_cache)
133
+ # Reader mode transitions
134
+ when :exit_help then Application::Commands::ReaderModeCommand.new(:exit_help)
135
+ end
136
+ end
137
+
138
+ # Check if a symbol can be converted to an Application command
139
+ #
140
+ # @param symbol [Symbol] Input symbol to check
141
+ # @return [Boolean] True if symbol has Application command equivalent
142
+ def command?(symbol)
143
+ !symbol_to_command(symbol).nil?
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,255 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'key_definitions'
4
+ require_relative '../output/terminal/terminal_sanitizer.rb'
5
+
6
+ module Shoko
7
+ module Adapters::Input
8
+ # Factory for creating common input command patterns.
9
+ module CommandFactory
10
+ module_function
11
+
12
+ def navigation_commands(_context, selection_field, max_value_proc)
13
+ selection_field = selection_field.to_sym
14
+ action_type = case selection_field
15
+ when :selected, :browse_selected
16
+ :menu
17
+ when :sidebar_toc_selected, :sidebar_bookmarks_selected, :sidebar_annotations_selected
18
+ :sidebar
19
+ end
20
+ return {} unless action_type
21
+
22
+ commands = {}
23
+ register_navigation(commands, :up, -1, selection_field, action_type, max_value_proc)
24
+ register_navigation(commands, :down, +1, selection_field, action_type, max_value_proc)
25
+ commands
26
+ end
27
+
28
+ def exit_commands(exit_action)
29
+ commands = {}
30
+ KeyDefinitions::ACTIONS[:cancel].each { |key| commands[key] = exit_action }
31
+ commands
32
+ end
33
+
34
+ def menu_selection_commands
35
+ commands = {}
36
+ KeyDefinitions::ACTIONS[:confirm].each do |key|
37
+ commands[key] = lambda do |ctx, _|
38
+ ctx.handle_menu_selection
39
+ :handled
40
+ end
41
+ end
42
+ commands
43
+ end
44
+
45
+ def reader_navigation_commands
46
+ reader = KeyDefinitions::READER
47
+ commands = {}
48
+ map_keys!(commands, reader[:next_page], :next_page)
49
+ map_keys!(commands, reader[:prev_page], :prev_page)
50
+ map_keys!(commands, reader[:scroll_down], :scroll_down)
51
+ map_keys!(commands, reader[:scroll_up], :scroll_up)
52
+ map_keys!(commands, reader[:next_chapter], :next_chapter)
53
+ map_keys!(commands, reader[:prev_chapter], :prev_chapter)
54
+ map_keys!(commands, reader[:go_to_start], :go_to_start)
55
+ map_keys!(commands, reader[:go_to_end], :go_to_end)
56
+ commands
57
+ end
58
+
59
+ def reader_control_commands
60
+ reader = KeyDefinitions::READER
61
+ actions = KeyDefinitions::ACTIONS
62
+ commands = {}
63
+
64
+ map_keys!(commands, reader[:toggle_view], :toggle_view_mode)
65
+ map_keys!(commands, reader[:toggle_page_mode], :toggle_page_numbering_mode)
66
+ map_keys!(commands, reader[:increase_spacing], :increase_line_spacing)
67
+ map_keys!(commands, reader[:decrease_spacing], :decrease_line_spacing)
68
+ map_keys!(commands, reader[:show_toc], :open_toc)
69
+ map_keys!(commands, reader[:add_bookmark], :add_bookmark)
70
+ map_keys!(commands, reader[:show_bookmarks], :open_bookmarks)
71
+ map_keys!(commands, reader[:show_help], :show_help)
72
+
73
+ map_keys!(commands, reader[:show_annotations], :open_annotations) if reader.key?(:show_annotations)
74
+ if reader.key?(:show_annotations_tab)
75
+ map_keys!(commands, reader[:show_annotations_tab], :open_annotations_tab)
76
+ end
77
+ map_keys!(commands, reader[:rebuild_pagination], :rebuild_pagination) if reader.key?(:rebuild_pagination)
78
+ if reader.key?(:invalidate_pagination)
79
+ map_keys!(commands, reader[:invalidate_pagination], :invalidate_pagination_cache)
80
+ end
81
+
82
+ map_keys!(commands, actions[:quit], :quit_to_menu)
83
+ map_keys!(commands, actions[:force_quit], :quit_application)
84
+ commands
85
+ end
86
+
87
+ def text_input_commands(input_field, context_method = nil, cursor_field: nil)
88
+ input_field = input_field.to_sym
89
+ input_path = input_path_for(input_field)
90
+
91
+ commands = {}
92
+ KeyDefinitions::ACTIONS[:backspace].each do |key|
93
+ commands[key] = lambda do |ctx, _|
94
+ handle_backspace(ctx, key, input_field, input_path, context_method, cursor_field)
95
+ end
96
+ end
97
+ KeyDefinitions::ACTIONS[:delete].each do |key|
98
+ commands[key] = lambda do |ctx, _|
99
+ current, cursor = current_and_cursor(ctx, input_path, cursor_field)
100
+ new_value = splice_delete(current, cursor)
101
+ apply_value(ctx, input_field, new_value, cursor_field, cursor)
102
+ :handled
103
+ end
104
+ end
105
+
106
+ commands[:__default__] = lambda do |ctx, key|
107
+ char = key.to_s
108
+ if Shoko::Adapters::Output::Terminal::TerminalSanitizer.printable_char?(char)
109
+ handle_character(ctx, key, input_field, input_path, context_method, cursor_field)
110
+ else
111
+ :pass
112
+ end
113
+ end
114
+
115
+ commands
116
+ end
117
+
118
+ def handle_backspace(ctx, key, input_field, input_path, context_method, cursor_field)
119
+ if context_method
120
+ ctx.send(context_method, key)
121
+ elsif input_path
122
+ current, cursor_pos = current_and_cursor(ctx, input_path, cursor_field)
123
+ new_value, new_cursor = splice_backspace(current, cursor_pos)
124
+ apply_value(ctx, input_field, new_value, cursor_field, new_cursor)
125
+ end
126
+ :handled
127
+ end
128
+ private_class_method :handle_backspace
129
+
130
+ def handle_character(ctx, key, input_field, input_path, context_method, cursor_field)
131
+ if context_method
132
+ ctx.send(context_method, key)
133
+ elsif input_path
134
+ current, cursor_pos = current_and_cursor(ctx, input_path, cursor_field)
135
+ new_value, new_cursor = splice_insert(current, cursor_pos, key.to_s)
136
+ apply_value(ctx, input_field, new_value, cursor_field, new_cursor)
137
+ end
138
+ :handled
139
+ end
140
+ private_class_method :handle_character
141
+
142
+ def input_path_for(input_field)
143
+ {
144
+ search_query: %i[menu search_query],
145
+ download_query: %i[menu download_query],
146
+ }[input_field]
147
+ end
148
+ private_class_method :input_path_for
149
+
150
+ def current_and_cursor(ctx, input_path, cursor_field)
151
+ return ['', 0] unless input_path
152
+
153
+ current = current_value(ctx, input_path)
154
+ cursor_pos = determine_cursor(ctx, cursor_field, current)
155
+ [current, cursor_pos]
156
+ end
157
+ private_class_method :current_and_cursor
158
+
159
+ def determine_cursor(ctx, cursor_field, current)
160
+ if cursor_field
161
+ (ctx.state.get([:menu, cursor_field]) || current.length).to_i
162
+ else
163
+ current.length
164
+ end
165
+ end
166
+ private_class_method :determine_cursor
167
+
168
+ def apply_value(ctx, input_field, new_value, cursor_field, new_cursor)
169
+ if cursor_field && !new_cursor.nil?
170
+ dispatch_menu(ctx, input_field => new_value, cursor_field => new_cursor)
171
+ else
172
+ dispatch_menu(ctx, input_field => new_value)
173
+ end
174
+ end
175
+ private_class_method :apply_value
176
+
177
+ def register_navigation(commands, direction, step, selection_field, action_type, max_value_proc)
178
+ handler = navigation_handler(step, selection_field, action_type, max_value_proc)
179
+ Array(KeyDefinitions::NAVIGATION[direction]).each { |key| commands[key] = handler }
180
+ end
181
+ private_class_method :register_navigation
182
+
183
+ def navigation_handler(step, selection_field, action_type, max_value_proc)
184
+ lambda do |ctx, _|
185
+ current = value_at(ctx, action_type == :menu ? :menu : :reader, selection_field)
186
+ target = if step.negative?
187
+ [current + step, 0].max
188
+ else
189
+ max_val = max_value_proc.call(ctx)
190
+ (current + step).clamp(0, max_val)
191
+ end
192
+ dispatch_for(ctx, action_type, selection_field, target)
193
+ :handled
194
+ end
195
+ end
196
+ private_class_method :navigation_handler
197
+
198
+ def map_keys!(commands, keys, action)
199
+ Array(keys).each { |key| commands[key] = action }
200
+ commands
201
+ end
202
+ private_class_method :map_keys!
203
+
204
+ def dispatch_for(ctx, action_type, field, value)
205
+ case action_type
206
+ when :menu
207
+ dispatch_menu(ctx, field => value)
208
+ when :sidebar
209
+ ctx.state.dispatch(Shoko::Application::Actions::UpdateSidebarAction.new(field => value))
210
+ end
211
+ end
212
+ private_class_method :dispatch_for
213
+
214
+ def dispatch_menu(ctx, hash)
215
+ ctx.state.dispatch(Shoko::Application::Actions::UpdateMenuAction.new(hash))
216
+ end
217
+ private_class_method :dispatch_menu
218
+
219
+ def value_at(ctx, base, field)
220
+ ctx.state.get([base, field]) || 0
221
+ end
222
+ private_class_method :value_at
223
+
224
+ def current_value(ctx, input_path)
225
+ (ctx.state.get(input_path) || '').to_s
226
+ end
227
+ private_class_method :current_value
228
+
229
+ def splice_backspace(current, cursor)
230
+ return [current, cursor] unless cursor.positive?
231
+
232
+ before = current[0, cursor - 1] || ''
233
+ after = current[cursor..] || ''
234
+ [before + after, cursor - 1]
235
+ end
236
+ private_class_method :splice_backspace
237
+
238
+ def splice_insert(current, cursor, char)
239
+ before = current[0, cursor] || ''
240
+ after = current[cursor..] || ''
241
+ [before + char + after, cursor + 1]
242
+ end
243
+ private_class_method :splice_insert
244
+
245
+ def splice_delete(current, cursor)
246
+ return current unless cursor < current.length
247
+
248
+ before = current[0, cursor] || ''
249
+ after = current[(cursor + 1)..] || ''
250
+ before + after
251
+ end
252
+ private_class_method :splice_delete
253
+ end
254
+ end
255
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'command_bridge'
4
+
5
+ module Shoko
6
+ module Adapters::Input
7
+ # Generic command execution helpers used by the input system.
8
+ module Commands
9
+ module_function
10
+
11
+ # Execute a command against the given context.
12
+ # Supports multiple command types:
13
+ # - Symbol: calls context.public_send(symbol, key) if arity allows, else without args
14
+ # - Proc/Lambda: calls with (context, key) if arity 2, else with (key)
15
+ # - Array [Symbol, *args]: calls method with args splat
16
+ # - BaseCommand instance: calls command.execute(context, key)
17
+ def execute(command, context, key = nil)
18
+ case command
19
+ when Shoko::Application::Commands::BaseCommand
20
+ # Support application commands with parameter conversion
21
+ params = { key: key, triggered_by: :input }
22
+ command.execute(context, params)
23
+ when Symbol
24
+ # Route all symbols through the command bridge
25
+ if CommandBridge.command?(command)
26
+ mapped_command = CommandBridge.symbol_to_command(command, context)
27
+ return :pass unless mapped_command
28
+
29
+ execute(mapped_command, context, key)
30
+ else
31
+ execute_symbol(command, context, key)
32
+ end
33
+ when Proc
34
+ ar = command.arity
35
+ ar_abs = ar.abs
36
+ return command.call(context, key) if ar_abs >= 2
37
+ return command.call(key) if ar_abs >= 1
38
+
39
+ command.call
40
+ when Array
41
+ sym, *args = command
42
+ return :pass unless sym.is_a?(Symbol) && context.respond_to?(sym)
43
+
44
+ context.public_send(sym, *args)
45
+ else
46
+ :pass
47
+ end
48
+ end
49
+
50
+ def execute_symbol(command, context, key)
51
+ return :pass unless context.respond_to?(command)
52
+
53
+ method = context.method(command)
54
+ return context.public_send(command) if method.arity.zero?
55
+
56
+ context.public_send(command, key)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'commands'
4
+
5
+ module Shoko
6
+ module Adapters::Input
7
+ # Dispatches keys through a stack of active input modes.
8
+ class Dispatcher
9
+ def initialize(context)
10
+ @context = context
11
+ @command_map = {}
12
+ @mode_stack = []
13
+ end
14
+
15
+ def handle_key(key)
16
+ return if key.nil?
17
+
18
+ @mode_stack.reverse_each do |mode|
19
+ bindings = @command_map[mode] || {}
20
+ cmd = bindings[key] || bindings[:__default__]
21
+ next unless cmd
22
+
23
+ result = Commands.execute(cmd, @context, key)
24
+ return result if result == :handled
25
+ end
26
+ :pass
27
+ end
28
+
29
+ def push_mode(mode, bindings = {})
30
+ @mode_stack << mode
31
+ @command_map[mode] = bindings if bindings && !bindings.empty?
32
+ end
33
+
34
+ def pop_mode
35
+ @mode_stack.pop
36
+ end
37
+
38
+ def remove_mode(mode)
39
+ @mode_stack.delete(mode)
40
+ @command_map.delete(mode)
41
+ end
42
+
43
+ def clear
44
+ @mode_stack.clear
45
+ end
46
+
47
+ # Register bindings for a mode without activating it
48
+ def register_mode(mode, bindings)
49
+ @command_map[mode] = bindings
50
+ end
51
+
52
+ # Activate a single mode (clears the stack, then pushes)
53
+ def activate(mode)
54
+ clear
55
+ push_mode(mode, @command_map[mode] || {})
56
+ end
57
+
58
+ # Activate a full stack of modes in order; last wins on dispatch
59
+ def activate_stack(modes)
60
+ clear
61
+ modes.each { |m| push_mode(m, @command_map[m] || {}) }
62
+ end
63
+
64
+ def mode_stack
65
+ @mode_stack.dup
66
+ end
67
+ end
68
+ end
69
+ end