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,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../ui/text_utils'
4
+ require_relative '../../../terminal/text_metrics.rb'
5
+
6
+ module Shoko
7
+ module Adapters::Output::Ui::Components
8
+ module Screens
9
+ # Normalized view of annotation data for screen rendering.
10
+ class AnnotationView
11
+ def initialize(annotation)
12
+ @annotation = annotation.is_a?(Hash) ? annotation : {}
13
+ end
14
+
15
+ def text
16
+ fetch(:text).to_s
17
+ end
18
+
19
+ def note
20
+ fetch(:note).to_s
21
+ end
22
+
23
+ def chapter_index
24
+ fetch(:chapter_index)
25
+ end
26
+
27
+ def id
28
+ fetch(:id)
29
+ end
30
+
31
+ def formatted_date
32
+ created = fetch(:created_at)
33
+ created.to_s.tr('T', ' ').sub('Z', '')
34
+ end
35
+
36
+ def page_meta
37
+ curr = fetch(:page_current)
38
+ total = fetch(:page_total)
39
+ return nil unless curr && total
40
+
41
+ mode = fetch(:page_mode).to_s
42
+ label = mode.empty? ? '' : "#{mode}: "
43
+ "#{label}#{curr}/#{total}"
44
+ end
45
+
46
+ private
47
+
48
+ def fetch(key)
49
+ @annotation[key] || @annotation[key.to_s]
50
+ end
51
+ end
52
+
53
+ # Text box helper for annotation screens.
54
+ class AnnotationTextBox
55
+ BOX_COLUMN = 2
56
+ TEXT_COLUMN = 4
57
+ BOX_SPACING = 2
58
+ MIN_HEIGHT = 6
59
+ BOTTOM_PADDING = 3
60
+
61
+ attr_reader :row, :height, :width, :label, :text
62
+
63
+ def initialize(row:, height:, width:, label:, text:)
64
+ @row = row
65
+ @height = height
66
+ @width = width
67
+ @label = label
68
+ @text = text.to_s
69
+ end
70
+
71
+ def inner_width
72
+ width - 4
73
+ end
74
+
75
+ def frame_args
76
+ {
77
+ row: row,
78
+ height: height,
79
+ width: width,
80
+ label: label,
81
+ }
82
+ end
83
+
84
+ def each_visible_line(&block)
85
+ return enum_for(__method__) unless block
86
+
87
+ lines = UI::TextUtils.wrap_text(text, inner_width)
88
+ lines.first(max_lines).each_with_index(&block)
89
+ end
90
+
91
+ def render(context, drawer:, color_prefix:)
92
+ drawer.draw_box(
93
+ context.surface,
94
+ context.bounds,
95
+ row,
96
+ BOX_COLUMN,
97
+ height,
98
+ width,
99
+ label: label
100
+ )
101
+ render_lines(context, color_prefix: color_prefix)
102
+ end
103
+
104
+ def render_lines(context, color_prefix:)
105
+ each_visible_line do |line, index|
106
+ padded = UI::TextUtils.pad_right(line, inner_width)
107
+ context.surface.write(
108
+ context.bounds,
109
+ row + 1 + index,
110
+ TEXT_COLUMN,
111
+ "#{color_prefix}#{padded}#{context.reset}"
112
+ )
113
+ end
114
+ end
115
+
116
+ def cursor_position(cursor)
117
+ cursor_lines = UI::TextUtils.wrap_text(text[0, cursor], inner_width)
118
+ cursor_row = row + 1 + [cursor_lines.length - 1, 0].max
119
+ cursor_col = TEXT_COLUMN + Shoko::Adapters::Output::Terminal::TextMetrics.visible_length(cursor_lines.last || '')
120
+ [cursor_row, cursor_col]
121
+ end
122
+
123
+ def next_box(total_height:, label:, text:)
124
+ next_row = row + height + BOX_SPACING
125
+ next_height = [total_height - next_row - BOTTOM_PADDING, MIN_HEIGHT].max
126
+ AnnotationTextBox.new(row: next_row, height: next_height, width: width, label: label, text: text)
127
+ end
128
+
129
+ private
130
+
131
+ def max_lines
132
+ [height - 2, 0].max
133
+ end
134
+ end
135
+
136
+ # Menu-state helper for annotation edit screens.
137
+ class AnnotationEditState
138
+ def initialize(state)
139
+ @state = state
140
+ end
141
+
142
+ def text
143
+ (@state.get(%i[menu annotation_edit_text]) || '').to_s
144
+ end
145
+
146
+ def cursor(text = self.text)
147
+ (@state.get(%i[menu annotation_edit_cursor]) || text.length).to_i
148
+ end
149
+
150
+ def update_from
151
+ current_text = text
152
+ current_cursor = cursor(current_text)
153
+ updated = yield(current_text, current_cursor)
154
+ update(text: updated[0], cursor: updated[1]) if updated
155
+ end
156
+
157
+ def update(text:, cursor:)
158
+ @state.dispatch(Shoko::Application::Actions::UpdateMenuAction.new(
159
+ annotation_edit_text: text,
160
+ annotation_edit_cursor: cursor
161
+ ))
162
+ end
163
+
164
+ def selected_annotation
165
+ ann = @state.get(%i[menu selected_annotation])
166
+ ann if ann.is_a?(Hash)
167
+ end
168
+
169
+ def annotation_update_payload
170
+ annotation = selected_annotation || {}
171
+ path = @state.get(%i[menu selected_annotation_book])
172
+ ann_id = annotation[:id] || annotation['id']
173
+ return nil unless path && ann_id
174
+
175
+ { path: path, ann_id: ann_id, text: text }
176
+ end
177
+
178
+ def refresh_annotations(service)
179
+ @state.dispatch(
180
+ Shoko::Application::Actions::UpdateMenuAction.new(annotations_all: service.list_all)
181
+ )
182
+ end
183
+
184
+ def return_to_annotations_list
185
+ @state.dispatch(Shoko::Application::Actions::UpdateMenuAction.new(mode: :annotations))
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,266 @@
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/text_utils'
7
+ require_relative '../ui/list_helpers'
8
+
9
+ module Shoko
10
+ module Adapters::Output::Ui::Components
11
+ module Screens
12
+ # Annotations screen component for viewing and managing annotations
13
+ class AnnotationsScreenComponent < BaseComponent
14
+ include Adapters::Output::Ui::Constants::UI
15
+ include UI::TextUtils
16
+
17
+ def initialize(state)
18
+ super()
19
+ @state = state
20
+ @selected = 0
21
+ @list = []
22
+ @mode = :book
23
+ @current_book_path = nil
24
+ @current_annotation = nil
25
+ refresh_data
26
+ end
27
+
28
+ attr_reader :selected, :current_annotation, :current_book_path
29
+
30
+ def selected=(value)
31
+ @selected = [value, 0].max
32
+ update_current_annotation
33
+ end
34
+
35
+ def navigate(direction)
36
+ annotations = current_annotations
37
+ return if annotations.empty?
38
+
39
+ case direction
40
+ when :up
41
+ @selected = [@selected - 1, 0].max
42
+ when :down
43
+ @selected = [@selected + 1, annotations.length - 1].min
44
+ end
45
+
46
+ update_current_annotation
47
+ end
48
+
49
+ # Normalize raw annotations (string-keyed hashes) into symbol-keyed items
50
+ # Includes page metadata when present
51
+ # (duplicate normalize_list method removed)
52
+
53
+ def refresh_data
54
+ prev_selected = @selected
55
+ path = @state.get(%i[reader book_path])
56
+ if path && !path.to_s.empty?
57
+ @mode = :book
58
+ @current_book_path = path
59
+ raw = @state.get(%i[reader annotations]) || []
60
+ @list = normalize_list(raw).map { |a| a.merge(book_path: path) }
61
+ else
62
+ @mode = :all
63
+ mapping = @state.get(%i[menu annotations_all]) || {}
64
+ flattened = []
65
+ mapping.each do |book_path, items|
66
+ normalize_list(items).each do |a|
67
+ flattened << a.merge(book_path: book_path)
68
+ end
69
+ end
70
+ @list = flattened
71
+ end
72
+
73
+ annotations = current_annotations
74
+ upper = [annotations.length - 1, 0].max
75
+ @selected = prev_selected.clamp(0, upper)
76
+ update_current_annotation
77
+ end
78
+
79
+ def do_render(surface, bounds)
80
+ # Ensure data is fresh each render
81
+ refresh_data
82
+ height = bounds.height
83
+ width = bounds.width
84
+
85
+ # Header
86
+ reset = Terminal::ANSI::RESET
87
+ count = current_annotations.length
88
+ all_mode = (@mode == :all)
89
+ book_label = if @current_book_path
90
+ raw = File.basename(@current_book_path)
91
+ Shoko::Adapters::Output::Terminal::TerminalSanitizer.sanitize(raw, preserve_newlines: false,
92
+ preserve_tabs: false)
93
+ else
94
+ (all_mode ? 'All Books' : 'No book selected')
95
+ end
96
+ header_left_plain = "📝 Annotations (#{count}) — #{book_label}"
97
+ header_left = "#{COLOR_TEXT_ACCENT}#{header_left_plain}#{reset}"
98
+
99
+ header_right_plain = '[Enter] Open • [e] Edit • [d] Delete'
100
+ header_right = "#{COLOR_TEXT_DIM}#{header_right_plain}#{reset}"
101
+
102
+ surface.write(bounds, 1, 2, header_left)
103
+ header_right_width = Shoko::Adapters::Output::Terminal::TextMetrics.visible_length(header_right_plain)
104
+ header_left_width = Shoko::Adapters::Output::Terminal::TextMetrics.visible_length(header_left_plain)
105
+ min_right_col = 2 + header_left_width + 2
106
+ right_aligned_col = width - header_right_width - 1
107
+ right_col = [right_aligned_col, min_right_col].max
108
+ surface.write(bounds, 1, right_col, header_right)
109
+ # Divider and column headers
110
+ surface.write(bounds, 2, 1, COLOR_TEXT_DIM + ('─' * width) + reset)
111
+ idx_w = 4
112
+ ch_w = 6
113
+ date_w = 10
114
+ book_w = all_mode ? 12 : 0
115
+ avail = width - (idx_w + ch_w + date_w + book_w + 8)
116
+ snippet_w = (avail * 0.55).to_i
117
+ note_w = avail - snippet_w
118
+ has_book_col = book_w.positive?
119
+ columns = [
120
+ ' ',
121
+ pad_right('#', idx_w),
122
+ ' ',
123
+ pad_right('Ch', ch_w),
124
+ ' ',
125
+ pad_right('Snippet', snippet_w),
126
+ ' ',
127
+ pad_right('Note', note_w),
128
+ (has_book_col ? " #{pad_right('Book', book_w)}" : ''),
129
+ ' ',
130
+ pad_right('Date', date_w),
131
+ ].join
132
+ surface.write(bounds, 3, 1, COLOR_TEXT_DIM + columns + reset)
133
+
134
+ annotations = current_annotations
135
+
136
+ if annotations.empty?
137
+ render_empty_state(surface, bounds, width, height)
138
+ else
139
+ render_annotations_list(surface, bounds, width, height, annotations, has_book_col)
140
+ end
141
+
142
+ # Footer instructions
143
+ surface.write(bounds, height - 2, 2,
144
+ "#{COLOR_TEXT_DIM}[↑/↓] Navigate • [Enter] Open • [d] Delete • [ESC] Back#{Terminal::ANSI::RESET}")
145
+ end
146
+
147
+ def preferred_height(_available_height)
148
+ :fill
149
+ end
150
+
151
+ private
152
+
153
+ def current_annotations
154
+ @list || []
155
+ end
156
+
157
+ def update_current_annotation
158
+ annotations = current_annotations
159
+ @current_annotation = annotations[@selected] if @selected < annotations.length
160
+ return unless @current_annotation
161
+
162
+ book_path = @current_annotation[:book_path]
163
+ return unless book_path
164
+
165
+ @current_book_path = book_path
166
+ end
167
+
168
+ def render_empty_state(surface, bounds, width, height)
169
+ reset = Terminal::ANSI::RESET
170
+ empty_text = "#{COLOR_TEXT_DIM}No annotations found#{reset}"
171
+ mid = height / 2
172
+ surface.write(bounds, mid,
173
+ [(width - Shoko::Adapters::Output::Terminal::TextMetrics.visible_length(empty_text) + 10) / 2, 1].max,
174
+ empty_text)
175
+
176
+ help_text = "#{COLOR_TEXT_DIM}Annotations you create while reading will appear here#{reset}"
177
+ surface.write(bounds, mid + 2,
178
+ [(width - Shoko::Adapters::Output::Terminal::TextMetrics.visible_length(help_text) + 10) / 2, 1].max,
179
+ help_text)
180
+ end
181
+
182
+ def render_annotations_list(surface, bounds, width, height, annotations, in_all)
183
+ list_start_row = 4
184
+ list_height = height - list_start_row - 2
185
+ return if list_height <= 0
186
+
187
+ start_index, visible_annotations = UI::ListHelpers.slice_visible(annotations, list_height, @selected)
188
+
189
+ visible_annotations.each_with_index do |annotation, index|
190
+ row = list_start_row + index
191
+ abs_idx = start_index + index
192
+ is_selected = (abs_idx == @selected)
193
+
194
+ render_annotation_item(surface, bounds, row, width, annotation, is_selected,
195
+ abs_idx, in_all)
196
+ end
197
+ end
198
+
199
+ def render_annotation_item(surface, bounds, row, width, annotation, is_selected,
200
+ absolute_index, in_all)
201
+ # Extract annotation details
202
+ text = (annotation[:text] || 'No text').to_s.tr("\n", ' ')
203
+ note = (annotation[:note] || '').to_s.tr("\n", ' ')
204
+ created = (annotation[:created_at] || '').to_s.split('T').first
205
+ chapter = annotation[:chapter_index]
206
+
207
+ # Column widths (match header)
208
+ idx_w = 4
209
+ ch_w = 6
210
+ date_w = 10
211
+ book_w = (in_all ? 12 : 0)
212
+ avail = width - (idx_w + ch_w + date_w + book_w + 8)
213
+ snippet_w = (avail * 0.6).to_i
214
+ note_w = avail - snippet_w
215
+
216
+ pointer = is_selected ? '▸' : ' '
217
+ idx = pad_left((absolute_index + 1).to_s, idx_w)
218
+ chv = chapter.nil? ? '-' : chapter.to_i
219
+ ch_col = pad_right(chv.to_s, ch_w)
220
+ snippet = pad_right(truncate_text(text, snippet_w), snippet_w)
221
+ note_tr = pad_right(truncate_text(note, note_w), note_w)
222
+ if in_all
223
+ bp = annotation[:book_path]
224
+ book = if bp
225
+ raw = File.basename(bp)
226
+ Shoko::Adapters::Output::Terminal::TerminalSanitizer.sanitize(raw, preserve_newlines: false,
227
+ preserve_tabs: false)
228
+ else
229
+ ''
230
+ end
231
+ book_col = pad_right(truncate_text(book, book_w), book_w)
232
+ date_col = pad_right(truncate_text(created, date_w), date_w)
233
+ line = [pointer, ' ', idx, ' ', ch_col, ' ', snippet, ' ', note_tr,
234
+ ' ', book_col, ' ', date_col].join
235
+ else
236
+ date_col = pad_right(truncate_text(created, date_w), date_w)
237
+ line = [pointer, ' ', idx, ' ', ch_col, ' ', snippet, ' ', note_tr,
238
+ ' ', date_col].join
239
+ end
240
+
241
+ color = is_selected ? SELECTION_HIGHLIGHT : COLOR_TEXT_PRIMARY
242
+ surface.write(bounds, row, 1, color + line + Terminal::ANSI::RESET)
243
+ end
244
+
245
+ # truncate_text provided by UI::TextUtils
246
+
247
+ def normalize_list(raw)
248
+ (raw || []).map do |a|
249
+ {
250
+ text: a['text'],
251
+ note: a['note'],
252
+ id: a['id'],
253
+ range: a['range'],
254
+ chapter_index: a['chapter_index'],
255
+ created_at: a['created_at'],
256
+ updated_at: a['updated_at'],
257
+ page_current: a['page_current'],
258
+ page_total: a['page_total'],
259
+ page_mode: a['page_mode'],
260
+ }
261
+ end
262
+ end
263
+ end
264
+ end
265
+ end
266
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../base_component'
4
+ require_relative '../render_style'
5
+ require_relative '../../../terminal/text_metrics.rb'
6
+
7
+ module Shoko
8
+ module Adapters::Output::Ui::Components
9
+ module Screens
10
+ # Base component for all screen renderers
11
+ class BaseScreenComponent < BaseComponent
12
+ def initialize(services = nil)
13
+ super
14
+ end
15
+
16
+ # Screens typically take the full available height
17
+ def preferred_height(available_height)
18
+ available_height
19
+ end
20
+
21
+ protected
22
+
23
+ def write_header(surface, bounds, title, help_text = nil)
24
+ w = bounds.width
25
+ surface.write(bounds, 1, 2, title)
26
+ return unless help_text
27
+
28
+ help_width = Shoko::Adapters::Output::Terminal::TextMetrics.visible_length(help_text)
29
+ surface.write(bounds, 1, [w - help_width - 2, w / 2].max, help_text)
30
+ end
31
+
32
+ def write_footer(surface, bounds, text)
33
+ surface.write(bounds, bounds.height - 1, 2, text)
34
+ end
35
+
36
+ def write_empty_message(surface, bounds, message)
37
+ col = [(bounds.width - Shoko::Adapters::Output::Terminal::TextMetrics.visible_length(message)) / 2, 1].max
38
+ row = bounds.height / 2
39
+ surface.write(bounds, row, col, message)
40
+ end
41
+
42
+ def write_selection_pointer(surface, bounds, row, selected: true)
43
+ text = selected ? Shoko::Adapters::Output::Ui::Components::RenderStyle.selection_pointer_colored : ' '
44
+ surface.write(bounds, row, 2, text)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end