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,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../core/services/base_service.rb'
4
+ require_relative '../../adapters/book_sources/epub/parsers/metadata_extractor.rb'
5
+ require_relative '../../adapters/storage/recent_files'
6
+ require_relative '../../adapters/storage/repositories/cached_library_repository'
7
+
8
+ module Shoko
9
+ module Application::UseCases
10
+ # Facade providing catalog data (cached books, scan status, metadata) to higher layers.
11
+ # Wraps the infrastructure scanner/metadata helpers so presentation never touches them directly.
12
+ class CatalogService < BaseService
13
+ def initialize(dependencies)
14
+ super
15
+ @scanner = resolve(:library_scanner)
16
+ @cached_library_repository = resolve(:cached_library_repository) if registered?(:cached_library_repository)
17
+ @metadata_cache = {}
18
+ end
19
+
20
+ def required_dependencies
21
+ [:library_scanner]
22
+ end
23
+
24
+ def load_cached
25
+ @scanner.load_cached
26
+ end
27
+
28
+ def cached_library_entries
29
+ return [] unless @cached_library_repository
30
+
31
+ entries = @cached_library_repository.list_entries
32
+ return [] if entries.empty?
33
+
34
+ recent_index = index_recent_by_path
35
+ entries.each do |entry|
36
+ path = entry[:epub_path] || entry['epub_path']
37
+ entry[:last_accessed] = recent_index[path] if path
38
+ end
39
+ entries
40
+ rescue StandardError
41
+ []
42
+ end
43
+
44
+ def start_scan(force: false)
45
+ @scanner.start_scan(force: force)
46
+ end
47
+
48
+ def process_results
49
+ results = @scanner.process_results
50
+ update_entries(results) if results
51
+ results
52
+ end
53
+
54
+ def entries
55
+ @scanner.epubs || []
56
+ end
57
+
58
+ def update_entries(entries)
59
+ @scanner.epubs = entries
60
+ clear_metadata_cache
61
+ end
62
+
63
+ def scan_status
64
+ @scanner.scan_status
65
+ end
66
+
67
+ def scan_status=(value)
68
+ @scanner.scan_status = value
69
+ end
70
+
71
+ def scan_message
72
+ @scanner.scan_message
73
+ end
74
+
75
+ def scan_message=(value)
76
+ @scanner.scan_message = value
77
+ end
78
+
79
+ def cleanup
80
+ @scanner.cleanup if @scanner.respond_to?(:cleanup)
81
+ end
82
+
83
+ def metadata_for(path)
84
+ return {} unless path
85
+
86
+ @metadata_cache[path] ||= begin
87
+ Adapters::BookSources::Epub::Parsers::MetadataExtractor.from_epub(path)
88
+ rescue StandardError
89
+ {}
90
+ end
91
+ end
92
+
93
+ def size_for(path)
94
+ return 0 unless path
95
+
96
+ File.size(path)
97
+ rescue StandardError
98
+ 0
99
+ end
100
+
101
+ def clear_metadata_cache
102
+ @metadata_cache.clear
103
+ end
104
+
105
+ private
106
+
107
+ def index_recent_by_path
108
+ items = Adapters::Storage::RecentFiles.load
109
+ Array(items).each_with_object({}) do |recent_item, acc|
110
+ path = recent_item['path'] || recent_item[:path]
111
+ accessed = recent_item['accessed'] || recent_item[:accessed]
112
+ acc[path] = accessed if path && accessed
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_command'
4
+
5
+ module Shoko
6
+ module Application
7
+ module Commands
8
+ # Commands for driving the Annotation Editor screen via a clean, public API.
9
+ class AnnotationEditorCommand < BaseCommand
10
+ def initialize(action, name: nil, description: nil)
11
+ @action = action
12
+ super(
13
+ name: name || "annotation_editor_#{action}",
14
+ description: description || "Annotation editor #{action.to_s.tr('_', ' ')}"
15
+ )
16
+ end
17
+
18
+ protected
19
+
20
+ def perform(context, params = {})
21
+ ui = context.dependencies.resolve(:ui_controller) if context.respond_to?(:dependencies)
22
+ # Prefer a context-provided editor component (menu or reader), else fall back to UI current_mode
23
+ mode = if context.respond_to?(:current_editor_component)
24
+ context.current_editor_component
25
+ else
26
+ ui&.current_mode
27
+ end
28
+
29
+ case @action
30
+ when :save
31
+ return :handled if dispatched_to_mode?(mode, :save_annotation)
32
+ when :cancel
33
+ # Reader: cleanup + back to read; Menu: back to annotations
34
+ if dispatched_to_mode?(mode, :cancel_annotation)
35
+ # Mode handled the cancel path completely
36
+ elsif ui
37
+ begin
38
+ ui.cleanup_popup_state
39
+ rescue StandardError
40
+ # no-op (menu has no popup state)
41
+ end
42
+ begin
43
+ ui.switch_mode(:read)
44
+ rescue StandardError
45
+ # fall through to menu path
46
+ end
47
+ else
48
+ # Menu context: switch to annotations screen
49
+ begin
50
+ context.state.dispatch(Shoko::Application::Actions::UpdateMenuAction.new(mode: :annotations))
51
+ rescue StandardError
52
+ # best-effort
53
+ end
54
+ end
55
+ when :backspace
56
+ return :handled if dispatched_to_mode?(mode, :handle_backspace)
57
+ when :enter
58
+ return :handled if dispatched_to_mode?(mode, :handle_enter)
59
+ when :insert_char
60
+ ch = (params[:key] || '').to_s
61
+ return :pass if ch.empty?
62
+
63
+ return :handled if dispatched_to_mode?(mode, :handle_character, ch)
64
+ else
65
+ return :pass
66
+ end
67
+
68
+ :handled
69
+ end
70
+
71
+ private
72
+
73
+ def dispatched_to_mode?(mode, method_name, *)
74
+ return false unless mode.respond_to?(method_name)
75
+
76
+ mode.public_send(method_name, *)
77
+ true
78
+ end
79
+ end
80
+
81
+ # Factory methods for building AnnotationEditor commands.
82
+ module AnnotationEditorCommandFactory
83
+ def self.save
84
+ AnnotationEditorCommand.new(:save)
85
+ end
86
+
87
+ def self.cancel
88
+ AnnotationEditorCommand.new(:cancel)
89
+ end
90
+
91
+ def self.backspace
92
+ AnnotationEditorCommand.new(:backspace)
93
+ end
94
+
95
+ def self.enter
96
+ AnnotationEditorCommand.new(:enter)
97
+ end
98
+
99
+ def self.insert_char
100
+ AnnotationEditorCommand.new(:insert_char)
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_command'
4
+
5
+ module Shoko
6
+ module Application
7
+ module Commands
8
+ # Application-level commands for mode switching and system control.
9
+ class ApplicationCommand < BaseCommand
10
+ def initialize(action, name: nil, description: nil)
11
+ @action = action
12
+ super(
13
+ name: name || "app_#{action}",
14
+ description: description || "Application #{action.to_s.tr('_', ' ')}"
15
+ )
16
+ end
17
+
18
+ protected
19
+
20
+ def perform(context, _params = {})
21
+ deps = dependencies_from(context)
22
+
23
+ case @action
24
+ when :quit_to_menu
25
+ handle_quit_to_menu(deps)
26
+ when :quit_application
27
+ handle_quit_application(deps)
28
+ when :toggle_view_mode
29
+ handle_toggle_view_mode(deps)
30
+ when :show_help
31
+ handle_show_help(deps)
32
+ when :show_toc
33
+ handle_show_toc(deps)
34
+ when :show_bookmarks
35
+ handle_show_bookmarks(deps)
36
+ when :show_annotations
37
+ handle_show_annotations(deps)
38
+ else
39
+ raise ExecutionError.new("Unknown application action: #{@action}", command_name: name)
40
+ end
41
+
42
+ @action
43
+ end
44
+
45
+ private
46
+
47
+ def handle_quit_to_menu(deps)
48
+ controller = resolve_optional(deps, :state_controller)
49
+ if controller.respond_to?(:quit_to_menu)
50
+ controller.quit_to_menu
51
+ else
52
+ dispatch_action(deps, Application::Actions::QuitToMenuAction.new)
53
+ end
54
+ end
55
+
56
+ def handle_quit_application(deps)
57
+ controller = resolve_optional(deps, :state_controller)
58
+ if controller.respond_to?(:quit_application)
59
+ controller.quit_application
60
+ return
61
+ end
62
+
63
+ handle_quit_to_menu(deps)
64
+ force_cleanup(deps)
65
+ Kernel.exit(0)
66
+ end
67
+
68
+ def handle_toggle_view_mode(deps)
69
+ controller = resolve_optional(deps, :ui_controller)
70
+ if controller.respond_to?(:toggle_view_mode)
71
+ controller.toggle_view_mode
72
+ else
73
+ dispatch_action(deps, Application::Actions::ToggleViewModeAction.new)
74
+ end
75
+ end
76
+
77
+ def handle_show_help(deps)
78
+ controller = resolve_optional(deps, :ui_controller)
79
+ if controller.respond_to?(:show_help)
80
+ controller.show_help
81
+ else
82
+ state_store = resolve_state_store(deps)
83
+ state_store&.set(%i[reader mode], :help)
84
+ end
85
+ end
86
+
87
+ def handle_show_toc(deps)
88
+ controller = resolve_optional(deps, :ui_controller)
89
+ return unless controller.respond_to?(:open_toc)
90
+
91
+ controller.open_toc
92
+ end
93
+
94
+ def handle_show_bookmarks(deps)
95
+ controller = resolve_optional(deps, :ui_controller)
96
+ return unless controller.respond_to?(:open_bookmarks)
97
+
98
+ controller.open_bookmarks
99
+ end
100
+
101
+ def handle_show_annotations(deps)
102
+ controller = resolve_optional(deps, :ui_controller)
103
+ return unless controller.respond_to?(:open_annotations)
104
+
105
+ controller.open_annotations
106
+ end
107
+
108
+ def dependencies_from(context)
109
+ return context.dependencies if context.respond_to?(:dependencies)
110
+
111
+ raise ExecutionError.new('Command context must expose dependencies', command_name: name)
112
+ end
113
+
114
+ def resolve_optional(deps, key)
115
+ return nil if deps.respond_to?(:registered?) && !deps.registered?(key)
116
+
117
+ deps.resolve(key)
118
+ rescue StandardError
119
+ nil
120
+ end
121
+
122
+ def resolve_state_store(deps)
123
+ resolve_optional(deps, :state_store) || resolve_optional(deps, :global_state)
124
+ end
125
+
126
+ def dispatch_action(deps, action)
127
+ state_store = resolve_state_store(deps)
128
+ state_store&.dispatch(action)
129
+ end
130
+
131
+ def force_cleanup(deps)
132
+ terminal = resolve_optional(deps, :terminal_service)
133
+ return unless terminal
134
+
135
+ if terminal.respond_to?(:force_cleanup)
136
+ terminal.force_cleanup
137
+ elsif terminal.respond_to?(:cleanup)
138
+ terminal.cleanup
139
+ end
140
+ end
141
+ end
142
+
143
+ # Command that switches the reader into a specific UI mode.
144
+ class ModeCommand < BaseCommand
145
+ def initialize(mode, name: nil, description: nil)
146
+ @mode = mode
147
+ super(
148
+ name: name || "mode_#{mode}",
149
+ description: description || "Switch to #{mode} mode"
150
+ )
151
+ end
152
+
153
+ def validate_parameters(params)
154
+ super
155
+
156
+ valid_modes = %i[read help search]
157
+ return if valid_modes.include?(@mode)
158
+
159
+ raise ValidationError.new("Mode must be one of #{valid_modes}", command_name: name)
160
+ end
161
+
162
+ protected
163
+
164
+ def perform(context, _params = {})
165
+ state_store = context.dependencies.resolve(:state_store)
166
+ state_store.set(%i[reader mode], @mode)
167
+
168
+ @mode
169
+ end
170
+ end
171
+
172
+ # Factory methods for common application commands
173
+ module ApplicationCommandFactory
174
+ def self.quit_to_menu
175
+ ApplicationCommand.new(:quit_to_menu)
176
+ end
177
+
178
+ def self.quit_application
179
+ ApplicationCommand.new(:quit_application)
180
+ end
181
+
182
+ def self.toggle_view_mode
183
+ ApplicationCommand.new(:toggle_view_mode)
184
+ end
185
+
186
+ def self.show_help
187
+ ApplicationCommand.new(:show_help)
188
+ end
189
+
190
+ def self.show_toc
191
+ ApplicationCommand.new(:show_toc)
192
+ end
193
+
194
+ def self.show_bookmarks
195
+ ApplicationCommand.new(:show_bookmarks)
196
+ end
197
+
198
+ def self.show_annotations
199
+ ApplicationCommand.new(:show_annotations)
200
+ end
201
+
202
+ def self.switch_to_mode(mode)
203
+ ModeCommand.new(mode)
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Application
5
+ module Commands
6
+ # Enhanced base command with proper error handling and validation.
7
+ # Replaces the mixed command patterns with consistent implementation.
8
+ class BaseCommand
9
+ # Base error type for command execution and validation failures.
10
+ class CommandError < StandardError
11
+ attr_reader :command_name, :context
12
+
13
+ def initialize(message, command_name: nil, context: nil)
14
+ super(message)
15
+ @command_name = command_name
16
+ @context = context
17
+ end
18
+ end
19
+
20
+ class ValidationError < CommandError; end
21
+ class ExecutionError < CommandError; end
22
+
23
+ attr_reader :name, :description
24
+
25
+ def initialize(name: nil, description: nil, logger: nil)
26
+ @name = name || self.class.name.split('::').last
27
+ @description = description || @name
28
+ @logger = logger
29
+ end
30
+
31
+ # Execute command with full error handling and validation
32
+ #
33
+ # @param context [Object] Execution context (controller, service, etc.)
34
+ # @param params [Hash] Command parameters
35
+ # @return [Symbol] :handled, :pass, or :error
36
+ def execute(context, params = {})
37
+ validate_context(context)
38
+ validate_parameters(params)
39
+
40
+ return :pass unless can_execute?(context, params)
41
+
42
+ begin
43
+ result = perform(context, params)
44
+ handle_success(context, result)
45
+ :handled
46
+ rescue StandardError => e
47
+ handle_error(context, e, params)
48
+ :error
49
+ end
50
+ end
51
+
52
+ # Check if command can be executed
53
+ #
54
+ # @param context [Object] Execution context
55
+ # @param params [Hash] Command parameters
56
+ # @return [Boolean]
57
+ def can_execute?(_context, _params = {})
58
+ true # Override in subclasses for conditional execution
59
+ end
60
+
61
+ # Validate execution context
62
+ #
63
+ # @param context [Object] Execution context
64
+ # @raise [ValidationError] if context is invalid
65
+ def validate_context(context)
66
+ raise ValidationError.new('Context cannot be nil', command_name: name) if context.nil?
67
+ end
68
+
69
+ # Validate command parameters
70
+ #
71
+ # @param params [Hash] Command parameters
72
+ # @raise [ValidationError] if parameters are invalid
73
+ def validate_parameters(params)
74
+ # Override in subclasses for parameter validation
75
+ end
76
+
77
+ protected
78
+
79
+ # Perform the actual command logic
80
+ # Must be implemented by subclasses
81
+ #
82
+ # @param context [Object] Execution context
83
+ # @param params [Hash] Command parameters
84
+ # @return [Object] Command result
85
+ def perform(context, params = {})
86
+ raise NotImplementedError, "#{self.class.name} must implement #perform"
87
+ end
88
+
89
+ # Handle successful command execution
90
+ #
91
+ # @param context [Object] Execution context
92
+ # @param result [Object] Command result
93
+ def handle_success(context, result)
94
+ log_success(context, result)
95
+ end
96
+
97
+ # Handle command execution errors
98
+ #
99
+ # @param context [Object] Execution context
100
+ # @param error [StandardError] The error that occurred
101
+ # @param params [Hash] Command parameters
102
+ def handle_error(context, error, params = {})
103
+ log_error(context, error, params)
104
+
105
+ # Show user-friendly error message if possible
106
+ return unless context.respond_to?(:show_error_message)
107
+
108
+ context.show_error_message(user_friendly_error_message(error))
109
+ end
110
+
111
+ private
112
+
113
+ def log_success(context, result)
114
+ logger = resolve_logger(context)
115
+ logger&.debug(
116
+ 'Command executed successfully',
117
+ command: name,
118
+ context: context.class.name,
119
+ result: result.inspect
120
+ )
121
+ end
122
+
123
+ def log_error(context, error, params)
124
+ logger = resolve_logger(context)
125
+ logger&.error(
126
+ 'Command execution failed',
127
+ command: name,
128
+ context: context.class.name,
129
+ error: error.message,
130
+ params: params,
131
+ backtrace: error.backtrace.first(5)
132
+ )
133
+ end
134
+
135
+ def resolve_logger(context)
136
+ return @logger if defined?(@logger) && @logger
137
+
138
+ candidate = begin
139
+ if context.respond_to?(:dependencies)
140
+ deps = context.dependencies
141
+ deps.resolve(:logger) if deps.respond_to?(:resolve)
142
+ elsif context.respond_to?(:logger)
143
+ context.logger
144
+ end
145
+ rescue StandardError
146
+ nil
147
+ end
148
+ @logger = candidate if candidate
149
+ candidate
150
+ end
151
+
152
+ def user_friendly_error_message(error)
153
+ msg = error.message
154
+ case error
155
+ when ValidationError
156
+ "Invalid input: #{msg}"
157
+ when CommandError
158
+ msg
159
+ else
160
+ 'An unexpected error occurred'
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_command'
4
+
5
+ module Shoko
6
+ module Application
7
+ module Commands
8
+ # Bookmark management commands using domain services.
9
+ class BookmarkCommand < BaseCommand
10
+ def initialize(action, name: nil, description: nil)
11
+ @action = action
12
+ super(
13
+ name: name || "bookmark_#{action}",
14
+ description: description || "Bookmark #{action.to_s.tr('_', ' ')}"
15
+ )
16
+ end
17
+
18
+ def can_execute?(context, _params = {})
19
+ deps = context.dependencies
20
+ deps.registered?(:bookmark_service) && deps.registered?(:state_store)
21
+ end
22
+
23
+ protected
24
+
25
+ def perform(context, params = {})
26
+ deps = context.dependencies
27
+ bookmark_service = deps.resolve(:bookmark_service)
28
+
29
+ case @action
30
+ when :add
31
+ handle_add_bookmark(bookmark_service, params)
32
+ when :remove
33
+ handle_remove_bookmark(bookmark_service, params)
34
+ when :toggle
35
+ handle_toggle_bookmark(bookmark_service, params)
36
+ when :jump_to
37
+ handle_jump_to_bookmark(bookmark_service, params)
38
+ else
39
+ raise ExecutionError.new("Unknown bookmark action: #{@action}", command_name: name)
40
+ end
41
+
42
+ @action
43
+ end
44
+
45
+ private
46
+
47
+ def handle_add_bookmark(service, params)
48
+ text_snippet = params[:text_snippet]
49
+ service.add_bookmark(text_snippet)
50
+ end
51
+
52
+ def handle_remove_bookmark(service, params)
53
+ bookmark = params[:bookmark]
54
+
55
+ raise ValidationError.new('Bookmark required for remove action', command_name: name) unless bookmark
56
+
57
+ service.remove_bookmark(bookmark)
58
+ end
59
+
60
+ def handle_toggle_bookmark(service, params)
61
+ text_snippet = params[:text_snippet]
62
+ result = service.toggle_bookmark(text_snippet)
63
+
64
+ { result: result }
65
+ end
66
+
67
+ def handle_jump_to_bookmark(service, params)
68
+ bookmark = params[:bookmark]
69
+
70
+ raise ValidationError.new('Bookmark required for jump_to action', command_name: name) unless bookmark
71
+
72
+ service.jump_to_bookmark(bookmark)
73
+ end
74
+ end
75
+
76
+ # Factory methods for bookmark commands
77
+ module BookmarkCommandFactory
78
+ def self.add_bookmark(text_snippet = nil)
79
+ command = BookmarkCommand.new(:add)
80
+
81
+ # If text_snippet provided, create a wrapper that includes it in params
82
+ if text_snippet
83
+ lambda do |context, params = {}|
84
+ command.execute(context, params.merge(text_snippet: text_snippet))
85
+ end
86
+ else
87
+ command
88
+ end
89
+ end
90
+
91
+ def self.remove_bookmark(bookmark)
92
+ command = BookmarkCommand.new(:remove)
93
+ lambda do |context, params = {}|
94
+ command.execute(context, params.merge(bookmark: bookmark))
95
+ end
96
+ end
97
+
98
+ def self.toggle_bookmark(text_snippet = nil)
99
+ command = BookmarkCommand.new(:toggle)
100
+ lambda do |context, params = {}|
101
+ command.execute(context, params.merge(text_snippet: text_snippet))
102
+ end
103
+ end
104
+
105
+ def self.jump_to_bookmark(bookmark)
106
+ command = BookmarkCommand.new(:jump_to)
107
+ lambda do |context, params = {}|
108
+ command.execute(context, params.merge(bookmark: bookmark))
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end