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,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_component'
4
+ require_relative 'layouts/vertical'
5
+ require_relative 'screens/menu_screen_component'
6
+ require_relative 'screens/browse_screen_component'
7
+ require_relative 'screens/library_screen_component'
8
+ require_relative 'screens/settings_screen_component'
9
+ require_relative 'screens/download_books_screen_component'
10
+ require_relative 'screens/annotations_screen_component'
11
+ require_relative 'screens/annotation_edit_screen_component'
12
+
13
+ module Shoko
14
+ module Adapters::Output::Ui::Components
15
+ # Root component for the main menu system
16
+ class MainMenuComponent < BaseComponent
17
+ def initialize(main_menu, dependencies)
18
+ super()
19
+ @main_menu = main_menu
20
+ @state = main_menu.state
21
+ @dependencies = dependencies
22
+ @catalog = @main_menu.catalog
23
+
24
+ setup_screen_components
25
+
26
+ # Initialize current screen
27
+ @current_screen = @screen_components[:menu]
28
+
29
+ # Observe mode changes to switch active component
30
+ @state.add_observer(self, %i[menu mode], %i[menu selected])
31
+ end
32
+
33
+ def state_changed(path, _old_value, new_value)
34
+ return unless path == %i[menu mode]
35
+
36
+ mapped = case new_value
37
+ when :search then :browse
38
+ when :download_search, :download then :download
39
+ else new_value
40
+ end
41
+ @current_screen = @screen_components[mapped] || @screen_components[:menu]
42
+ end
43
+
44
+ def do_render(surface, bounds)
45
+ current_screen&.render(surface, bounds)
46
+ end
47
+
48
+ def preferred_height(_available_height)
49
+ :fill
50
+ end
51
+
52
+ attr_reader :current_screen
53
+
54
+ # Delegate screen-specific methods
55
+ def browse_screen
56
+ @screen_components[:browse]
57
+ end
58
+
59
+ def library_screen
60
+ @screen_components[:library]
61
+ end
62
+
63
+ # recent screen removed
64
+
65
+ def settings_screen
66
+ @screen_components[:settings]
67
+ end
68
+
69
+ def download_books_screen
70
+ @screen_components[:download]
71
+ end
72
+
73
+ def annotations_screen
74
+ @screen_components[:annotations]
75
+ end
76
+
77
+ def annotation_detail_screen
78
+ @screen_components[:annotation_detail]
79
+ end
80
+
81
+ private
82
+
83
+ def setup_screen_components
84
+ @screen_components = {
85
+ menu: Screens::MenuScreenComponent.new(@state),
86
+ browse: Screens::BrowseScreenComponent.new(@catalog, @state),
87
+ library: Screens::LibraryScreenComponent.new(@state, @dependencies),
88
+ settings: Screens::SettingsScreenComponent.new(@state, @catalog),
89
+ download: Screens::DownloadBooksScreenComponent.new(@state),
90
+ annotations: Screens::AnnotationsScreenComponent.new(@state),
91
+ annotation_editor: Screens::AnnotationEditScreenComponent.new(@state, @dependencies),
92
+ annotation_detail: Screens::AnnotationDetailScreenComponent.new(@state),
93
+ }
94
+ end
95
+
96
+ public
97
+
98
+ def annotation_edit_screen
99
+ @screen_components[:annotation_editor]
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../base_component'
4
+ require_relative '../../../rendering/models/rendering_context.rb'
5
+ require_relative '../../../rendering/models/render_params.rb'
6
+ require_relative '../../../terminal/text_metrics.rb'
7
+ require_relative 'config_helpers'
8
+ require_relative 'line_drawer'
9
+ require_relative 'wrapped_lines_fetcher'
10
+
11
+ module Shoko
12
+ module Adapters::Output::Ui::Components
13
+ module Reading
14
+ # Base class for all view renderers.
15
+ #
16
+ # Subclasses implement `render_with_context` and can use helpers for layout,
17
+ # wrapped line fetching, and line drawing.
18
+ class BaseViewRenderer < BaseComponent
19
+ def initialize(dependencies)
20
+ super()
21
+ @dependencies = dependencies
22
+ raise ArgumentError, 'Dependencies must be provided to BaseViewRenderer' unless @dependencies
23
+
24
+ @layout_service = @dependencies.resolve(:layout_service)
25
+ @wrapped_lines_fetcher = WrappedLinesFetcher.new(@dependencies)
26
+ @line_drawer = nil
27
+ @last_render_key = nil
28
+ end
29
+
30
+ # Standard ComponentInterface implementation
31
+ def do_render(surface, bounds)
32
+ context = create_rendering_context
33
+ return unless context
34
+
35
+ render_key = render_key_for(context, bounds)
36
+ record_geometry = rendered_lines_missing?(context.state) || render_key != @last_render_key
37
+ rendered_lines_buffer = record_geometry ? {} : nil
38
+ placed_kitty_images = {}
39
+ @line_drawer = LineDrawer.new(
40
+ dependencies: @dependencies,
41
+ rendered_lines_buffer: rendered_lines_buffer,
42
+ placed_kitty_images: placed_kitty_images,
43
+ record_geometry: record_geometry
44
+ )
45
+
46
+ render_with_context(surface, bounds, context)
47
+
48
+ if record_geometry
49
+ dispatch_rendered_lines(context.state, rendered_lines_buffer)
50
+ @last_render_key = render_key
51
+ end
52
+ ensure
53
+ @line_drawer = nil
54
+ end
55
+
56
+ # New rendering interface using context
57
+ def render_with_context(surface, bounds, context)
58
+ raise NotImplementedError, 'Subclasses must implement render_with_context method'
59
+ end
60
+
61
+ protected
62
+
63
+ def layout_metrics(width, height, view_mode)
64
+ @layout_service.calculate_metrics(width, height, view_mode)
65
+ end
66
+
67
+ def adjust_for_line_spacing(height, line_spacing = :normal)
68
+ @layout_service.adjust_for_line_spacing(height, line_spacing)
69
+ end
70
+
71
+ def calculate_center_start_row(content_height, lines_count, line_spacing)
72
+ @layout_service.calculate_center_start_row(content_height, lines_count, line_spacing)
73
+ end
74
+
75
+ # Compute common layout values for a given view mode.
76
+ # Returns `[col_width, content_height, spacing, displayable]`.
77
+ def compute_layout(bounds, view_mode, config)
78
+ col_width, content_height = layout_metrics(bounds.width, bounds.height, view_mode)
79
+ spacing = ConfigHelpers.line_spacing(config)
80
+ displayable = adjust_for_line_spacing(content_height, spacing)
81
+ [col_width, content_height, spacing, displayable]
82
+ end
83
+
84
+ # Draw a vertical divider between columns (shared helper)
85
+ def draw_divider(surface, bounds, divider_col, start_row = 3)
86
+ col = divider_col.to_i
87
+ return if col <= 0
88
+
89
+ (start_row..[bounds.height - 1, start_row + 1].max).each do |row|
90
+ surface.write(
91
+ bounds,
92
+ row,
93
+ col,
94
+ "#{Shoko::Adapters::Output::Ui::Constants::UI::BORDER_PRIMARY}│#{Terminal::ANSI::RESET}"
95
+ )
96
+ end
97
+ end
98
+
99
+ # Shared helpers for common renderer patterns
100
+ def center_start_col(total_width, col_width)
101
+ [(total_width - col_width) / 2, 1].max
102
+ end
103
+
104
+ def fetch_wrapped_lines(document:, chapter_index:, col_width:, offset:, length:)
105
+ @wrapped_lines_fetcher.fetch(
106
+ document: document,
107
+ chapter_index: chapter_index,
108
+ col_width: col_width,
109
+ offset: offset,
110
+ length: length
111
+ )
112
+ end
113
+
114
+ def fetch_wrapped_lines_with_offset(document:, chapter_index:, col_width:, offset:, length:)
115
+ @wrapped_lines_fetcher.fetch_with_offset(
116
+ document: document,
117
+ chapter_index: chapter_index,
118
+ col_width: col_width,
119
+ offset: offset,
120
+ length: length
121
+ )
122
+ end
123
+
124
+ def snap_offset_to_image_start(lines, offset)
125
+ @wrapped_lines_fetcher.snap_offset_to_image_start(lines, offset)
126
+ end
127
+
128
+ # Shared helper to draw a list of lines with spacing and clipping considerations.
129
+ def draw_lines(surface, bounds, lines, params)
130
+ drawer = line_drawer
131
+ ctx = params.context
132
+ spacing = ctx ? ConfigHelpers.line_spacing(ctx.config) : :normal
133
+ lines.each_with_index do |line, idx|
134
+ row = params.start_row + (spacing == :relaxed ? idx * 2 : idx)
135
+ break if row > bounds.height - 1
136
+
137
+ drawer.draw_line(surface: surface, bounds: bounds, line: line, row: row, col: params.col_start,
138
+ width: params.col_width, context: ctx, column_id: params.column_id,
139
+ line_offset: params.line_offset + idx, page_id: params.page_id)
140
+ end
141
+ end
142
+
143
+ private
144
+
145
+ def create_rendering_context
146
+ state = @dependencies.resolve(:global_state)
147
+ Adapters::Output::Rendering::Models::RenderingContext.new(
148
+ document: safe_resolve(:document),
149
+ page_calculator: safe_resolve(:page_calculator),
150
+ state: state,
151
+ config: state,
152
+ view_model: nil
153
+ )
154
+ end
155
+
156
+ def safe_resolve(name)
157
+ @dependencies.registered?(name) ? @dependencies.resolve(name) : nil
158
+ end
159
+
160
+ def dispatch_rendered_lines(state, rendered_lines)
161
+ state&.dispatch(Shoko::Application::Actions::UpdateRenderedLinesAction.new(rendered_lines))
162
+ rescue StandardError
163
+ nil
164
+ end
165
+
166
+ def render_key_for(context, bounds)
167
+ state = context.state
168
+ [
169
+ bounds.width,
170
+ bounds.height,
171
+ state.get(%i[reader current_chapter]),
172
+ state.get(%i[reader current_page_index]),
173
+ state.get(%i[reader left_page]),
174
+ state.get(%i[reader right_page]),
175
+ state.get(%i[reader single_page]),
176
+ context.view_mode,
177
+ context.page_numbering_mode,
178
+ Shoko::Application::Selectors::ConfigSelectors.line_spacing(state),
179
+ Shoko::Application::Selectors::ConfigSelectors.kitty_images(state),
180
+ context.document&.object_id
181
+ ]
182
+ end
183
+
184
+ def rendered_lines_missing?(state)
185
+ lines = Shoko::Application::Selectors::ReaderSelectors.rendered_lines(state)
186
+ !lines || lines.empty?
187
+ rescue StandardError
188
+ true
189
+ end
190
+
191
+ def line_drawer
192
+ return @line_drawer if @line_drawer
193
+
194
+ raise ArgumentError, 'LineDrawer not initialized (do_render not active)'
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Adapters::Output::Ui::Components
5
+ module Reading
6
+ # Shared helpers for resolving configuration values from the state store.
7
+ module ConfigHelpers
8
+ module_function
9
+
10
+ def config_store(config)
11
+ return config if config.respond_to?(:get)
12
+ return config.state if config.respond_to?(:state) && config.state.respond_to?(:get)
13
+
14
+ nil
15
+ end
16
+
17
+ def line_spacing(config)
18
+ store = config_store(config)
19
+ return Shoko::Core::Models::ReaderSettings::DEFAULT_LINE_SPACING unless store
20
+
21
+ Shoko::Application::Selectors::ConfigSelectors.line_spacing(store) ||
22
+ Shoko::Core::Models::ReaderSettings::DEFAULT_LINE_SPACING
23
+ rescue StandardError
24
+ Shoko::Core::Models::ReaderSettings::DEFAULT_LINE_SPACING
25
+ end
26
+
27
+ def highlight_quotes?(store)
28
+ value = store&.get(%i[config highlight_quotes])
29
+ value.nil? || value
30
+ rescue StandardError
31
+ true
32
+ end
33
+
34
+ def highlight_keywords?(store)
35
+ !!store&.get(%i[config highlight_keywords])
36
+ rescue StandardError
37
+ false
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_view_renderer'
4
+
5
+ module Shoko
6
+ module Adapters::Output::Ui::Components
7
+ module Reading
8
+ # Renderer for help screen display
9
+ class HelpRenderer < BaseViewRenderer
10
+ HELP_LINES = [
11
+ '',
12
+ 'Navigation Keys:',
13
+ ' j / ↓ Scroll down',
14
+ ' k / ↑ Scroll up',
15
+ ' l / → Next page',
16
+ ' h / ← Previous page',
17
+ ' SPACE Next page',
18
+ ' n Next chapter',
19
+ ' p Previous chapter',
20
+ ' g Go to beginning of chapter',
21
+ ' G Go to end of chapter',
22
+ '',
23
+ 'View Options:',
24
+ ' v Toggle split/single view',
25
+ ' P Toggle page numbering mode (Absolute/Dynamic)',
26
+ ' + / - Adjust line spacing',
27
+ '',
28
+ 'Features:',
29
+ ' t Show Table of Contents',
30
+ ' b Add a bookmark',
31
+ ' B Show bookmarks',
32
+ ' A Show annotations sidebar',
33
+ ' Ctrl+A Toggle annotations overlay',
34
+ '',
35
+ 'Other Keys:',
36
+ ' ? Show/hide this help',
37
+ ' q Quit to menu',
38
+ ' Q Quit application',
39
+ '',
40
+ '',
41
+ 'Press any key to return to reading...',
42
+ ].freeze
43
+
44
+ def render_with_context(surface, bounds, _context)
45
+ b_height = bounds.height
46
+ b_width = bounds.width
47
+ start_row = [(b_height - HELP_LINES.size) / 2, 1].max
48
+
49
+ HELP_LINES.each_with_index do |line, idx|
50
+ row = start_row + idx
51
+ break if row >= b_height - 2
52
+
53
+ text_width = Shoko::Adapters::Output::Terminal::TextMetrics.visible_length(line)
54
+ col = [(b_width - text_width) / 2, 1].max
55
+ surface.write(bounds, row, col,
56
+ Shoko::Adapters::Output::Ui::Constants::UI::COLOR_TEXT_PRIMARY + line + Terminal::ANSI::RESET)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../../../core/models/content_block.rb'
4
+
5
+ module Shoko
6
+ module Adapters::Output::Ui::Components
7
+ module Reading
8
+ # Applies inline quote/keyword highlighting to a list of TextSegment objects.
9
+ #
10
+ # This is used for DisplayLine rendering where segments already carry style metadata.
11
+ module InlineSegmentHighlighter
12
+ module_function
13
+
14
+ def apply(segments, block_type:, highlight_quotes:, highlight_keywords:)
15
+ return segments unless highlightable?(segments, block_type, highlight_quotes, highlight_keywords)
16
+
17
+ text = segments_text(segments)
18
+ return segments if text.empty?
19
+
20
+ quote_ranges, keyword_ranges = build_ranges(text, highlight_quotes, highlight_keywords)
21
+ return segments if quote_ranges.empty? && keyword_ranges.empty?
22
+
23
+ applicator = RangeApplicator.new(text: text, quote_ranges: quote_ranges, keyword_ranges: keyword_ranges)
24
+ applicator.apply_to_segments(segments)
25
+ rescue StandardError
26
+ segments
27
+ end
28
+
29
+ def match_ranges(text, pattern)
30
+ ranges = []
31
+ text.to_enum(:scan, pattern).each do
32
+ match = Regexp.last_match
33
+ next unless match
34
+
35
+ start_index = match.begin(0)
36
+ end_index = match.end(0)
37
+ ranges << (start_index...end_index) if end_index > start_index
38
+ end
39
+ ranges
40
+ rescue StandardError
41
+ []
42
+ end
43
+
44
+ def in_ranges?(index, ranges)
45
+ idx = index.to_i
46
+ ranges.any? { |range| idx >= range.begin && idx < range.end }
47
+ rescue StandardError
48
+ false
49
+ end
50
+
51
+ def segments_text(segments)
52
+ segments.map { |segment| segment&.text.to_s }.join
53
+ end
54
+
55
+ private
56
+
57
+ def highlightable?(segments, block_type, highlight_quotes, highlight_keywords)
58
+ return false unless highlight_quotes || highlight_keywords
59
+ return false if segments.empty?
60
+
61
+ block_type != :code
62
+ end
63
+
64
+ def build_ranges(text, highlight_quotes, highlight_keywords)
65
+ quote_ranges = highlight_quotes ? match_ranges(text, Shoko::Adapters::Output::Ui::Constants::Highlighting::QUOTE_PATTERNS) : []
66
+ keyword_ranges = highlight_keywords ? match_ranges(text, Shoko::Adapters::Output::Ui::Constants::Highlighting::HIGHLIGHT_PATTERNS) : []
67
+ [quote_ranges, keyword_ranges]
68
+ end
69
+
70
+ module_function :highlightable?, :build_ranges
71
+ private_class_method :highlightable?, :build_ranges
72
+
73
+ # Applies computed range boundaries to segments and emits new TextSegment objects.
74
+ class RangeApplicator
75
+ def initialize(text:, quote_ranges:, keyword_ranges:)
76
+ @text = text
77
+ @quote_ranges = quote_ranges
78
+ @keyword_ranges = keyword_ranges
79
+ end
80
+
81
+ def apply_to_segments(segments)
82
+ output = []
83
+ offset = 0
84
+
85
+ segments.each do |segment|
86
+ seg_text = segment.text.to_s
87
+ seg_start = offset
88
+ seg_end = seg_start + seg_text.length
89
+ offset = seg_end
90
+ next if seg_text.empty?
91
+
92
+ base_styles = segment.styles || {}
93
+ boundaries = boundaries_for_segment(seg_start, seg_end, base_styles)
94
+ output.concat(segment_pieces(boundaries, base_styles))
95
+ end
96
+
97
+ output
98
+ end
99
+
100
+ private
101
+
102
+ attr_reader :text, :quote_ranges, :keyword_ranges
103
+
104
+ def boundaries_for_segment(seg_start, seg_end, base_styles)
105
+ boundaries = [seg_start, seg_end]
106
+ return boundaries if base_styles[:code]
107
+
108
+ (quote_ranges + keyword_ranges).each do |range|
109
+ add_boundary(boundaries, range.begin, seg_start, seg_end)
110
+ add_boundary(boundaries, range.end, seg_start, seg_end)
111
+ end
112
+
113
+ boundaries.sort.uniq
114
+ end
115
+
116
+ def add_boundary(boundaries, value, seg_start, seg_end)
117
+ boundaries << value if value > seg_start && value < seg_end
118
+ end
119
+
120
+ def segment_pieces(boundaries, base_styles)
121
+ boundaries.each_cons(2).filter_map do |start_index, end_index|
122
+ next if start_index >= end_index
123
+
124
+ piece = text[start_index...end_index].to_s
125
+ next if piece.empty?
126
+
127
+ styles = styles_for_piece(base_styles, start_index)
128
+ Shoko::Core::Models::TextSegment.new(text: piece, styles: styles)
129
+ end
130
+ end
131
+
132
+ def styles_for_piece(base_styles, index)
133
+ return base_styles if base_styles[:code]
134
+
135
+ styles = base_styles
136
+ styles = styles.merge(quote: true) if InlineSegmentHighlighter.in_ranges?(index, quote_ranges)
137
+ styles = styles.merge(accent: true) if InlineSegmentHighlighter.in_ranges?(index, keyword_ranges)
138
+ styles
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end