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,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../base_component'
4
+ require_relative '../../constants/ui_constants'
5
+ require_relative '../../../terminal/terminal_sanitizer.rb'
6
+ require_relative '../ui/box_drawer'
7
+ require_relative 'annotation_rendering_helpers'
8
+
9
+ module Shoko
10
+ module Adapters::Output::Ui::Components
11
+ module Screens
12
+ # Detailed view for a single annotation selected from the list
13
+ class AnnotationDetailScreenComponent < BaseComponent
14
+ include Adapters::Output::Ui::Constants::UI
15
+ include UI::BoxDrawer
16
+
17
+ # Rendering context for this screen to avoid parameter clumps.
18
+ RenderContext = Struct.new(
19
+ :surface,
20
+ :bounds,
21
+ :width,
22
+ :height,
23
+ :reset,
24
+ :annotation,
25
+ :book_label,
26
+ keyword_init: true
27
+ )
28
+
29
+ def initialize(state)
30
+ super()
31
+ @state = state
32
+ @render_context = nil
33
+ end
34
+
35
+ def do_render(surface, bounds)
36
+ @render_context = build_context(surface, bounds)
37
+ render_header
38
+ return render_empty(context.surface, context.bounds) unless context.annotation
39
+
40
+ render_body
41
+ ensure
42
+ @render_context = nil
43
+ end
44
+
45
+ def preferred_height(_available_height)
46
+ :fill
47
+ end
48
+
49
+ private
50
+
51
+ def build_context(surface, bounds)
52
+ annotation = selected_annotation
53
+ RenderContext.new(
54
+ surface: surface,
55
+ bounds: bounds,
56
+ width: bounds.width,
57
+ height: bounds.height,
58
+ reset: Terminal::ANSI::RESET,
59
+ annotation: annotation ? AnnotationView.new(annotation) : nil,
60
+ book_label: resolve_book_label
61
+ )
62
+ end
63
+
64
+ def resolve_book_label
65
+ book_path = @state.get(%i[menu selected_annotation_book])
66
+ return 'Unknown Book' unless book_path
67
+
68
+ raw = File.basename(book_path)
69
+ Shoko::Adapters::Output::Terminal::TerminalSanitizer.sanitize(
70
+ raw,
71
+ preserve_newlines: false,
72
+ preserve_tabs: false
73
+ )
74
+ end
75
+
76
+ def render_header
77
+ title_width = render_title
78
+ render_actions(title_width)
79
+ render_divider
80
+ end
81
+
82
+ def render_title
83
+ title_plain = "📝 Annotation • #{context.book_label}"
84
+ title_width = Shoko::Adapters::Output::Terminal::TextMetrics.visible_length(title_plain)
85
+ title = "#{COLOR_TEXT_ACCENT}#{title_plain}#{context.reset}"
86
+ context.surface.write(context.bounds, 1, 2, title)
87
+ title_width
88
+ end
89
+
90
+ def render_actions(title_width)
91
+ actions_plain = '[o] Open • [e] Edit • [d] Delete • [ESC] Back'
92
+ actions_col = actions_column(title_width, actions_plain)
93
+ context.surface.write(
94
+ context.bounds,
95
+ 1,
96
+ actions_col,
97
+ "#{COLOR_TEXT_DIM}#{actions_plain}#{context.reset}"
98
+ )
99
+ end
100
+
101
+ def render_divider
102
+ context.surface.write(
103
+ context.bounds,
104
+ 2,
105
+ 1,
106
+ COLOR_TEXT_DIM + ('─' * context.width) + context.reset
107
+ )
108
+ end
109
+
110
+ def actions_column(title_width, actions_plain)
111
+ actions_width = Shoko::Adapters::Output::Terminal::TextMetrics.visible_length(actions_plain)
112
+ min_actions_col = 2 + title_width + 2
113
+ right_actions_col = context.width - actions_width
114
+ [right_actions_col, min_actions_col].max
115
+ end
116
+
117
+ def render_metadata
118
+ annotation = context.annotation
119
+ page_meta = annotation.page_meta
120
+ meta_line = [
121
+ "Ch: #{annotation.chapter_index || '-'}",
122
+ page_meta && "Page: #{page_meta}",
123
+ "Saved: #{annotation.formatted_date}",
124
+ ].compact.join(' ')
125
+ context.surface.write(
126
+ context.bounds,
127
+ 3,
128
+ 2,
129
+ COLOR_TEXT_DIM + meta_line + context.reset
130
+ )
131
+ end
132
+
133
+ def render_body
134
+ render_metadata
135
+ text_box = selected_text_box
136
+ render_text_box(text_box)
137
+ render_text_box(note_box(text_box))
138
+ end
139
+
140
+ def selected_text_box
141
+ AnnotationTextBox.new(
142
+ row: 5,
143
+ height: [context.height * 0.35, 8].max.to_i,
144
+ width: context.width - 4,
145
+ label: 'Selected Text',
146
+ text: context.annotation.text
147
+ )
148
+ end
149
+
150
+ def note_box(text_box)
151
+ text_box.next_box(
152
+ total_height: context.height,
153
+ label: 'Note',
154
+ text: context.annotation.note
155
+ )
156
+ end
157
+
158
+ def render_text_box(box)
159
+ box.render(context, drawer: self, color_prefix: COLOR_TEXT_PRIMARY)
160
+ end
161
+
162
+ def context
163
+ @render_context
164
+ end
165
+
166
+ def selected_annotation
167
+ ann = @state.get(%i[menu selected_annotation])
168
+ ann if ann.is_a?(Hash)
169
+ end
170
+
171
+ # draw_box and wrap_text are provided by included UI modules
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,221 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../base_component'
4
+ require_relative '../../constants/ui_constants'
5
+ require_relative '../../../terminal/terminal_sanitizer.rb'
6
+ require_relative '../ui/box_drawer'
7
+ require_relative 'annotation_rendering_helpers'
8
+
9
+ module Shoko
10
+ module Adapters::Output::Ui::Components
11
+ module Screens
12
+ # Simple annotation note editor within the menu (no book load)
13
+ class AnnotationEditScreenComponent < BaseComponent
14
+ include Adapters::Output::Ui::Constants::UI
15
+ include UI::BoxDrawer
16
+
17
+ attr_reader :edit_state
18
+
19
+ # Rendering context for this screen to avoid parameter clumps.
20
+ RenderContext = Struct.new(
21
+ :surface,
22
+ :bounds,
23
+ :width,
24
+ :height,
25
+ :reset,
26
+ :annotation,
27
+ :book_label,
28
+ keyword_init: true
29
+ )
30
+
31
+ def initialize(state, dependencies = nil)
32
+ super(dependencies)
33
+ @state = state
34
+ @dependencies = dependencies
35
+ @render_context = nil
36
+ @edit_state = AnnotationEditState.new(@state)
37
+ end
38
+
39
+ def do_render(surface, bounds)
40
+ @render_context = build_context(surface, bounds)
41
+ render_header
42
+ render_body
43
+ render_footer
44
+ ensure
45
+ @render_context = nil
46
+ end
47
+
48
+ def preferred_height(_available_height)
49
+ :fill
50
+ end
51
+
52
+ # --- Unified editor API (used by Application::Commands) ---
53
+ def save_annotation
54
+ payload = edit_state.annotation_update_payload
55
+ return unless payload
56
+
57
+ persist_annotation(payload)
58
+ edit_state.return_to_annotations_list
59
+ end
60
+
61
+ def handle_backspace
62
+ edit_state.update_from do |text, cursor|
63
+ next nil if cursor <= 0
64
+
65
+ prev_cursor = cursor - 1
66
+ [text[0...prev_cursor] + text[(prev_cursor + 1)..].to_s, prev_cursor]
67
+ end
68
+ end
69
+
70
+ def handle_enter
71
+ edit_state.update_from do |text, cursor|
72
+ new_text = text.dup
73
+ new_text.insert(cursor, "\n")
74
+ [new_text, cursor + 1]
75
+ end
76
+ end
77
+
78
+ def handle_character(char)
79
+ return unless Shoko::Adapters::Output::Terminal::TerminalSanitizer.printable_char?(char.to_s)
80
+
81
+ edit_state.update_from do |text, cursor|
82
+ new_text = text.dup
83
+ new_text.insert(cursor, char)
84
+ [new_text, cursor + 1]
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ def build_context(surface, bounds)
91
+ annotation = edit_state.selected_annotation
92
+ RenderContext.new(
93
+ surface: surface,
94
+ bounds: bounds,
95
+ width: bounds.width,
96
+ height: bounds.height,
97
+ reset: Terminal::ANSI::RESET,
98
+ annotation: AnnotationView.new(annotation || {}),
99
+ book_label: resolve_book_label
100
+ )
101
+ end
102
+
103
+ def resolve_book_label
104
+ book_path = @state.get(%i[menu selected_annotation_book])
105
+ return 'Unknown Book' unless book_path
106
+
107
+ raw = File.basename(book_path)
108
+ Shoko::Adapters::Output::Terminal::TerminalSanitizer.sanitize(
109
+ raw,
110
+ preserve_newlines: false,
111
+ preserve_tabs: false
112
+ )
113
+ end
114
+
115
+ def render_header
116
+ title_width = render_title
117
+ render_hint(title_width)
118
+ render_divider
119
+ end
120
+
121
+ def render_title
122
+ title_plain = "📝 Edit Annotation • #{context.book_label}"
123
+ title_width = Shoko::Adapters::Output::Terminal::TextMetrics.visible_length(title_plain)
124
+ title = "#{COLOR_TEXT_ACCENT}#{title_plain}#{context.reset}"
125
+ context.surface.write(context.bounds, 1, 2, title)
126
+ title_width
127
+ end
128
+
129
+ def render_hint(title_width)
130
+ hint_plain = '[Ctrl+S] Save • [ESC] Cancel'
131
+ hint_col = hint_column(title_width, hint_plain)
132
+ context.surface.write(
133
+ context.bounds,
134
+ 1,
135
+ hint_col,
136
+ "#{COLOR_TEXT_DIM}#{hint_plain}#{context.reset}"
137
+ )
138
+ end
139
+
140
+ def render_divider
141
+ context.surface.write(
142
+ context.bounds,
143
+ 2,
144
+ 1,
145
+ COLOR_TEXT_DIM + ('─' * context.width) + context.reset
146
+ )
147
+ end
148
+
149
+ def hint_column(title_width, hint_plain)
150
+ hint_width = Shoko::Adapters::Output::Terminal::TextMetrics.visible_length(hint_plain)
151
+ min_hint_col = 2 + title_width + 2
152
+ right_hint_col = context.width - hint_width
153
+ [right_hint_col, min_hint_col].max
154
+ end
155
+
156
+ def render_body
157
+ text_box = snippet_box
158
+ render_text_box(text_box)
159
+ render_note_box(note_box(text_box))
160
+ end
161
+
162
+ def render_footer
163
+ context.surface.write(
164
+ context.bounds,
165
+ context.height - 1,
166
+ 2,
167
+ "#{COLOR_TEXT_DIM}[Type] to edit • [Backspace] delete • [Enter] newline#{context.reset}"
168
+ )
169
+ end
170
+
171
+ def snippet_box
172
+ AnnotationTextBox.new(
173
+ row: 4,
174
+ height: [context.height * 0.25, 6].max.to_i,
175
+ width: context.width - 4,
176
+ label: 'Selected Text',
177
+ text: context.annotation.text
178
+ )
179
+ end
180
+
181
+ def note_box(text_box)
182
+ text_box.next_box(
183
+ total_height: context.height,
184
+ label: 'Note (editable)',
185
+ text: edit_state.text
186
+ )
187
+ end
188
+
189
+ def render_text_box(box)
190
+ box.render(context, drawer: self, color_prefix: COLOR_TEXT_PRIMARY)
191
+ end
192
+
193
+ def render_note_box(box)
194
+ render_text_box(box)
195
+ render_cursor(box)
196
+ end
197
+
198
+ def render_cursor(box)
199
+ cursor = edit_state.cursor(box.text)
200
+ row, col = box.cursor_position(cursor)
201
+ context.surface.write(context.bounds, row, col, "#{SELECTION_HIGHLIGHT}_#{context.reset}")
202
+ end
203
+
204
+ def persist_annotation(payload)
205
+ service = @dependencies&.resolve(:annotation_service)
206
+ return unless service
207
+
208
+ path, ann_id, text = payload.values_at(:path, :ann_id, :text)
209
+ service.update(path, ann_id, text)
210
+ edit_state.refresh_annotations(service)
211
+ rescue StandardError
212
+ nil
213
+ end
214
+
215
+ def context
216
+ @render_context
217
+ end
218
+ end
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../base_component'
4
+ require_relative '../../constants/ui_constants'
5
+ require_relative '../ui/box_drawer'
6
+ require_relative 'annotation_rendering_helpers'
7
+
8
+ module Shoko
9
+ module Adapters::Output::Ui::Components
10
+ module Screens
11
+ # Reader-context annotation editor as a proper component
12
+ # Replaces ReaderModes::AnnotationEditorMode
13
+ class AnnotationEditorScreenComponent < BaseComponent
14
+ include Adapters::Output::Ui::Constants::UI
15
+ include UI::BoxDrawer
16
+
17
+ # Rendering context for this screen to avoid parameter clumps.
18
+ RenderContext = Struct.new(
19
+ :surface,
20
+ :bounds,
21
+ :width,
22
+ :height,
23
+ :reset,
24
+ :selected_text,
25
+ :note_text,
26
+ keyword_init: true
27
+ )
28
+
29
+ def initialize(ui_controller, text: nil, range: nil, annotation: nil, chapter_index: nil,
30
+ dependencies: nil)
31
+ super(dependencies)
32
+ @ui = ui_controller
33
+ @dependencies = dependencies
34
+ @annotation = annotation
35
+ @selected_text = (text || annotation&.fetch('text', '') || '').dup
36
+ @note = (annotation&.fetch('note', '') || '').dup
37
+ @range = range || annotation&.fetch('range')
38
+ @chapter_index = chapter_index || annotation&.fetch('chapter_index')
39
+ @cursor_pos = @note.length
40
+ @is_editing = !annotation.nil?
41
+ @render_context = nil
42
+ end
43
+
44
+ def do_render(surface, bounds)
45
+ @render_context = build_context(surface, bounds)
46
+ render_header
47
+ render_body
48
+ render_footer
49
+ ensure
50
+ @render_context = nil
51
+ end
52
+
53
+ # Public API used by InputController bindings
54
+ def save_annotation
55
+ path = @ui.current_book_path
56
+ service = @dependencies&.resolve(:annotation_service)
57
+ return unless path && service
58
+
59
+ persist_annotation(service, path)
60
+ finalize_save
61
+ end
62
+
63
+ def handle_backspace
64
+ return unless @cursor_pos.positive?
65
+
66
+ @note.slice!(@cursor_pos - 1)
67
+ @cursor_pos -= 1
68
+ end
69
+
70
+ def handle_enter
71
+ @note.insert(@cursor_pos, "\n")
72
+ @cursor_pos += 1
73
+ end
74
+
75
+ def handle_character(key)
76
+ ord = key.ord
77
+ return unless key.to_s.length == 1 && ord >= 32 && ord < 127
78
+
79
+ @note.insert(@cursor_pos, key)
80
+ @cursor_pos += 1
81
+ end
82
+
83
+ private
84
+
85
+ def build_context(surface, bounds)
86
+ RenderContext.new(
87
+ surface: surface,
88
+ bounds: bounds,
89
+ width: bounds.width,
90
+ height: bounds.height,
91
+ reset: Terminal::ANSI::RESET,
92
+ selected_text: @selected_text.to_s.tr("\n", ' '),
93
+ note_text: @note.to_s
94
+ )
95
+ end
96
+
97
+ def render_header
98
+ title_width = render_title
99
+ render_hint(title_width)
100
+ render_divider
101
+ end
102
+
103
+ def render_title
104
+ title_plain = @is_editing ? 'Editing Annotation' : 'Creating Annotation'
105
+ title = "#{COLOR_TEXT_ACCENT}#{title_plain}#{context.reset}"
106
+ context.surface.write(context.bounds, 1, 2, title)
107
+ Shoko::Adapters::Output::Terminal::TextMetrics.visible_length(title_plain)
108
+ end
109
+
110
+ def render_hint(title_width)
111
+ hint_plain = '[Ctrl+S] Save • [ESC] Cancel'
112
+ hint_col = hint_column(title_width, hint_plain)
113
+ context.surface.write(
114
+ context.bounds,
115
+ 1,
116
+ hint_col,
117
+ "#{COLOR_TEXT_DIM}#{hint_plain}#{context.reset}"
118
+ )
119
+ end
120
+
121
+ def render_divider
122
+ context.surface.write(
123
+ context.bounds,
124
+ 2,
125
+ 1,
126
+ COLOR_TEXT_DIM + ('─' * context.width) + context.reset
127
+ )
128
+ end
129
+
130
+ def hint_column(title_width, hint_plain)
131
+ hint_width = Shoko::Adapters::Output::Terminal::TextMetrics.visible_length(hint_plain)
132
+ min_hint_col = 2 + title_width + 2
133
+ right_hint_col = context.width - hint_width
134
+ [right_hint_col, min_hint_col].max
135
+ end
136
+
137
+ def render_body
138
+ text_box = selected_text_box
139
+ render_text_box(text_box)
140
+ render_note_box(note_box(text_box))
141
+ end
142
+
143
+ def selected_text_box
144
+ AnnotationTextBox.new(
145
+ row: 4,
146
+ height: [context.height * 0.25, 6].max.to_i,
147
+ width: context.width - 4,
148
+ label: 'Selected Text',
149
+ text: context.selected_text
150
+ )
151
+ end
152
+
153
+ def note_box(text_box)
154
+ text_box.next_box(
155
+ total_height: context.height,
156
+ label: 'Note (editable)',
157
+ text: context.note_text
158
+ )
159
+ end
160
+
161
+ def render_text_box(box)
162
+ box.render(context, drawer: self, color_prefix: COLOR_TEXT_PRIMARY)
163
+ end
164
+
165
+ def render_note_box(box)
166
+ render_text_box(box)
167
+ render_cursor(box)
168
+ end
169
+
170
+ def render_cursor(box)
171
+ row, col = box.cursor_position(@cursor_pos)
172
+ context.surface.write(context.bounds, row, col, "#{SELECTION_HIGHLIGHT}_#{context.reset}")
173
+ end
174
+
175
+ def render_footer
176
+ context.surface.write(
177
+ context.bounds,
178
+ context.height - 1,
179
+ 2,
180
+ "#{COLOR_TEXT_DIM}[Type] to edit • [Backspace] delete • [Enter] newline#{context.reset}"
181
+ )
182
+ end
183
+
184
+ def persist_annotation(service, path)
185
+ if @is_editing && @annotation
186
+ service.update(path, @annotation['id'], @note)
187
+ else
188
+ service.add(path, @selected_text, @note, @range, @chapter_index, nil)
189
+ end
190
+ end
191
+
192
+ def finalize_save
193
+ @ui.refresh_annotations
194
+ @ui.cleanup_popup_state
195
+ @ui.set_message('Annotation saved!')
196
+ @ui.switch_mode(:read)
197
+ end
198
+
199
+ def context
200
+ @render_context
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end