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,156 @@
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 single-view reading mode (supports both dynamic and absolute page numbering)
9
+ class SingleViewRenderer < BaseViewRenderer
10
+ # Layout metrics for single-view rendering.
11
+ SingleLayout = Struct.new(
12
+ :col_width,
13
+ :content_height,
14
+ :spacing,
15
+ :displayable,
16
+ :col_start,
17
+ keyword_init: true
18
+ )
19
+
20
+ # Encapsulates per-render state shared across helpers.
21
+ RenderFrame = Struct.new(:surface, :bounds, :context, :layout, keyword_init: true)
22
+
23
+ private_constant :SingleLayout, :RenderFrame
24
+
25
+ def initialize(dependencies, page_numbering_mode: :dynamic)
26
+ super(dependencies)
27
+ @page_numbering_mode = page_numbering_mode
28
+ end
29
+
30
+ def render_with_context(surface, bounds, context)
31
+ mode = context.page_numbering_mode || @page_numbering_mode
32
+ if mode == :dynamic
33
+ render_dynamic_mode_with_context(surface, bounds, context)
34
+ else
35
+ render_absolute_mode_with_context(surface, bounds, context)
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ # Context-based rendering methods
42
+ def render_dynamic_mode_with_context(surface, bounds, context)
43
+ layout = single_layout(bounds, context.config)
44
+ frame = RenderFrame.new(surface: surface, bounds: bounds, context: context, layout: layout)
45
+
46
+ page_data = context.page_calculator&.get_page(context.current_page_index)
47
+ lines, line_offset = page_data ? dynamic_window(frame, page_data) : dynamic_fallback_window(frame)
48
+ render_single_column(frame, lines, line_offset)
49
+ end
50
+
51
+ def render_absolute_mode_with_context(surface, bounds, context)
52
+ chapter = context.current_chapter
53
+ return unless chapter
54
+
55
+ state_store = context&.state
56
+ return unless state_store
57
+
58
+ layout = single_layout(bounds, context.config)
59
+ frame = RenderFrame.new(surface: surface, bounds: bounds, context: context, layout: layout)
60
+
61
+ offset = state_store.get(%i[reader single_page]) || 0
62
+ chapter_index = state_store.get(%i[reader current_chapter]) || 0
63
+ lines, line_offset = fetch_window(
64
+ frame,
65
+ chapter_index: chapter_index,
66
+ offset: offset,
67
+ length: layout.displayable
68
+ )
69
+ render_single_column(frame, lines, line_offset)
70
+ end
71
+
72
+ def single_layout(bounds, config)
73
+ col_width, content_height, spacing, displayable = compute_layout(bounds, :single, config)
74
+ SingleLayout.new(
75
+ col_width: col_width,
76
+ content_height: content_height,
77
+ spacing: spacing,
78
+ displayable: displayable,
79
+ col_start: center_start_col(bounds.width, col_width)
80
+ )
81
+ end
82
+
83
+ def dynamic_window(frame, page_data)
84
+ context = frame.context
85
+ layout = frame.layout
86
+
87
+ line_offset = page_data[:start_line].to_i
88
+ end_line = page_data[:end_line].to_i
89
+ span_length = [end_line - line_offset + 1, layout.displayable].max
90
+ state = context.state
91
+ chapter_index = (page_data[:chapter_index] || state&.get(%i[reader current_chapter]) || 0).to_i
92
+ request = { chapter_index: chapter_index, offset: line_offset, length: span_length }
93
+
94
+ lines = page_data[:lines] || []
95
+ resolve_dynamic_lines(frame, lines, request)
96
+ end
97
+
98
+ def resolve_dynamic_lines(frame, lines, request)
99
+ return fetch_window(frame, request) if lines.empty?
100
+
101
+ line_offset = request.fetch(:offset)
102
+ snapped = snap_offset_to_image_start(lines, line_offset)
103
+ return [lines, line_offset] if snapped == line_offset
104
+
105
+ fetch_window(frame, request.merge(offset: snapped))
106
+ end
107
+
108
+ def dynamic_fallback_window(frame)
109
+ context = frame.context
110
+ layout = frame.layout
111
+
112
+ displayable = layout.displayable
113
+ chapter_index = context&.state&.get(%i[reader current_chapter]) || 0
114
+ offset = compute_dynamic_offset(context, displayable)
115
+ fetch_window(frame, chapter_index: chapter_index, offset: offset, length: displayable)
116
+ end
117
+
118
+ def fetch_window(frame, request)
119
+ context = frame.context
120
+ layout = frame.layout
121
+
122
+ fetch_wrapped_lines_with_offset(
123
+ document: context.document,
124
+ chapter_index: request.fetch(:chapter_index),
125
+ col_width: layout.col_width,
126
+ offset: request.fetch(:offset),
127
+ length: request.fetch(:length)
128
+ )
129
+ end
130
+
131
+ def compute_dynamic_offset(context, displayable)
132
+ pending = context.state.get(%i[reader pending_progress]) if context&.state
133
+ line_offset = pending && (pending[:line_offset] || pending['line_offset'])
134
+ return line_offset.to_i if line_offset
135
+
136
+ (context.current_page_index || 0) * [displayable, 1].max
137
+ end
138
+
139
+ def render_single_column(frame, lines, line_offset)
140
+ layout = frame.layout
141
+ context = frame.context
142
+
143
+ start_row = calculate_center_start_row(layout.content_height, lines.size, layout.spacing)
144
+ params = Adapters::Output::Rendering::Models::RenderParams.new(start_row: start_row, col_start: layout.col_start,
145
+ col_width: layout.col_width, context: context,
146
+ line_offset: line_offset,
147
+ page_id: context.current_page_index,
148
+ column_id: 0)
149
+ draw_lines(frame.surface, frame.bounds, lines, params)
150
+ end
151
+
152
+ # helpers provided by BaseViewRenderer
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,221 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_view_renderer'
4
+ require_relative '../render_style'
5
+
6
+ module Shoko
7
+ module Adapters::Output::Ui::Components
8
+ module Reading
9
+ # Renderer for split-view (two-column) reading mode
10
+ # Supports both dynamic and absolute page numbering modes
11
+ class SplitViewRenderer < BaseViewRenderer
12
+ LEFT_MARGIN = Shoko::Core::Services::LayoutService::SPLIT_LEFT_MARGIN
13
+ RIGHT_MARGIN = Shoko::Core::Services::LayoutService::SPLIT_RIGHT_MARGIN
14
+ COLUMN_GAP = Shoko::Core::Services::LayoutService::SPLIT_COLUMN_GAP
15
+ COLUMN_START_ROW = 3
16
+
17
+ # Layout metrics for split-view rendering.
18
+ SplitLayout = Struct.new(
19
+ :col_width,
20
+ :content_height,
21
+ :spacing,
22
+ :displayable,
23
+ :left_start,
24
+ :right_start,
25
+ :divider_col,
26
+ keyword_init: true
27
+ )
28
+
29
+ # Encapsulates per-render state shared across helpers.
30
+ RenderFrame = Struct.new(:surface, :bounds, :context, :layout, keyword_init: true)
31
+
32
+ private_constant :SplitLayout, :RenderFrame
33
+
34
+ def render_with_context(surface, bounds, context)
35
+ mode = context.page_numbering_mode || :dynamic
36
+ if mode == :dynamic
37
+ render_dynamic_mode_with_context(surface, bounds, context)
38
+ else
39
+ render_absolute_mode_with_context(surface, bounds, context)
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ # Divider provided by BaseViewRenderer#draw_divider
46
+
47
+ def render_column_lines(frame, lines, params)
48
+ draw_lines(frame.surface, frame.bounds, lines, params)
49
+ end
50
+
51
+ # Context-based rendering methods
52
+ def render_dynamic_mode_with_context(surface, bounds, context)
53
+ layout = split_layout(bounds, context.config)
54
+ frame = RenderFrame.new(surface: surface, bounds: bounds, context: context, layout: layout)
55
+ render_chapter_header(frame)
56
+
57
+ left_pd = context.page_calculator&.get_page(context.current_page_index)
58
+ if left_pd
59
+ render_dynamic_from_page_data(frame, left_pd)
60
+ else
61
+ render_dynamic_fallback(frame)
62
+ end
63
+ end
64
+
65
+ def render_absolute_mode_with_context(surface, bounds, context)
66
+ chapter = context.current_chapter
67
+ return unless chapter
68
+
69
+ st = context&.state
70
+ return unless st
71
+
72
+ layout = split_layout(bounds, context.config)
73
+ frame = RenderFrame.new(surface: surface, bounds: bounds, context: context, layout: layout)
74
+ render_chapter_header(frame)
75
+
76
+ display_height = layout.displayable
77
+ left_offset = st.get(%i[reader left_page]) || 0
78
+ right_offset = st.get(%i[reader right_page]) || display_height
79
+ render_absolute_columns(frame, left_offset, right_offset)
80
+ end
81
+
82
+ def render_chapter_header(frame)
83
+ chapter = frame.context.current_chapter
84
+ return unless chapter
85
+
86
+ header_col = LEFT_MARGIN + 1
87
+ frame.surface.write(frame.bounds, 1, header_col, chapter_header_line(frame, chapter, header_col))
88
+ end
89
+
90
+ def chapter_header_line(frame, chapter, header_col)
91
+ bounds = frame.bounds
92
+ idx = frame.context.state.get(%i[reader current_chapter]) + 1
93
+ info = "[#{idx}] #{chapter.title || 'Unknown'}"
94
+ available = bounds.width - LEFT_MARGIN - RIGHT_MARGIN
95
+ start_column = bounds.x + header_col - 2
96
+ clipped = Shoko::Adapters::Output::Terminal::TextMetrics.truncate_to(info, available, start_column: start_column)
97
+ heading_color = Shoko::Adapters::Output::Ui::Components::RenderStyle.color(:heading)
98
+ heading_color + clipped + Terminal::ANSI::RESET
99
+ end
100
+
101
+ def render_dynamic_from_page_data(frame, left_page_data)
102
+ layout = frame.layout
103
+ context = frame.context
104
+ page_id = context.current_page_index
105
+ right_page_id = page_id ? page_id + 1 : nil
106
+
107
+ render_page_data_column(frame, left_page_data,
108
+ { start_col: layout.left_start, column_id: 0, page_id: page_id })
109
+ draw_divider(frame.surface, frame.bounds, layout.divider_col)
110
+
111
+ right_page_data = page_id ? context.page_calculator&.get_page(page_id + 1) : nil
112
+ return unless right_page_data
113
+
114
+ render_page_data_column(frame, right_page_data,
115
+ { start_col: layout.right_start, column_id: 1, page_id: right_page_id })
116
+ end
117
+
118
+ def render_dynamic_fallback(frame)
119
+ layout = frame.layout
120
+ page_id = frame.context.current_page_index
121
+ right_page_id = page_id ? page_id + 1 : nil
122
+
123
+ displayable = layout.displayable
124
+ base_offset = (page_id || 0) * [displayable, 1].max
125
+
126
+ render_offset_column(frame, base_offset,
127
+ { start_col: layout.left_start, column_id: 0, page_id: page_id })
128
+ draw_divider(frame.surface, frame.bounds, layout.divider_col)
129
+
130
+ render_offset_column(frame, base_offset + displayable,
131
+ { start_col: layout.right_start, column_id: 1, page_id: right_page_id })
132
+ end
133
+
134
+ def render_offset_column(frame, offset, column_spec)
135
+ displayable = frame.layout.displayable
136
+ lines, line_offset = fetch_wrapped_lines_window(frame, offset, displayable)
137
+ render_column_lines(frame, lines, column_params(frame, column_spec, line_offset))
138
+ line_offset
139
+ end
140
+
141
+ def render_absolute_columns(frame, left_offset, right_offset)
142
+ layout = frame.layout
143
+ page_id = frame.context.current_page_index
144
+
145
+ left_render_offset = render_offset_column(
146
+ frame,
147
+ left_offset,
148
+ { start_col: layout.left_start, column_id: 0, page_id: page_id }
149
+ )
150
+ draw_divider(frame.surface, frame.bounds, layout.divider_col)
151
+
152
+ display_height = layout.displayable
153
+ paired = right_offset.to_i == left_offset.to_i + display_height
154
+ right_input = paired ? left_render_offset + display_height : right_offset
155
+ render_offset_column(frame, right_input,
156
+ { start_col: layout.right_start, column_id: 1, page_id: page_id })
157
+ end
158
+
159
+ def fetch_wrapped_lines_window(frame, offset, length)
160
+ st = frame.context.state
161
+ chapter_index = st.get(%i[reader current_chapter]) || 0
162
+ fetch_wrapped_lines_with_offset(
163
+ document: frame.context.document,
164
+ chapter_index: chapter_index,
165
+ col_width: frame.layout.col_width,
166
+ offset: offset,
167
+ length: length
168
+ )
169
+ end
170
+
171
+ def column_lines_from_page_data(frame, page_data)
172
+ line_offset = page_data[:start_line].to_i
173
+ lines = page_data[:lines]
174
+ end_line = page_data[:end_line].to_i
175
+ span_length = [end_line - line_offset + 1, 1].max
176
+
177
+ return fetch_wrapped_lines_window(frame, line_offset, span_length) if lines.nil? || lines.empty?
178
+
179
+ snapped = snap_offset_to_image_start(lines, line_offset)
180
+ return [lines, line_offset] if snapped == line_offset
181
+
182
+ fetch_wrapped_lines_window(frame, snapped, span_length)
183
+ end
184
+
185
+ def render_page_data_column(frame, page_data, column_spec)
186
+ lines, line_offset = column_lines_from_page_data(frame, page_data)
187
+ render_column_lines(frame, lines, column_params(frame, column_spec, line_offset))
188
+ end
189
+
190
+ def column_params(frame, column_spec, line_offset)
191
+ Adapters::Output::Rendering::Models::RenderParams.new(
192
+ start_row: COLUMN_START_ROW,
193
+ col_start: column_spec.fetch(:start_col),
194
+ col_width: frame.layout.col_width,
195
+ context: frame.context,
196
+ column_id: column_spec.fetch(:column_id),
197
+ line_offset: line_offset,
198
+ page_id: column_spec[:page_id]
199
+ )
200
+ end
201
+
202
+ def split_layout(bounds, config)
203
+ col_width, content_height, spacing, displayable = compute_layout(bounds, :split, config)
204
+ left_start = LEFT_MARGIN + 1
205
+ right_start = left_start + col_width + COLUMN_GAP
206
+ divider_col = left_start + col_width + 1
207
+
208
+ SplitLayout.new(
209
+ col_width: col_width,
210
+ content_height: content_height,
211
+ spacing: spacing,
212
+ displayable: displayable,
213
+ left_start: left_start,
214
+ right_start: right_start,
215
+ divider_col: divider_col
216
+ )
217
+ end
218
+ end
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Adapters::Output::Ui::Components
5
+ module Reading
6
+ # Factory for creating appropriate view renderers based on configuration
7
+ class ViewRendererFactory
8
+ def self.create(state, dependencies)
9
+ case Application::Selectors::ConfigSelectors.view_mode(state)
10
+ when :split
11
+ SplitViewRenderer.new(dependencies)
12
+ else
13
+ mode = Application::Selectors::ConfigSelectors.page_numbering_mode(state)
14
+ SingleViewRenderer.new(dependencies, page_numbering_mode: mode)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Adapters::Output::Ui::Components
5
+ module Reading
6
+ # Fetches wrapped lines from the document using the formatting and wrapping services.
7
+ #
8
+ # Also applies the "image block offset snap" behavior so paging doesn't land in the
9
+ # middle of a Kitty image placeholder block (which can cause disappearing images).
10
+ class WrappedLinesFetcher
11
+ def initialize(dependencies)
12
+ @dependencies = dependencies
13
+ end
14
+
15
+ def fetch(document:, chapter_index:, col_width:, offset:, length:)
16
+ chapter = document&.get_chapter(chapter_index)
17
+ return [] unless chapter
18
+
19
+ lines = fetch_via_formatting_service(document: document, chapter_index: chapter_index, col_width: col_width,
20
+ offset: offset, length: length)
21
+ return lines unless lines.empty?
22
+
23
+ lines = fetch_via_wrapping_service(chapter: chapter, chapter_index: chapter_index, col_width: col_width,
24
+ offset: offset, length: length)
25
+ return lines unless lines.empty?
26
+
27
+ fallback_lines(chapter, offset, length)
28
+ end
29
+
30
+ def fetch_with_offset(document:, chapter_index:, col_width:, offset:, length:)
31
+ offset_i = offset.to_i
32
+ lines = fetch(document: document, chapter_index: chapter_index, col_width: col_width, offset: offset_i,
33
+ length: length)
34
+ snapped = snap_offset_to_image_start(lines, offset_i)
35
+ return [lines, offset_i] if snapped == offset_i
36
+
37
+ [fetch(document: document, chapter_index: chapter_index, col_width: col_width, offset: snapped,
38
+ length: length), snapped]
39
+ end
40
+
41
+ def snap_offset_to_image_start(lines, offset)
42
+ offset_i = offset.to_i
43
+ return offset_i if offset_i <= 0
44
+
45
+ meta = image_block_metadata(lines)
46
+ return offset_i unless meta
47
+
48
+ idx = image_line_index(meta)
49
+ return offset_i unless idx
50
+
51
+ snapped_offset(offset_i, idx)
52
+ rescue StandardError
53
+ offset.to_i
54
+ end
55
+
56
+ private
57
+
58
+ def fetch_via_formatting_service(document:, chapter_index:, col_width:, offset:, length:)
59
+ return [] unless @dependencies&.registered?(:formatting_service)
60
+
61
+ config = @dependencies.registered?(:global_state) ? @dependencies.resolve(:global_state) : nil
62
+
63
+ Array(
64
+ @dependencies.resolve(:formatting_service).wrap_window(
65
+ document,
66
+ chapter_index,
67
+ col_width,
68
+ offset: offset,
69
+ length: length,
70
+ config: config,
71
+ lines_per_page: length
72
+ )
73
+ )
74
+ rescue StandardError
75
+ []
76
+ end
77
+
78
+ def fetch_via_wrapping_service(chapter:, chapter_index:, col_width:, offset:, length:)
79
+ return [] unless @dependencies&.registered?(:wrapping_service)
80
+
81
+ wrapping = @dependencies.resolve(:wrapping_service)
82
+ wrapping.wrap_window(chapter.lines || [], chapter_index, col_width, offset, length) || []
83
+ rescue StandardError
84
+ []
85
+ end
86
+
87
+ def fallback_lines(chapter, offset, length)
88
+ (chapter.lines || [])[offset, length] || []
89
+ rescue StandardError
90
+ []
91
+ end
92
+
93
+ def first_line_metadata(lines)
94
+ first = Array(lines).first
95
+ return nil unless first.respond_to?(:metadata)
96
+
97
+ meta = first.metadata
98
+ meta.is_a?(Hash) ? meta : nil
99
+ end
100
+
101
+ def image_render_hash?(meta)
102
+ return false unless meta.is_a?(Hash)
103
+
104
+ render = meta[:image_render] || meta['image_render']
105
+ render.is_a?(Hash)
106
+ end
107
+
108
+ def image_block_metadata(lines)
109
+ meta = first_line_metadata(lines)
110
+ return nil unless image_render_hash?(meta)
111
+ return nil if image_render_line?(meta)
112
+
113
+ meta
114
+ end
115
+
116
+ def image_render_line?(meta)
117
+ value = meta.key?(:image_render_line) ? meta[:image_render_line] : meta['image_render_line']
118
+ value == true
119
+ rescue StandardError
120
+ false
121
+ end
122
+
123
+ def image_line_index(meta)
124
+ value = meta.key?(:image_line_index) ? meta[:image_line_index] : meta['image_line_index']
125
+ value&.to_i
126
+ rescue StandardError
127
+ nil
128
+ end
129
+
130
+ def snapped_offset(offset, index)
131
+ snapped = offset.to_i - index.to_i
132
+ snapped.negative? ? 0 : snapped
133
+ rescue StandardError
134
+ offset.to_i
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Adapters::Output::Ui::Components
5
+ Rect = Struct.new(:x, :y, :width, :height, keyword_init: true) do
6
+ def bottom
7
+ y + height - 1
8
+ end
9
+
10
+ def right
11
+ x + width - 1
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Adapters::Output::Ui::Components
5
+ # Small helper for composing styled strings and common UI elements.
6
+ module RenderStyle
7
+ DEFAULT_PALETTE = Shoko::Adapters::Output::Ui::Constants::Themes::DEFAULT_PALETTE
8
+
9
+ @palette = DEFAULT_PALETTE.dup
10
+
11
+ class << self
12
+ def configure(palette)
13
+ @palette = DEFAULT_PALETTE.merge(palette || {})
14
+ end
15
+
16
+ def palette
17
+ @palette || DEFAULT_PALETTE
18
+ end
19
+
20
+ def color(key)
21
+ palette[key] || DEFAULT_PALETTE[key]
22
+ end
23
+
24
+ def primary(text)
25
+ color(:primary) + text.to_s + Terminal::ANSI::RESET
26
+ end
27
+
28
+ def accent(text)
29
+ color(:accent) + text.to_s + Terminal::ANSI::RESET
30
+ end
31
+
32
+ def dim(text)
33
+ color(:dim) + text.to_s + Terminal::ANSI::RESET
34
+ end
35
+
36
+ def selection_pointer
37
+ Shoko::Adapters::Output::Ui::Constants::UI::SELECTION_POINTER
38
+ end
39
+
40
+ def selection_pointer_colored
41
+ color(:accent) + selection_pointer + Terminal::ANSI::RESET
42
+ end
43
+
44
+ def styled_segment(text, styles = {}, metadata: {})
45
+ content = text.to_s
46
+ return content if content.empty?
47
+
48
+ codes = []
49
+ block_type = metadata && metadata[:block_type]
50
+ highlight_allowed = metadata.key?(:highlight_enabled) ? metadata[:highlight_enabled] : true
51
+
52
+ color_code = color_for(styles, block_type, highlight_allowed)
53
+ codes << color_code if color_code
54
+
55
+ codes << Terminal::ANSI::BOLD if styles[:bold] || block_type == :heading
56
+ codes << Terminal::ANSI::ITALIC if styles[:italic] || styles[:quote] || block_type == :quote
57
+ codes << Terminal::ANSI::DIM if styles[:prefix] || styles[:dim]
58
+
59
+ codes.join + content + Terminal::ANSI::RESET
60
+ end
61
+
62
+ private
63
+
64
+ def color_for(styles, block_type, highlight_allowed)
65
+ if styles[:code] || block_type == :code
66
+ color(:code)
67
+ elsif styles[:accent] || styles[:highlight] || styles[:keyword]
68
+ color(:accent)
69
+ elsif block_type == :heading
70
+ highlight_allowed ? color(:heading) : color(:primary)
71
+ elsif block_type == :quote || styles[:quote]
72
+ highlight_allowed ? color(:quote) : color(:primary)
73
+ elsif block_type == :separator
74
+ color(:separator)
75
+ elsif styles[:prefix]
76
+ color(:prefix)
77
+ else
78
+ color(:primary)
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_component'
4
+
5
+ module Shoko
6
+ module Adapters::Output::Ui::Components
7
+ # Universal screen component that can render any screen type
8
+ # Provides a unified interface for screen rendering
9
+ class ScreenComponent < BaseComponent
10
+ def initialize(screen_manager)
11
+ super()
12
+ @screen_manager = screen_manager
13
+ end
14
+
15
+ def render(surface, bounds)
16
+ @screen_manager.render_current_screen(surface, bounds)
17
+ end
18
+
19
+ def preferred_height(_available_height)
20
+ :fill
21
+ end
22
+ end
23
+ end
24
+ end