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,250 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Adapters
5
+ module Input
6
+ # Handles all input processing: key handling, popup management, mode switching
7
+ class InputController
8
+ def initialize(state, dependencies)
9
+ @state = state
10
+ @dependencies = dependencies
11
+ @dispatcher = nil
12
+ @modal_mode_stack = []
13
+ end
14
+
15
+ def setup_input_dispatcher(reader_controller)
16
+ @dispatcher = Adapters::Input::Dispatcher.new(reader_controller)
17
+ setup_consolidated_reader_bindings(reader_controller)
18
+ @dispatcher.activate_stack([:read])
19
+ end
20
+
21
+ def handle_key(key)
22
+ @dispatcher&.handle_key(key)
23
+ end
24
+
25
+ # Enhanced popup navigation handlers for direct key routing
26
+ def handle_popup_navigation(key)
27
+ with_popup_menu do |menu|
28
+ res = menu.handle_key(key)
29
+ next :pass unless res
30
+
31
+ :handled
32
+ end
33
+ end
34
+
35
+ def handle_popup_action_key(key)
36
+ with_popup_menu do |menu|
37
+ res = menu.handle_key(key) || { type: :noop }
38
+ process_popup_result(res)
39
+ end
40
+ end
41
+
42
+ def handle_popup_cancel(key)
43
+ with_popup_menu do |menu|
44
+ res = menu.handle_key(key) || { type: :noop }
45
+ process_popup_result(res)
46
+ end
47
+ end
48
+
49
+ def handle_popup_menu_input(keys)
50
+ popup_menu = Shoko::Application::Selectors::ReaderSelectors.popup_menu(@state)
51
+ return unless popup_menu
52
+
53
+ ui_controller = @dependencies.resolve(:ui_controller)
54
+ keys.each do |key|
55
+ res = popup_menu.handle_key(key) || { type: :noop }
56
+ process_popup_result(res, ui_controller)
57
+ end
58
+ end
59
+
60
+ def handle_annotations_overlay_input(keys)
61
+ overlay = Shoko::Application::Selectors::ReaderSelectors.annotations_overlay(@state)
62
+ return unless overlay
63
+
64
+ ui_controller = @dependencies.resolve(:ui_controller)
65
+ keys.each do |key|
66
+ result = overlay.handle_key(key)
67
+ next unless result
68
+
69
+ case result[:type]
70
+ when :selection_change
71
+ index = result[:index]
72
+ @state.dispatch(Shoko::Application::Actions::UpdateSidebarAction.new(
73
+ annotations_selected: index,
74
+ sidebar_annotations_selected: index
75
+ ))
76
+ when :open
77
+ if ui_controller.respond_to?(:open_annotation_from_overlay)
78
+ ui_controller.open_annotation_from_overlay(result[:annotation])
79
+ end
80
+ when :edit
81
+ if ui_controller.respond_to?(:edit_annotation_from_overlay)
82
+ ui_controller.edit_annotation_from_overlay(result[:annotation])
83
+ end
84
+ when :delete
85
+ if ui_controller.respond_to?(:delete_annotation_from_overlay)
86
+ ui_controller.delete_annotation_from_overlay(result[:annotation])
87
+ end
88
+ when :close
89
+ ui_controller.close_annotations_overlay if ui_controller.respond_to?(:close_annotations_overlay)
90
+ end
91
+ end
92
+ end
93
+
94
+ private
95
+
96
+ def with_popup_menu
97
+ popup_menu = Shoko::Application::Selectors::ReaderSelectors.popup_menu(@state)
98
+ return :pass unless popup_menu
99
+
100
+ yield popup_menu
101
+ end
102
+
103
+ def process_popup_result(result, ui_controller = @dependencies.resolve(:ui_controller))
104
+ case result[:type]
105
+ when :selection_change
106
+ # Selection change handled by popup itself
107
+ :handled
108
+ when :action
109
+ ui_controller.handle_popup_action(result)
110
+ :handled
111
+ when :cancel
112
+ ui_controller.cleanup_popup_state
113
+ ui_controller.switch_mode(:read)
114
+ :handled
115
+ else
116
+ :pass
117
+ end
118
+ end
119
+
120
+ def setup_consolidated_reader_bindings(reader_controller)
121
+ # Register reader mode bindings using Adapters::Input::CommandFactory patterns
122
+ register_read_bindings(reader_controller)
123
+ register_popup_menu_bindings(reader_controller)
124
+
125
+ # Keep legacy bindings for modes not yet converted
126
+ register_help_bindings_new(reader_controller)
127
+ register_annotation_editor_bindings_new(reader_controller)
128
+ register_library_bindings_new(reader_controller)
129
+ end
130
+
131
+ def register_read_bindings(_reader_controller)
132
+ bindings = Adapters::Input::CommandFactory.reader_navigation_commands
133
+ bindings.merge!(Adapters::Input::CommandFactory.reader_control_commands)
134
+
135
+ # When sidebar is visible, redirect up/down/enter to sidebar handlers
136
+ nav_down = Adapters::Input::KeyDefinitions::NAVIGATION[:down]
137
+ nav_down.each do |key|
138
+ bindings[key] = :conditional_down
139
+ end
140
+
141
+ nav_up = Adapters::Input::KeyDefinitions::NAVIGATION[:up]
142
+ nav_up.each do |key|
143
+ bindings[key] = :conditional_up
144
+ end
145
+
146
+ confirm_keys = Adapters::Input::KeyDefinitions::ACTIONS[:confirm]
147
+ confirm_keys.each do |key|
148
+ bindings[key] = :conditional_select
149
+ end
150
+
151
+ # Ensure TOC toggle is bound explicitly and marked handled
152
+ %w[t T].each do |key|
153
+ bindings[key] = :open_toc
154
+ end
155
+
156
+ @dispatcher.register_mode(:read, bindings)
157
+ end
158
+
159
+ def register_popup_menu_bindings(_reader_controller)
160
+ # Popup menu navigation is now handled directly in main_loop via handle_popup_menu_input
161
+ bindings = {}
162
+ bindings.merge!(Adapters::Input::CommandFactory.menu_selection_commands)
163
+ bindings.merge!(Adapters::Input::CommandFactory.exit_commands(:exit_popup_menu))
164
+ @dispatcher.register_mode(:popup_menu, bindings)
165
+ end
166
+
167
+ def register_help_bindings_new(_reader_controller)
168
+ bindings = { __default__: :exit_help }
169
+ @dispatcher.register_mode(:help, bindings)
170
+ end
171
+
172
+ def register_library_bindings_new(_reader_controller)
173
+ # Keys are registered in MainMenu#register_library_bindings; this hook ensures mode exists
174
+ # No-op here as dispatcher registration happens in MainMenu.
175
+ end
176
+
177
+ def register_annotation_editor_bindings_new(_reader_controller)
178
+ bindings = {}
179
+
180
+ cancel_cmd = Shoko::Application::Commands::AnnotationEditorCommandFactory.cancel
181
+ save_cmd = Shoko::Application::Commands::AnnotationEditorCommandFactory.save
182
+ back_cmd = Shoko::Application::Commands::AnnotationEditorCommandFactory.backspace
183
+ enter_cmd = Shoko::Application::Commands::AnnotationEditorCommandFactory.enter
184
+ insert_cmd = Shoko::Application::Commands::AnnotationEditorCommandFactory.insert_char
185
+
186
+ # Cancel editor
187
+ bindings["\e"] = cancel_cmd
188
+
189
+ # Save: Ctrl+S and 'S'
190
+ bindings["\x13"] = save_cmd
191
+ bindings['S'] = save_cmd
192
+
193
+ # Backspace (both variants)
194
+ bindings["\x7F"] = back_cmd
195
+ bindings["\b"] = back_cmd
196
+
197
+ # Enter (CR and LF)
198
+ confirm_keys = Adapters::Input::KeyDefinitions::ACTIONS[:confirm]
199
+ confirm_keys.each { |k| bindings[k] = enter_cmd }
200
+
201
+ # Default: insert printable characters
202
+ bindings[:__default__] = insert_cmd
203
+
204
+ @dispatcher.register_mode(:annotation_editor, bindings)
205
+ end
206
+
207
+ public
208
+
209
+ # Switch active bindings according to mode
210
+ def activate_for_mode(mode)
211
+ return unless @dispatcher
212
+
213
+ @modal_mode_stack.clear
214
+ case mode
215
+ when :annotation_editor
216
+ @dispatcher.activate(:annotation_editor)
217
+ when :help
218
+ @dispatcher.activate(:help)
219
+ else
220
+ @dispatcher.activate_stack([:read])
221
+ end
222
+ end
223
+
224
+ def enter_modal_mode(mode)
225
+ return unless @dispatcher
226
+
227
+ current_stack = @dispatcher.mode_stack
228
+ return if current_stack.last == mode
229
+
230
+ @modal_mode_stack << current_stack
231
+ new_stack = current_stack.empty? ? [mode] : current_stack + [mode]
232
+ @dispatcher.activate_stack(new_stack)
233
+ end
234
+
235
+ def exit_modal_mode(_mode)
236
+ return unless @dispatcher
237
+
238
+ previous_stack = @modal_mode_stack.pop
239
+ if previous_stack&.any?
240
+ @dispatcher.activate_stack(previous_stack)
241
+ else
242
+ activate_for_mode(@state.get(%i[reader mode]) || :read)
243
+ end
244
+ end
245
+
246
+ # Removed reader annotations list bindings; annotations are managed via the sidebar
247
+ end
248
+ end
249
+ end
250
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Adapters::Input
5
+ # Centralized key definitions to eliminate duplication and inconsistencies
6
+ # across the codebase. All input handling should reference these definitions.
7
+ module KeyDefinitions
8
+ # Navigation keys
9
+ NAVIGATION = {
10
+ up: ['k', "\e[A", "\eOA"].freeze,
11
+ down: ['j', "\e[B", "\eOB"].freeze,
12
+ left: ['h', "\e[D", "\eOD"].freeze,
13
+ right: ['l', "\e[C", "\eOC"].freeze,
14
+ }.freeze
15
+
16
+ # Action keys
17
+ ACTIONS = {
18
+ confirm: ["\r", "\n"].freeze,
19
+ cancel: ["\e", "\x1B"].freeze,
20
+ quit: ['q'].freeze,
21
+ force_quit: ['Q'].freeze,
22
+ space: [' '].freeze,
23
+ backspace: ['\b', "\x7F", "\x08"].freeze,
24
+ delete: ["\e[3~"].freeze,
25
+ }.freeze
26
+
27
+ # Reader-specific keys
28
+ READER = {
29
+ next_page: ['l', ' ', "\e[C", "\eOC"].freeze,
30
+ prev_page: ['h', "\e[D", "\eOD"].freeze,
31
+ scroll_down: ['j', "\e[B", "\eOB"].freeze,
32
+ scroll_up: ['k', "\e[A", "\eOA"].freeze,
33
+ next_chapter: %w[n N].freeze,
34
+ prev_chapter: ['p'].freeze,
35
+ go_to_start: ['g'].freeze,
36
+ go_to_end: ['G'].freeze,
37
+ toggle_view: %w[v V].freeze,
38
+ toggle_page_mode: ['P'].freeze,
39
+ increase_spacing: ['+'].freeze,
40
+ decrease_spacing: ['-'].freeze,
41
+ show_toc: %w[t T].freeze,
42
+ add_bookmark: ['b'].freeze,
43
+ show_bookmarks: ['B'].freeze,
44
+ show_annotations_tab: ['A'].freeze,
45
+ show_help: ['?'].freeze,
46
+ show_annotations: ["\u0001"].freeze,
47
+ rebuild_pagination: ['R'].freeze,
48
+ invalidate_pagination: ['I'].freeze,
49
+ }.freeze
50
+
51
+ # Menu navigation keys
52
+ MENU = {
53
+ browse: ['f'].freeze,
54
+ download_books: ['d'].freeze,
55
+ settings: ['s'].freeze,
56
+ search: ['S'].freeze,
57
+ refresh_scan: ['r'].freeze,
58
+ }.freeze
59
+
60
+ # Utility methods for checking key membership
61
+ module Helpers
62
+ def self.navigation_key?(key)
63
+ NAVIGATION.values.flatten.include?(key)
64
+ end
65
+
66
+ def self.up_key?(key)
67
+ NAVIGATION[:up].include?(key)
68
+ end
69
+
70
+ def self.down_key?(key)
71
+ NAVIGATION[:down].include?(key)
72
+ end
73
+
74
+ def self.confirm_key?(key)
75
+ ACTIONS[:confirm].include?(key)
76
+ end
77
+
78
+ def self.cancel_key?(key)
79
+ ACTIONS[:cancel].include?(key)
80
+ end
81
+
82
+ def self.quit_key?(key)
83
+ ACTIONS[:quit].include?(key)
84
+ end
85
+
86
+ def self.backspace_key?(key)
87
+ ACTIONS[:backspace].include?(key)
88
+ end
89
+
90
+ def self.escape_key?(key)
91
+ ACTIONS[:cancel].include?(key)
92
+ end
93
+
94
+ # Check if key matches any keys in a definition
95
+ def self.matches_keys?(key, key_list)
96
+ key_list.include?(key)
97
+ end
98
+
99
+ # Create binding hash from key list and command
100
+ def self.create_bindings(key_list, command)
101
+ key_list.each_with_object({}) do |k, bindings|
102
+ bindings[k] = command
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../core/validator.rb'
4
+ require_relative '../../storage/epub_cache.rb'
5
+
6
+ module Shoko
7
+ module Adapters::Input::Validators
8
+ # Validates file paths for EPUB files.
9
+ # Ensures paths exist, are readable, and have correct extension.
10
+ #
11
+ # @example
12
+ # validator = FilePathValidator.new
13
+ # if validator.validate?("/path/to/book.epub")
14
+ # # Path is valid
15
+ # else
16
+ # puts validator.errors
17
+ # end
18
+ class FilePathValidator < Core::Validator
19
+ # Validate a file path
20
+ #
21
+ # @param path [String] File path to validate
22
+ # @return [Boolean] Validation result
23
+ def validate?(path)
24
+ clear_errors
25
+
26
+ presence_valid?(path, :path) &&
27
+ exists?(path) &&
28
+ readable?(path) &&
29
+ extension_valid?(path) &&
30
+ not_empty?(path)
31
+ end
32
+
33
+ private
34
+
35
+ # Check if file exists
36
+ #
37
+ # @param path [String] File path
38
+ # @return [Boolean] true if exists
39
+ def exists?(path)
40
+ return true if File.exist?(path)
41
+
42
+ add_error(:path, "file does not exist: #{path}")
43
+ false
44
+ end
45
+
46
+ # Check if file is readable
47
+ #
48
+ # @param path [String] File path
49
+ # @return [Boolean] true if readable
50
+ def readable?(path)
51
+ return true if File.readable?(path)
52
+
53
+ add_error(:path, "file is not readable: #{path}")
54
+ false
55
+ end
56
+
57
+ # Check if file has EPUB extension
58
+ #
59
+ # @param path [String] File path
60
+ # @return [Boolean] true if EPUB
61
+ def extension_valid?(path)
62
+ return true if path.downcase.end_with?('.epub')
63
+ return true if Shoko::Adapters::Storage::EpubCache.cache_file?(path)
64
+
65
+ add_error(:path, 'file must have .epub or .cache extension')
66
+ false
67
+ end
68
+
69
+ # Check if file is not empty
70
+ #
71
+ # @param path [String] File path
72
+ # @return [Boolean] true if not empty
73
+ def not_empty?(path)
74
+ return true if File.size(path).positive?
75
+
76
+ add_error(:path, 'file is empty')
77
+ false
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../core/validator.rb'
4
+
5
+ module Shoko
6
+ module Adapters::Input::Validators
7
+ # Validates terminal dimensions for proper display.
8
+ # Ensures terminal is large enough for the reader interface.
9
+ class TerminalSizeValidator < Core::Validator
10
+ # Minimum terminal dimensions
11
+ MIN_WIDTH = Adapters::Output::Ui::Constants::UI::MIN_WIDTH
12
+ MIN_HEIGHT = Adapters::Output::Ui::Constants::UI::MIN_HEIGHT
13
+
14
+ # Recommended terminal dimensions for optimal experience
15
+ RECOMMENDED_WIDTH = 80
16
+ RECOMMENDED_HEIGHT = 24
17
+
18
+ # Validate terminal size
19
+ #
20
+ # @param width [Integer] Terminal width
21
+ # @param height [Integer] Terminal height
22
+ # @return [Boolean] Validation result
23
+ def validate?(width, height)
24
+ clear_errors
25
+
26
+ validate_minimum_width?(width) &
27
+ validate_minimum_height?(height)
28
+ end
29
+
30
+ # Check if terminal meets recommended size
31
+ #
32
+ # @param width [Integer] Terminal width
33
+ # @param height [Integer] Terminal height
34
+ # @return [Boolean] true if recommended size or larger
35
+ def recommended_size?(width, height)
36
+ width >= RECOMMENDED_WIDTH && height >= RECOMMENDED_HEIGHT
37
+ end
38
+
39
+ # Get size recommendations
40
+ #
41
+ # @param width [Integer] Current width
42
+ # @param height [Integer] Current height
43
+ # @return [Hash] Recommendations
44
+ def recommendations(width, height)
45
+ {
46
+ current: { width:, height: },
47
+ minimum: { width: MIN_WIDTH, height: MIN_HEIGHT },
48
+ recommended: { width: RECOMMENDED_WIDTH, height: RECOMMENDED_HEIGHT },
49
+ needs_resize: !recommended_size?(width, height),
50
+ }
51
+ end
52
+
53
+ private
54
+
55
+ def validate_minimum_width?(width)
56
+ context = RangeValidationContext.new(
57
+ width,
58
+ MIN_WIDTH..Float::INFINITY,
59
+ :width,
60
+ "Terminal width must be at least #{MIN_WIDTH} columns"
61
+ )
62
+ range_valid?(context)
63
+ end
64
+
65
+ def validate_minimum_height?(height)
66
+ context = RangeValidationContext.new(
67
+ height,
68
+ MIN_HEIGHT..Float::INFINITY,
69
+ :height,
70
+ "Terminal height must be at least #{MIN_HEIGHT} rows"
71
+ )
72
+ range_valid?(context)
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'time'
5
+
6
+ module Shoko
7
+ module Adapters::Monitoring
8
+ # Centralized logging system for the Shoko application.
9
+ # Provides structured logging with different severity levels and
10
+ # contextual information for debugging and monitoring.
11
+ #
12
+ # @example Basic usage
13
+ # Logger.info("Application started")
14
+ # Logger.error("Failed to parse EPUB", error: e, path: epub_path)
15
+ #
16
+ # @example With context
17
+ # Logger.with_context(user_id: 123) do
18
+ # Logger.info("Opening book", book_id: 456)
19
+ # end
20
+ class Logger
21
+ LEVELS = {
22
+ debug: 0,
23
+ info: 1,
24
+ warn: 2,
25
+ error: 3,
26
+ fatal: 4,
27
+ }.freeze
28
+
29
+ class << self
30
+ # Current logging level (default: info)
31
+ attr_accessor :level
32
+
33
+ # Output destination (default: STDERR)
34
+ attr_accessor :output
35
+
36
+ # Thread-local context storage
37
+ def context
38
+ Thread.current[:logger_context] ||= {}
39
+ end
40
+
41
+ # Add context for a block of code
42
+ #
43
+ # @param ctx [Hash] Context to add
44
+ # @yield Block to execute with added context
45
+ def with_context(ctx)
46
+ old_context = context.dup
47
+ context.merge!(ctx)
48
+ yield
49
+ ensure
50
+ Thread.current[:logger_context] = old_context
51
+ end
52
+
53
+ # Log at debug level
54
+ #
55
+ # @param message [String] Log message
56
+ # @param metadata [Hash] Additional metadata
57
+ def debug(message, **metadata)
58
+ log(:debug, message, metadata)
59
+ end
60
+
61
+ # Log at info level
62
+ #
63
+ # @param message [String] Log message
64
+ # @param metadata [Hash] Additional metadata
65
+ def info(message, **metadata)
66
+ log(:info, message, metadata)
67
+ end
68
+
69
+ # Log at warn level
70
+ #
71
+ # @param message [String] Log message
72
+ # @param metadata [Hash] Additional metadata
73
+ def warn(message, **metadata)
74
+ log(:warn, message, metadata)
75
+ end
76
+
77
+ # Log at error level
78
+ #
79
+ # @param message [String] Log message
80
+ # @param metadata [Hash] Additional metadata
81
+ def error(message, **metadata)
82
+ log(:error, message, metadata)
83
+ end
84
+
85
+ # Log at fatal level
86
+ #
87
+ # @param message [String] Log message
88
+ # @param metadata [Hash] Additional metadata
89
+ def fatal(message, **metadata)
90
+ log(:fatal, message, metadata)
91
+ end
92
+
93
+ private
94
+
95
+ def log(severity, message, metadata)
96
+ return if LEVELS[severity] < LEVELS[@level || :info]
97
+
98
+ entry = build_log_entry(severity, message, metadata)
99
+ (@output || $stderr).puts(entry)
100
+ rescue StandardError
101
+ # Logging should never crash the application
102
+ end
103
+
104
+ def build_log_entry(severity, message, metadata)
105
+ {
106
+ timestamp: Time.now.iso8601,
107
+ severity: severity.upcase,
108
+ message: normalize_string(message),
109
+ context: sanitize_payload(context),
110
+ metadata: sanitize_payload(metadata),
111
+ thread_id: Thread.current.object_id,
112
+ }.to_json
113
+ end
114
+
115
+ def sanitize_payload(value)
116
+ case value
117
+ when String
118
+ normalize_string(value)
119
+ when Hash
120
+ value.each_with_object({}) do |(key, val), acc|
121
+ safe_key = key.is_a?(String) ? normalize_string(key) : key
122
+ acc[safe_key] = sanitize_payload(val)
123
+ end
124
+ when Array
125
+ value.map { |item| sanitize_payload(item) }
126
+ else
127
+ value
128
+ end
129
+ end
130
+
131
+ def normalize_string(value)
132
+ str = value.to_s
133
+ return str if str.encoding == Encoding::UTF_8 && str.valid_encoding?
134
+
135
+ str.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: '?')
136
+ rescue StandardError
137
+ value.to_s
138
+ end
139
+
140
+ # Clear logger state (used in tests)
141
+ def clear
142
+ @output = nil
143
+ @level = nil
144
+ Thread.current[:logger_context] = {}
145
+ end
146
+ public :clear
147
+ end
148
+ end
149
+ end
150
+ end