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,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_command'
4
+ require_relative 'navigation_commands'
5
+ require_relative 'sidebar_commands'
6
+
7
+ module Shoko
8
+ module Application
9
+ module Commands
10
+ # Commands that route to different actions based on application state
11
+ class ConditionalNavigationCommand < BaseCommand
12
+ def initialize(primary_action, sidebar_action, name: nil, description: nil)
13
+ @primary_action = primary_action # Action when sidebar not visible
14
+ @sidebar_action = sidebar_action # Action when sidebar is visible
15
+ super(
16
+ name: name || "conditional_#{primary_action}",
17
+ description: description || "Conditional #{primary_action.to_s.tr('_', ' ')} navigation"
18
+ )
19
+ end
20
+
21
+ protected
22
+
23
+ def perform(context, params = {})
24
+ state = context.dependencies.resolve(:global_state)
25
+ sidebar_visible = state.get(%i[reader sidebar_visible])
26
+
27
+ if sidebar_visible
28
+ # Route to sidebar command
29
+ sidebar_command = SidebarCommand.new(@sidebar_action)
30
+ sidebar_command.execute(context, params)
31
+ else
32
+ # Route to navigation command
33
+ nav_command = NavigationCommand.new(@primary_action)
34
+ nav_command.execute(context, params)
35
+ end
36
+
37
+ sidebar_visible ? @sidebar_action : @primary_action
38
+ end
39
+
40
+ class << self
41
+ # Factory methods for common conditional navigation
42
+ def up_or_sidebar
43
+ new(:scroll_up, :up)
44
+ end
45
+
46
+ def down_or_sidebar
47
+ new(:scroll_down, :down)
48
+ end
49
+
50
+ def select_or_sidebar
51
+ new(:next_page, :select) # Enter key: next page normally, select in sidebar
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_command'
4
+
5
+ module Shoko
6
+ module Application
7
+ module Commands
8
+ # Menu commands for top-level and browse screens
9
+ class MenuCommand < BaseCommand
10
+ SETTINGS_ACTIONS = %i[
11
+ back_to_menu
12
+ toggle_view_mode
13
+ cycle_line_spacing
14
+ toggle_page_numbering_mode
15
+ toggle_page_numbers
16
+ toggle_highlight_quotes
17
+ toggle_kitty_images
18
+ wipe_cache
19
+ ].freeze
20
+ SETTINGS_MAX_INDEX = SETTINGS_ACTIONS.length - 1
21
+ private_constant :SETTINGS_ACTIONS, :SETTINGS_MAX_INDEX
22
+
23
+ def initialize(action)
24
+ @action = action
25
+ super(name: "menu_#{action}", description: "Menu action #{action}")
26
+ end
27
+
28
+ def can_execute?(context, _params = {})
29
+ context.respond_to?(:state) && (context.respond_to?(:dependencies) || true)
30
+ end
31
+
32
+ protected
33
+
34
+ def perform(context, _params = {})
35
+ state = context.state
36
+ mmc = context.respond_to?(:main_menu_component) ? context.main_menu_component : nil
37
+ can_switch = context.respond_to?(:switch_to_mode)
38
+ case @action
39
+ when :menu_up then update_menu_index(context, :selected, -1, 0, 5)
40
+ when :menu_down then update_menu_index(context, :selected, +1, 0, 5)
41
+ when :menu_select then context.handle_menu_selection if context.respond_to?(:handle_menu_selection)
42
+ when :menu_quit then context.cleanup_and_exit(0, '') if context.respond_to?(:cleanup_and_exit)
43
+ when :back_to_menu then switch_mode(context, :menu, can_switch)
44
+ when :browse_up then browse_nav(context, -1)
45
+ when :browse_down then browse_nav(context, +1)
46
+ when :browse_select then context.open_selected_book if context.respond_to?(:open_selected_book)
47
+ when :library_up then context.library_up if context.respond_to?(:library_up)
48
+ when :library_down then context.library_down if context.respond_to?(:library_down)
49
+ when :library_select then context.library_select if context.respond_to?(:library_select)
50
+ when :settings_up then update_menu_index(context, :settings_selected, -1, 0, SETTINGS_MAX_INDEX)
51
+ when :settings_down then update_menu_index(context, :settings_selected, +1, 0, SETTINGS_MAX_INDEX)
52
+ when :settings_select then perform_settings_select(context, can_switch)
53
+ when :toggle_view_mode then context.toggle_view_mode if context.respond_to?(:toggle_view_mode)
54
+ when :cycle_line_spacing then context.cycle_line_spacing if context.respond_to?(:cycle_line_spacing)
55
+ when :toggle_page_numbers then context.toggle_page_numbers if context.respond_to?(:toggle_page_numbers)
56
+ when :toggle_page_numbering_mode
57
+ context.toggle_page_numbering_mode if context.respond_to?(:toggle_page_numbering_mode)
58
+ when :toggle_highlight_quotes
59
+ context.toggle_highlight_quotes if context.respond_to?(:toggle_highlight_quotes)
60
+ when :toggle_kitty_images
61
+ context.toggle_kitty_images if context.respond_to?(:toggle_kitty_images)
62
+ when :wipe_cache then context.wipe_cache if context.respond_to?(:wipe_cache)
63
+ when :start_search
64
+ if context.respond_to?(:switch_to_search)
65
+ context.switch_to_search
66
+ else
67
+ # Fallback: set mode/search_active via actions
68
+ state.dispatch(Shoko::Application::Actions::UpdateMenuAction.new(mode: :search,
69
+ search_active: true))
70
+ end
71
+ current = (state.get(%i[menu search_query]) || '').to_s
72
+ state.dispatch(Shoko::Application::Actions::UpdateMenuAction.new(search_cursor: current.length))
73
+ when :exit_search
74
+ if context.respond_to?(:switch_to_browse)
75
+ context.switch_to_browse
76
+ else
77
+ state.dispatch(Shoko::Application::Actions::UpdateMenuAction.new(mode: :browse,
78
+ search_active: false))
79
+ end
80
+ # recent_* actions removed
81
+ # Annotations list (menu) actions
82
+ when :annotations_up
83
+ mmc&.annotations_screen&.navigate(:up)
84
+ when :annotations_down
85
+ mmc&.annotations_screen&.navigate(:down)
86
+ when :annotations_select
87
+ if mmc
88
+ screen = mmc.annotations_screen
89
+ ann = screen.current_annotation
90
+ path = screen.current_book_path
91
+ if ann && path
92
+ state.dispatch(Shoko::Application::Actions::UpdateMenuAction.new(
93
+ selected_annotation: ann,
94
+ selected_annotation_book: path
95
+ ))
96
+ switch_mode(context, :annotation_detail, can_switch)
97
+ end
98
+ end
99
+ when :annotations_edit, :annotation_detail_edit
100
+ context.open_selected_annotation_for_edit if context.respond_to?(:open_selected_annotation_for_edit)
101
+ when :annotations_delete, :annotation_detail_delete
102
+ delete_selected_annotation_if_available(context, can_switch)
103
+ # Annotation detail actions
104
+ when :annotation_detail_open
105
+ context.open_selected_annotation if context.respond_to?(:open_selected_annotation)
106
+ when :annotation_detail_back
107
+ switch_mode(context, :annotations, can_switch)
108
+ else
109
+ :pass
110
+ end
111
+ end
112
+
113
+ private
114
+
115
+ def switch_mode(context, mode, can_switch)
116
+ context.switch_to_mode(mode) if can_switch
117
+ end
118
+
119
+ def delete_selected_annotation_if_available(context, can_switch)
120
+ return unless context.respond_to?(:delete_selected_annotation)
121
+
122
+ context.delete_selected_annotation
123
+ switch_mode(context, :annotations, can_switch)
124
+ end
125
+
126
+ def update_menu_index(context, field, delta, min_idx, max_idx)
127
+ state = context.state
128
+ current = state.get([:menu, field]) || 0
129
+ new_val = (current + delta).clamp(min_idx, max_idx)
130
+ state.dispatch(Shoko::Application::Actions::UpdateMenuAction.new(field => new_val))
131
+ new_val
132
+ end
133
+
134
+ def perform_settings_select(context, can_switch)
135
+ state = context.state
136
+ index = state.get(%i[menu settings_selected]) || 0
137
+ action = SETTINGS_ACTIONS[index]
138
+ return unless action
139
+
140
+ if action == :back_to_menu
141
+ switch_mode(context, :menu, can_switch)
142
+ return
143
+ end
144
+
145
+ context.public_send(action) if context.respond_to?(action)
146
+ end
147
+
148
+ def browse_nav(context, delta)
149
+ state = context.state
150
+ # Prefer component's filtered list length; fall back to public accessor
151
+ mmc = context.respond_to?(:main_menu_component) ? context.main_menu_component : nil
152
+ max_idx = if mmc.respond_to?(:browse_screen)
153
+ cnt = mmc.browse_screen.filtered_count
154
+ [(cnt || 0) - 1, 0].max
155
+ else
156
+ epubs = (context.respond_to?(:filtered_epubs) && context.filtered_epubs) || []
157
+ [epubs.length - 1, 0].max
158
+ end
159
+
160
+ current = state.get(%i[menu browse_selected]) || 0
161
+ new_val = (current + delta).clamp(0, max_idx)
162
+ state.dispatch(Shoko::Application::Actions::UpdateMenuAction.new(browse_selected: new_val))
163
+ new_val
164
+ end
165
+
166
+ # recent navigation removed
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_command'
4
+
5
+ module Shoko
6
+ module Application
7
+ module Commands
8
+ # Navigation commands using proper domain services.
9
+ # Eliminates direct controller coupling and state manipulation.
10
+ class NavigationCommand < BaseCommand
11
+ def initialize(action, name: nil, description: nil)
12
+ @action = action
13
+ super(
14
+ name: name || "navigate_#{action}",
15
+ description: description || "Navigate #{humanize_action(action)}"
16
+ )
17
+ end
18
+
19
+ def validate_context(context)
20
+ super
21
+ return if context.respond_to?(:dependencies)
22
+
23
+ raise ValidationError.new('Context must provide dependencies', command_name: name)
24
+ end
25
+
26
+ def can_execute?(context, _params = {})
27
+ deps = context.dependencies
28
+ deps.registered?(:navigation_service) && deps.registered?(:state_store)
29
+ end
30
+
31
+ protected
32
+
33
+ def perform(context, _params = {})
34
+ deps = context.dependencies
35
+ navigation_service = deps.resolve(:navigation_service)
36
+ state_store = deps.resolve(:state_store)
37
+ current_chapter = current_chapter_from(state_store)
38
+
39
+ case @action
40
+ when :next_page
41
+ navigation_service.next_page
42
+ when :prev_page
43
+ navigation_service.prev_page
44
+ when :next_chapter
45
+ navigation_service.jump_to_chapter(current_chapter + 1)
46
+ when :prev_chapter
47
+ navigation_service.jump_to_chapter([current_chapter - 1, 0].max)
48
+ when :go_to_start
49
+ navigation_service.go_to_start
50
+ when :go_to_end
51
+ navigation_service.go_to_end
52
+ else
53
+ raise ExecutionError.new("Unknown navigation action: #{@action}", command_name: name)
54
+ end
55
+
56
+ @action
57
+ end
58
+
59
+ private
60
+
61
+ def humanize_action(action)
62
+ action.to_s.tr('_', ' ')
63
+ end
64
+
65
+ def current_chapter_from(state_store)
66
+ return 0 unless state_store.respond_to?(:current_state)
67
+
68
+ (state_store.current_state || {}).dig(:reader, :current_chapter) || 0
69
+ rescue StandardError
70
+ 0
71
+ end
72
+ end
73
+
74
+ # Command that scrolls within the current chapter by a number of lines.
75
+ class ScrollCommand < BaseCommand
76
+ def initialize(direction, lines: 1, name: nil, description: nil)
77
+ @direction = direction
78
+ @lines = lines
79
+ super(
80
+ name: name || "scroll_#{direction}",
81
+ description: description || "Scroll #{direction} #{lines} line(s)"
82
+ )
83
+ end
84
+
85
+ def validate_parameters(params)
86
+ super
87
+
88
+ valid_directions = %i[up down]
89
+ unless valid_directions.include?(@direction)
90
+ raise ValidationError.new("Direction must be one of #{valid_directions}",
91
+ command_name: name)
92
+ end
93
+
94
+ return if @lines.is_a?(Integer) && @lines.positive?
95
+
96
+ raise ValidationError.new('Lines must be a positive integer', command_name: name)
97
+ end
98
+
99
+ protected
100
+
101
+ def perform(context, _params = {})
102
+ navigation_service = context.dependencies.resolve(:navigation_service)
103
+ navigation_service.scroll(@direction, @lines)
104
+
105
+ { direction: @direction, lines: @lines }
106
+ end
107
+ end
108
+
109
+ # Command that jumps to a specific chapter index.
110
+ class JumpToChapterCommand < BaseCommand
111
+ def initialize(chapter_index = nil, name: nil, description: nil)
112
+ @chapter_index = chapter_index
113
+ super(
114
+ name: name || 'jump_to_chapter',
115
+ description: description || 'Jump to specific chapter'
116
+ )
117
+ end
118
+
119
+ def validate_parameters(params)
120
+ super
121
+
122
+ # Chapter index can come from params or constructor
123
+ index = params[:chapter_index] || @chapter_index
124
+
125
+ return if index.is_a?(Integer) && index >= 0
126
+
127
+ raise ValidationError.new('Chapter index must be a non-negative integer',
128
+ command_name: name)
129
+ end
130
+
131
+ protected
132
+
133
+ def perform(context, params = {})
134
+ navigation_service = context.dependencies.resolve(:navigation_service)
135
+ index = params[:chapter_index] || @chapter_index
136
+
137
+ navigation_service.jump_to_chapter(index)
138
+
139
+ { chapter_index: index }
140
+ end
141
+ end
142
+
143
+ # Factory methods for common navigation commands
144
+ module NavigationCommandFactory
145
+ def self.next_page
146
+ NavigationCommand.new(:next_page)
147
+ end
148
+
149
+ def self.prev_page
150
+ NavigationCommand.new(:prev_page)
151
+ end
152
+
153
+ def self.next_chapter
154
+ NavigationCommand.new(:next_chapter)
155
+ end
156
+
157
+ def self.prev_chapter
158
+ NavigationCommand.new(:prev_chapter)
159
+ end
160
+
161
+ def self.go_to_start
162
+ NavigationCommand.new(:go_to_start)
163
+ end
164
+
165
+ def self.go_to_end
166
+ NavigationCommand.new(:go_to_end)
167
+ end
168
+
169
+ def self.scroll_up(lines = 1)
170
+ ScrollCommand.new(:up, lines: lines)
171
+ end
172
+
173
+ def self.scroll_down(lines = 1)
174
+ ScrollCommand.new(:down, lines: lines)
175
+ end
176
+
177
+ def self.jump_to_chapter(index)
178
+ JumpToChapterCommand.new(index)
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_command'
4
+
5
+ module Shoko
6
+ module Application
7
+ module Commands
8
+ # Handles reader mode transitions (help) via domain command flow.
9
+ class ReaderModeCommand < BaseCommand
10
+ def initialize(action)
11
+ @action = action
12
+ super(name: "reader_mode_#{action}", description: "Reader mode action #{action}")
13
+ end
14
+
15
+ protected
16
+
17
+ def perform(context, _params = {})
18
+ ui_controller = resolve_ui_controller(context)
19
+ return :pass unless ui_controller
20
+
21
+ case @action
22
+ when :exit_help
23
+ ui_controller.switch_mode(:read)
24
+ else
25
+ raise ExecutionError.new("Unknown reader mode action: #{@action}", command_name: name)
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def resolve_ui_controller(context)
32
+ if context.respond_to?(:dependencies)
33
+ begin
34
+ deps = context.dependencies
35
+ return deps.resolve(:ui_controller) if deps.respond_to?(:resolve)
36
+ rescue StandardError
37
+ # fall through
38
+ end
39
+ end
40
+
41
+ context.respond_to?(:ui_controller) ? context.ui_controller : nil
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_command'
4
+
5
+ module Shoko
6
+ module Application
7
+ module Commands
8
+ # Commands for sidebar navigation and interaction
9
+ class SidebarCommand < BaseCommand
10
+ def initialize(action, name: nil, description: nil)
11
+ @action = action
12
+ super(
13
+ name: name || "sidebar_#{action}",
14
+ description: description || "Sidebar #{action.to_s.tr('_', ' ')}"
15
+ )
16
+ end
17
+
18
+ protected
19
+
20
+ def perform(context, _params = {})
21
+ # Get UI controller through dependency injection
22
+ ui_controller = context.dependencies.resolve(:ui_controller)
23
+
24
+ case @action
25
+ when :up
26
+ ui_controller.sidebar_up
27
+ when :down
28
+ ui_controller.sidebar_down
29
+ when :select
30
+ ui_controller.sidebar_select
31
+ else
32
+ raise ExecutionError.new("Unknown sidebar action: #{@action}", command_name: name)
33
+ end
34
+
35
+ @action
36
+ end
37
+
38
+ class << self
39
+ # Factory methods for common sidebar commands
40
+ def up
41
+ new(:up)
42
+ end
43
+
44
+ def down
45
+ new(:down)
46
+ end
47
+
48
+ def select
49
+ new(:select)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ require_relative '../../core/services/base_service.rb'
6
+ require_relative '../../adapters/storage/cache_paths'
7
+ require_relative '../../adapters/storage/recent_files'
8
+ require_relative '../state/actions/update_config_action.rb'
9
+ require_relative '../selectors/config_selectors'
10
+
11
+ module Shoko
12
+ module Application::UseCases
13
+ # Centralises configuration toggles and cache maintenance for menu settings flows.
14
+ class SettingsService < BaseService
15
+ WIPE_CACHE_MESSAGE = "All caches wiped. Use 'Find Book' to rescan"
16
+
17
+ def initialize(dependencies)
18
+ super
19
+ @state_store = resolve(:state_store)
20
+ @terminal_service = resolve(:terminal_service)
21
+ @wrapping_service = resolve(:wrapping_service) if registered?(:wrapping_service)
22
+ @recent_repository = Adapters::Storage::RecentFiles
23
+ end
24
+
25
+ # Toggle split/single view mode and persist the change.
26
+ def toggle_view_mode
27
+ current = Shoko::Application::Selectors::ConfigSelectors.view_mode(@state_store) || :split
28
+ new_mode = current == :split ? :single : :split
29
+ dispatch_config(view_mode: new_mode)
30
+ new_mode
31
+ end
32
+
33
+ # Toggle whether page numbers are displayed.
34
+ def toggle_page_numbers
35
+ current = Shoko::Application::Selectors::ConfigSelectors.show_page_numbers(@state_store)
36
+ dispatch_config(show_page_numbers: !current)
37
+ end
38
+
39
+ # Cycle through line spacing options (compact → normal → relaxed → ...).
40
+ def cycle_line_spacing
41
+ modes = Shoko::Core::Models::ReaderSettings::LINE_SPACING_VALUES
42
+ current = Shoko::Application::Selectors::ConfigSelectors.line_spacing(@state_store) || Shoko::Core::Models::ReaderSettings::DEFAULT_LINE_SPACING
43
+ next_mode = modes[(modes.index(current) || 1) + 1] || modes.first
44
+ dispatch_config(line_spacing: next_mode)
45
+ next_mode
46
+ end
47
+
48
+ # Toggle quote highlighting preference.
49
+ def toggle_highlight_quotes
50
+ current = Shoko::Application::Selectors::ConfigSelectors.highlight_quotes(@state_store)
51
+ dispatch_config(highlight_quotes: !current)
52
+ end
53
+
54
+ def toggle_kitty_images
55
+ current = Shoko::Application::Selectors::ConfigSelectors.kitty_images(@state_store)
56
+ dispatch_config(kitty_images: !current)
57
+ end
58
+
59
+ # Toggle dynamic/absolute page numbering mode.
60
+ def toggle_page_numbering_mode
61
+ current = Shoko::Application::Selectors::ConfigSelectors.page_numbering_mode(@state_store) || :dynamic
62
+ next_mode = current == :absolute ? :dynamic : :absolute
63
+ dispatch_config(page_numbering_mode: next_mode)
64
+ next_mode
65
+ end
66
+
67
+ # Wipe cached EPUB data, recent file history, and wrapping caches.
68
+ # Returns the status message applied to the catalog.
69
+ def wipe_cache(catalog: nil)
70
+ Shoko::Adapters::BookSources::EPUBFinder.clear_cache
71
+ remove_epub_cache_on_disk
72
+ @recent_repository.clear
73
+ @wrapping_service&.clear_cache
74
+
75
+ target_catalog = catalog || resolve(:catalog_service)
76
+ if target_catalog
77
+ target_catalog.update_entries([])
78
+ target_catalog.scan_status = :idle
79
+ target_catalog.scan_message = WIPE_CACHE_MESSAGE
80
+ end
81
+
82
+ WIPE_CACHE_MESSAGE
83
+ end
84
+
85
+ private
86
+
87
+ def required_dependencies
88
+ %i[state_store terminal_service]
89
+ end
90
+
91
+ def dispatch_config(payload)
92
+ @state_store.dispatch(Shoko::Application::Actions::UpdateConfigAction.new(payload))
93
+ @state_store.save_config if @state_store.respond_to?(:save_config)
94
+ end
95
+
96
+ def remove_epub_cache_on_disk
97
+ cache_root = Adapters::Storage::CachePaths.cache_root
98
+ return unless cache_root && File.directory?(cache_root)
99
+
100
+ cache_real = safe_realpath(cache_root)
101
+ return unless cache_real
102
+
103
+ FileUtils.rm_rf(cache_real)
104
+ rescue StandardError
105
+ nil
106
+ end
107
+
108
+ def safe_realpath(path)
109
+ root_real = File.realpath(File.dirname(path))
110
+ cache_real = File.realpath(path)
111
+ return cache_real if cache_real.start_with?(root_real) && allowed_cache_dir?(File.basename(cache_real))
112
+
113
+ nil
114
+ rescue StandardError
115
+ allowed_cache_dir?(File.basename(path)) ? path : nil
116
+ end
117
+
118
+ def allowed_cache_dir?(name)
119
+ %w[shoko reader].include?(name)
120
+ end
121
+ end
122
+ end
123
+ end