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.
- checksums.yaml +7 -0
- data/.bundle/config +4 -0
- data/.bundle/config.bak +3 -0
- data/.rspec_status +42 -0
- data/.rubocop.yml +124 -0
- data/Gemfile +19 -0
- data/LICENSE +21 -0
- data/README.md +82 -0
- data/Rakefile +29 -0
- data/bin/start +15 -0
- data/lib/shoko/adapters/book_sources/document_service.rb +201 -0
- data/lib/shoko/adapters/book_sources/download_service.rb +95 -0
- data/lib/shoko/adapters/book_sources/epub/epub_resource_loader.rb +137 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/html_processor.rb +151 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/metadata_extractor.rb +53 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/entry_reader.rb +77 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/metadata_extractor.rb +67 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_context.rb +86 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_document_index.rb +75 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_document_scanner.rb +47 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_extractor.rb +46 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_label_resolver.rb +83 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_list_item.rb +55 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_result.rb +8 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_selector.rb +100 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_source_locator.rb +93 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_traversal.rb +103 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_walker.rb +56 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf_processor.rb +102 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/xhtml_content_parser.rb +661 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/xml_text_normalizer.rb +41 -0
- data/lib/shoko/adapters/book_sources/epub_document.rb +253 -0
- data/lib/shoko/adapters/book_sources/epub_finder/directory_scanner.rb +134 -0
- data/lib/shoko/adapters/book_sources/epub_finder/scanner_context.rb +28 -0
- data/lib/shoko/adapters/book_sources/epub_finder.rb +161 -0
- data/lib/shoko/adapters/book_sources/epub_importer.rb +268 -0
- data/lib/shoko/adapters/book_sources/gutendex_client.rb +150 -0
- data/lib/shoko/adapters/book_sources/library_scanner.rb +93 -0
- data/lib/shoko/adapters/book_sources/source_fingerprint.rb +57 -0
- data/lib/shoko/adapters/input/annotations/mouse_handler.rb +84 -0
- data/lib/shoko/adapters/input/command_bridge.rb +148 -0
- data/lib/shoko/adapters/input/command_factory.rb +255 -0
- data/lib/shoko/adapters/input/commands.rb +60 -0
- data/lib/shoko/adapters/input/dispatcher.rb +69 -0
- data/lib/shoko/adapters/input/input_controller.rb +250 -0
- data/lib/shoko/adapters/input/key_definitions.rb +108 -0
- data/lib/shoko/adapters/input/validators/file_path_validator.rb +81 -0
- data/lib/shoko/adapters/input/validators/terminal_size_validator.rb +76 -0
- data/lib/shoko/adapters/monitoring/logger.rb +150 -0
- data/lib/shoko/adapters/monitoring/perf_tracer.rb +183 -0
- data/lib/shoko/adapters/monitoring/performance_monitor.rb +110 -0
- data/lib/shoko/adapters/output/clipboard/clipboard_service.rb +125 -0
- data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler/image_builder.rb +149 -0
- data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler/text_wrapper.rb +149 -0
- data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler/tokenizer.rb +91 -0
- data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler.rb +144 -0
- data/lib/shoko/adapters/output/formatting/formatting_service/plain_lines_builder.rb +54 -0
- data/lib/shoko/adapters/output/formatting/formatting_service.rb +247 -0
- data/lib/shoko/adapters/output/formatting/wrapping_service.rb +228 -0
- data/lib/shoko/adapters/output/instrumentation_service.rb +52 -0
- data/lib/shoko/adapters/output/kitty/image_transcoder.rb +71 -0
- data/lib/shoko/adapters/output/kitty/kitty_graphics.rb +114 -0
- data/lib/shoko/adapters/output/kitty/kitty_image_renderer.rb +239 -0
- data/lib/shoko/adapters/output/kitty/kitty_unicode_placeholders.rb +139 -0
- data/lib/shoko/adapters/output/kitty/kitty_unicode_placeholders_diacritic_codepoints.txt +26 -0
- data/lib/shoko/adapters/output/notification_service.rb +58 -0
- data/lib/shoko/adapters/output/render_registry.rb +45 -0
- data/lib/shoko/adapters/output/rendering/models/line_geometry.rb +60 -0
- data/lib/shoko/adapters/output/rendering/models/page_rendering_context.rb +22 -0
- data/lib/shoko/adapters/output/rendering/models/render_params.rb +28 -0
- data/lib/shoko/adapters/output/rendering/models/rendering_context.rb +58 -0
- data/lib/shoko/adapters/output/terminal/buffer.rb +275 -0
- data/lib/shoko/adapters/output/terminal/constants/terminal_defaults.rb +11 -0
- data/lib/shoko/adapters/output/terminal/input/decoder.rb +347 -0
- data/lib/shoko/adapters/output/terminal/input.rb +161 -0
- data/lib/shoko/adapters/output/terminal/output.rb +105 -0
- data/lib/shoko/adapters/output/terminal/terminal.rb +167 -0
- data/lib/shoko/adapters/output/terminal/terminal_sanitizer.rb +243 -0
- data/lib/shoko/adapters/output/terminal/terminal_service.rb +138 -0
- data/lib/shoko/adapters/output/terminal/text_metrics.rb +273 -0
- data/lib/shoko/adapters/output/ui/builders/page_setup_builder.rb +47 -0
- data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay/footer_renderer.rb +80 -0
- data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay/geometry.rb +61 -0
- data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay/note_renderer.rb +86 -0
- data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay_component.rb +234 -0
- data/lib/shoko/adapters/output/ui/components/annotations_overlay/list_renderer.rb +142 -0
- data/lib/shoko/adapters/output/ui/components/annotations_overlay_component.rb +185 -0
- data/lib/shoko/adapters/output/ui/components/base_component.rb +110 -0
- data/lib/shoko/adapters/output/ui/components/component_interface.rb +80 -0
- data/lib/shoko/adapters/output/ui/components/content_component.rb +61 -0
- data/lib/shoko/adapters/output/ui/components/enhanced_popup_menu.rb +191 -0
- data/lib/shoko/adapters/output/ui/components/footer_component.rb +120 -0
- data/lib/shoko/adapters/output/ui/components/header_component.rb +46 -0
- data/lib/shoko/adapters/output/ui/components/layouts/horizontal.rb +63 -0
- data/lib/shoko/adapters/output/ui/components/layouts/vertical.rb +73 -0
- data/lib/shoko/adapters/output/ui/components/main_menu_component.rb +103 -0
- data/lib/shoko/adapters/output/ui/components/reading/base_view_renderer.rb +199 -0
- data/lib/shoko/adapters/output/ui/components/reading/config_helpers.rb +42 -0
- data/lib/shoko/adapters/output/ui/components/reading/help_renderer.rb +62 -0
- data/lib/shoko/adapters/output/ui/components/reading/inline_segment_highlighter.rb +144 -0
- data/lib/shoko/adapters/output/ui/components/reading/kitty_image_line_renderer.rb +262 -0
- data/lib/shoko/adapters/output/ui/components/reading/line_content_composer.rb +114 -0
- data/lib/shoko/adapters/output/ui/components/reading/line_drawer.rb +87 -0
- data/lib/shoko/adapters/output/ui/components/reading/line_geometry_builder.rb +41 -0
- data/lib/shoko/adapters/output/ui/components/reading/rendered_lines_recorder.rb +64 -0
- data/lib/shoko/adapters/output/ui/components/reading/single_view_renderer.rb +156 -0
- data/lib/shoko/adapters/output/ui/components/reading/split_view_renderer.rb +221 -0
- data/lib/shoko/adapters/output/ui/components/reading/view_renderer_factory.rb +20 -0
- data/lib/shoko/adapters/output/ui/components/reading/wrapped_lines_fetcher.rb +139 -0
- data/lib/shoko/adapters/output/ui/components/rect.rb +15 -0
- data/lib/shoko/adapters/output/ui/components/render_style.rb +84 -0
- data/lib/shoko/adapters/output/ui/components/screen_component.rb +24 -0
- data/lib/shoko/adapters/output/ui/components/screens/annotation_detail_screen_component.rb +175 -0
- data/lib/shoko/adapters/output/ui/components/screens/annotation_edit_screen_component.rb +221 -0
- data/lib/shoko/adapters/output/ui/components/screens/annotation_editor_screen_component.rb +205 -0
- data/lib/shoko/adapters/output/ui/components/screens/annotation_rendering_helpers.rb +190 -0
- data/lib/shoko/adapters/output/ui/components/screens/annotations_screen_component.rb +266 -0
- data/lib/shoko/adapters/output/ui/components/screens/base_screen_component.rb +49 -0
- data/lib/shoko/adapters/output/ui/components/screens/browse_screen_component.rb +319 -0
- data/lib/shoko/adapters/output/ui/components/screens/download_books_screen_component.rb +340 -0
- data/lib/shoko/adapters/output/ui/components/screens/library_screen_component.rb +205 -0
- data/lib/shoko/adapters/output/ui/components/screens/loading_overlay_component.rb +49 -0
- data/lib/shoko/adapters/output/ui/components/screens/menu_screen_component.rb +107 -0
- data/lib/shoko/adapters/output/ui/components/screens/settings_screen_component.rb +238 -0
- data/lib/shoko/adapters/output/ui/components/sidebar/annotations_tab_renderer.rb +159 -0
- data/lib/shoko/adapters/output/ui/components/sidebar/bookmarks_tab_renderer.rb +139 -0
- data/lib/shoko/adapters/output/ui/components/sidebar/tab_header_component.rb +157 -0
- data/lib/shoko/adapters/output/ui/components/sidebar/toc_tab_renderer.rb +111 -0
- data/lib/shoko/adapters/output/ui/components/sidebar/toc_tab_support.rb +1606 -0
- data/lib/shoko/adapters/output/ui/components/sidebar_panel_component.rb +217 -0
- data/lib/shoko/adapters/output/ui/components/surface.rb +88 -0
- data/lib/shoko/adapters/output/ui/components/tooltip_overlay_component.rb +224 -0
- data/lib/shoko/adapters/output/ui/components/ui/box_drawer.rb +32 -0
- data/lib/shoko/adapters/output/ui/components/ui/list_helpers.rb +33 -0
- data/lib/shoko/adapters/output/ui/components/ui/overlay_layout.rb +79 -0
- data/lib/shoko/adapters/output/ui/components/ui/text_utils.rb +46 -0
- data/lib/shoko/adapters/output/ui/constants/highlighting.rb +21 -0
- data/lib/shoko/adapters/output/ui/constants/messages.rb +12 -0
- data/lib/shoko/adapters/output/ui/constants/themes.rb +79 -0
- data/lib/shoko/adapters/output/ui/constants/ui_constants.rb +85 -0
- data/lib/shoko/adapters/output/ui/rendering/frame_coordinator.rb +42 -0
- data/lib/shoko/adapters/output/ui/rendering/reader_render_coordinator.rb +169 -0
- data/lib/shoko/adapters/output/ui/rendering/render_pipeline.rb +55 -0
- data/lib/shoko/adapters/storage/atomic_file_writer.rb +43 -0
- data/lib/shoko/adapters/storage/background_worker.rb +66 -0
- data/lib/shoko/adapters/storage/book_cache_pipeline.rb +653 -0
- data/lib/shoko/adapters/storage/cache/epub/memory_cache.rb +99 -0
- data/lib/shoko/adapters/storage/cache/epub/persistence.rb +131 -0
- data/lib/shoko/adapters/storage/cache/epub/serializer/deserialize.rb +225 -0
- data/lib/shoko/adapters/storage/cache/epub/serializer/helpers.rb +63 -0
- data/lib/shoko/adapters/storage/cache/epub/serializer/serialize.rb +83 -0
- data/lib/shoko/adapters/storage/cache/epub/serializer.rb +5 -0
- data/lib/shoko/adapters/storage/cache/epub/source_reference.rb +58 -0
- data/lib/shoko/adapters/storage/cache_paths.rb +21 -0
- data/lib/shoko/adapters/storage/cache_pointer_manager.rb +60 -0
- data/lib/shoko/adapters/storage/config_paths.rb +30 -0
- data/lib/shoko/adapters/storage/epub_cache.rb +195 -0
- data/lib/shoko/adapters/storage/file_writer_service.rb +47 -0
- data/lib/shoko/adapters/storage/json_cache_store/chapters.rb +141 -0
- data/lib/shoko/adapters/storage/json_cache_store/layouts.rb +67 -0
- data/lib/shoko/adapters/storage/json_cache_store/manifest.rb +42 -0
- data/lib/shoko/adapters/storage/json_cache_store/payload_helpers.rb +113 -0
- data/lib/shoko/adapters/storage/json_cache_store/resources.rb +84 -0
- data/lib/shoko/adapters/storage/json_cache_store.rb +167 -0
- data/lib/shoko/adapters/storage/lazy_file_string.rb +65 -0
- data/lib/shoko/adapters/storage/pagination_cache.rb +127 -0
- data/lib/shoko/adapters/storage/recent_files.rb +78 -0
- data/lib/shoko/adapters/storage/repositories/annotation_repository.rb +182 -0
- data/lib/shoko/adapters/storage/repositories/base_repository.rb +81 -0
- data/lib/shoko/adapters/storage/repositories/bookmark_repository.rb +132 -0
- data/lib/shoko/adapters/storage/repositories/cached_library_repository.rb +129 -0
- data/lib/shoko/adapters/storage/repositories/config_repository.rb +262 -0
- data/lib/shoko/adapters/storage/repositories/progress_repository.rb +166 -0
- data/lib/shoko/adapters/storage/repositories/storage/annotation_file_store.rb +128 -0
- data/lib/shoko/adapters/storage/repositories/storage/bookmark_file_store.rb +109 -0
- data/lib/shoko/adapters/storage/repositories/storage/file_store_utils.rb +20 -0
- data/lib/shoko/adapters/storage/repositories/storage/progress_file_store.rb +59 -0
- data/lib/shoko/application/annotation_editor_overlay_session.rb +138 -0
- data/lib/shoko/application/cli.rb +134 -0
- data/lib/shoko/application/controllers/menu/input_controller.rb +189 -0
- data/lib/shoko/application/controllers/menu/state_controller.rb +642 -0
- data/lib/shoko/application/controllers/menu_controller.rb +469 -0
- data/lib/shoko/application/controllers/mouseable_reader.rb +377 -0
- data/lib/shoko/application/controllers/reader_controller.rb +449 -0
- data/lib/shoko/application/controllers/state_controller.rb +410 -0
- data/lib/shoko/application/controllers/ui_controller.rb +782 -0
- data/lib/shoko/application/dependency_container.rb +301 -0
- data/lib/shoko/application/infrastructure/event_bus.rb +80 -0
- data/lib/shoko/application/infrastructure/observer_state_store.rb +136 -0
- data/lib/shoko/application/infrastructure/state_store.rb +413 -0
- data/lib/shoko/application/main_menu/menu_progress_presenter.rb +83 -0
- data/lib/shoko/application/pending_jump_handler.rb +122 -0
- data/lib/shoko/application/reader_lifecycle.rb +65 -0
- data/lib/shoko/application/reader_startup_orchestrator.rb +113 -0
- data/lib/shoko/application/selectors/config_selectors.rb +62 -0
- data/lib/shoko/application/selectors/menu_selectors.rb +62 -0
- data/lib/shoko/application/selectors/reader_selectors.rb +186 -0
- data/lib/shoko/application/state/actions/base_action.rb +24 -0
- data/lib/shoko/application/state/actions/quit_to_menu_action.rb +16 -0
- data/lib/shoko/application/state/actions/switch_reader_mode_action.rb +22 -0
- data/lib/shoko/application/state/actions/toggle_view_mode_action.rb +31 -0
- data/lib/shoko/application/state/actions/update_annotation_editor_overlay_action.rb +27 -0
- data/lib/shoko/application/state/actions/update_annotations_action.rb +20 -0
- data/lib/shoko/application/state/actions/update_annotations_overlay_action.rb +27 -0
- data/lib/shoko/application/state/actions/update_bookmarks_action.rb +20 -0
- data/lib/shoko/application/state/actions/update_chapter_action.rb +24 -0
- data/lib/shoko/application/state/actions/update_config_action.rb +22 -0
- data/lib/shoko/application/state/actions/update_field_helpers.rb +26 -0
- data/lib/shoko/application/state/actions/update_menu_action.rb +21 -0
- data/lib/shoko/application/state/actions/update_message_action.rb +35 -0
- data/lib/shoko/application/state/actions/update_page_action.rb +21 -0
- data/lib/shoko/application/state/actions/update_pagination_state_action.rb +21 -0
- data/lib/shoko/application/state/actions/update_popup_menu_action.rb +27 -0
- data/lib/shoko/application/state/actions/update_reader_meta_action.rb +21 -0
- data/lib/shoko/application/state/actions/update_reader_mode_action.rb +20 -0
- data/lib/shoko/application/state/actions/update_rendered_lines_action.rb +40 -0
- data/lib/shoko/application/state/actions/update_selection_action.rb +27 -0
- data/lib/shoko/application/state/actions/update_selections_action.rb +21 -0
- data/lib/shoko/application/state/actions/update_sidebar_action.rb +34 -0
- data/lib/shoko/application/state/actions/update_ui_loading_action.rb +23 -0
- data/lib/shoko/application/ui/reader_view_model_builder.rb +74 -0
- data/lib/shoko/application/ui/view_models/reader_view_model.rb +177 -0
- data/lib/shoko/application/unified_application.rb +48 -0
- data/lib/shoko/application/use_cases/catalog_service.rb +117 -0
- data/lib/shoko/application/use_cases/commands/annotation_editor_commands.rb +105 -0
- data/lib/shoko/application/use_cases/commands/application_commands.rb +208 -0
- data/lib/shoko/application/use_cases/commands/base_command.rb +166 -0
- data/lib/shoko/application/use_cases/commands/bookmark_commands.rb +114 -0
- data/lib/shoko/application/use_cases/commands/conditional_navigation_commands.rb +57 -0
- data/lib/shoko/application/use_cases/commands/menu_commands.rb +170 -0
- data/lib/shoko/application/use_cases/commands/navigation_commands.rb +183 -0
- data/lib/shoko/application/use_cases/commands/reader_commands.rb +46 -0
- data/lib/shoko/application/use_cases/commands/sidebar_commands.rb +55 -0
- data/lib/shoko/application/use_cases/settings_service.rb +123 -0
- data/lib/shoko/core/events/annotation_events.rb +94 -0
- data/lib/shoko/core/events/base_domain_event.rb +169 -0
- data/lib/shoko/core/events/bookmark_events.rb +41 -0
- data/lib/shoko/core/events/domain_event_bus.rb +163 -0
- data/lib/shoko/core/events/progress_events.rb +108 -0
- data/lib/shoko/core/models/bookmark.rb +36 -0
- data/lib/shoko/core/models/bookmark_data.rb +10 -0
- data/lib/shoko/core/models/chapter.rb +25 -0
- data/lib/shoko/core/models/content_block.rb +44 -0
- data/lib/shoko/core/models/reader_settings.rb +20 -0
- data/lib/shoko/core/models/selection_anchor.rb +73 -0
- data/lib/shoko/core/models/toc_entry.rb +14 -0
- data/lib/shoko/core/ports/annotation_repository.rb +0 -0
- data/lib/shoko/core/ports/book_repository.rb +0 -0
- data/lib/shoko/core/ports/book_source.rb +0 -0
- data/lib/shoko/core/ports/bookmark_repository.rb +0 -0
- data/lib/shoko/core/ports/cache.rb +0 -0
- data/lib/shoko/core/ports/input_handler.rb +0 -0
- data/lib/shoko/core/ports/renderer.rb +0 -0
- data/lib/shoko/core/ports/storage.rb +0 -0
- data/lib/shoko/core/services/annotation_service.rb +102 -0
- data/lib/shoko/core/services/base_service.rb +60 -0
- data/lib/shoko/core/services/bookmark_service.rb +267 -0
- data/lib/shoko/core/services/coordinate_service.rb +265 -0
- data/lib/shoko/core/services/layout_service.rb +95 -0
- data/lib/shoko/core/services/navigation/absolute_change_applier.rb +96 -0
- data/lib/shoko/core/services/navigation/absolute_layout.rb +101 -0
- data/lib/shoko/core/services/navigation/absolute_strategy.rb +179 -0
- data/lib/shoko/core/services/navigation/context_builder.rb +52 -0
- data/lib/shoko/core/services/navigation/context_helpers.rb +63 -0
- data/lib/shoko/core/services/navigation/dynamic_change_applier.rb +50 -0
- data/lib/shoko/core/services/navigation/dynamic_strategy.rb +51 -0
- data/lib/shoko/core/services/navigation/image_offset_snapper.rb +150 -0
- data/lib/shoko/core/services/navigation/nav_context.rb +27 -0
- data/lib/shoko/core/services/navigation/state_updater.rb +29 -0
- data/lib/shoko/core/services/navigation/strategy_factory.rb +20 -0
- data/lib/shoko/core/services/navigation_service.rb +150 -0
- data/lib/shoko/core/services/page_calculator_service.rb +242 -0
- data/lib/shoko/core/services/pagination/internal/absolute_page_map_builder.rb +28 -0
- data/lib/shoko/core/services/pagination/internal/chapter_cache.rb +60 -0
- data/lib/shoko/core/services/pagination/internal/dynamic_page_map_builder.rb +157 -0
- data/lib/shoko/core/services/pagination/internal/layout_metrics_calculator.rb +73 -0
- data/lib/shoko/core/services/pagination/internal/page_hydrator.rb +145 -0
- data/lib/shoko/core/services/pagination/internal/pagination_workflow.rb +152 -0
- data/lib/shoko/core/services/pagination/page_info_calculator.rb +247 -0
- data/lib/shoko/core/services/pagination/pagination_cache_preloader.rb +173 -0
- data/lib/shoko/core/services/pagination/pagination_coordinator.rb +202 -0
- data/lib/shoko/core/services/pagination/pagination_orchestrator.rb +291 -0
- data/lib/shoko/core/services/pagination.rb +10 -0
- data/lib/shoko/core/services/progress_helper.rb +22 -0
- data/lib/shoko/core/services/selection_service.rb +126 -0
- data/lib/shoko/core/validator.rb +76 -0
- data/lib/shoko/shared/errors.rb +97 -0
- data/lib/shoko/shared/version.rb +5 -0
- data/lib/shoko/test_support/terminal_double.rb +175 -0
- data/lib/shoko/test_support/test_mode.rb +78 -0
- data/lib/shoko.rb +279 -0
- data/lib/zip.rb +732 -0
- data/zip.rb +5 -0
- metadata +370 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_domain_event'
|
|
4
|
+
|
|
5
|
+
module Shoko
|
|
6
|
+
module Core
|
|
7
|
+
module Events
|
|
8
|
+
# Domain event for annotation creation
|
|
9
|
+
class AnnotationAdded < BaseDomainEvent
|
|
10
|
+
required_attributes :book_path, :annotation
|
|
11
|
+
typed_attributes book_path: String
|
|
12
|
+
|
|
13
|
+
def initialize(book_path:, annotation:, **)
|
|
14
|
+
super(
|
|
15
|
+
aggregate_id: book_path,
|
|
16
|
+
book_path: book_path,
|
|
17
|
+
annotation: annotation,
|
|
18
|
+
**
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def book_path
|
|
23
|
+
get_attribute(:book_path)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def annotation
|
|
27
|
+
get_attribute(:annotation)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Domain event for annotation updates
|
|
32
|
+
class AnnotationUpdated < BaseDomainEvent
|
|
33
|
+
required_attributes :book_path, :annotation_id, :old_note, :new_note
|
|
34
|
+
typed_attributes book_path: String, annotation_id: String, old_note: String,
|
|
35
|
+
new_note: String
|
|
36
|
+
|
|
37
|
+
def initialize(book_path:, annotation_id:, old_note:, new_note:, **)
|
|
38
|
+
super(
|
|
39
|
+
aggregate_id: book_path,
|
|
40
|
+
book_path: book_path,
|
|
41
|
+
annotation_id: annotation_id,
|
|
42
|
+
old_note: old_note,
|
|
43
|
+
new_note: new_note,
|
|
44
|
+
**
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def book_path
|
|
49
|
+
get_attribute(:book_path)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def annotation_id
|
|
53
|
+
get_attribute(:annotation_id)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def old_note
|
|
57
|
+
get_attribute(:old_note)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def new_note
|
|
61
|
+
get_attribute(:new_note)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Domain event for annotation removal
|
|
66
|
+
class AnnotationRemoved < BaseDomainEvent
|
|
67
|
+
required_attributes :book_path, :annotation_id
|
|
68
|
+
typed_attributes book_path: String, annotation_id: String
|
|
69
|
+
|
|
70
|
+
def initialize(book_path:, annotation_id:, annotation: nil, **)
|
|
71
|
+
super(
|
|
72
|
+
aggregate_id: book_path,
|
|
73
|
+
book_path: book_path,
|
|
74
|
+
annotation_id: annotation_id,
|
|
75
|
+
annotation: annotation,
|
|
76
|
+
**
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def book_path
|
|
81
|
+
get_attribute(:book_path)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def annotation_id
|
|
85
|
+
get_attribute(:annotation_id)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def annotation
|
|
89
|
+
get_attribute(:annotation)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
require 'time'
|
|
5
|
+
|
|
6
|
+
module Shoko
|
|
7
|
+
module Core
|
|
8
|
+
module Events
|
|
9
|
+
# Base class for all domain events.
|
|
10
|
+
#
|
|
11
|
+
# Domain events represent something that has happened in the domain
|
|
12
|
+
# and are used to trigger side effects and maintain loose coupling
|
|
13
|
+
# between different parts of the system.
|
|
14
|
+
#
|
|
15
|
+
# @example Creating a domain event
|
|
16
|
+
# class BookmarkAdded < BaseDomainEvent
|
|
17
|
+
# attribute :book_path, String
|
|
18
|
+
# attribute :bookmark, Bookmark
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# @example Using a domain event
|
|
22
|
+
# event = BookmarkAdded.new(book_path: '/path/to/book.epub', bookmark: bookmark)
|
|
23
|
+
# event_bus.publish(event)
|
|
24
|
+
class BaseDomainEvent
|
|
25
|
+
# Event metadata
|
|
26
|
+
attr_reader :event_id, :occurred_at, :aggregate_id, :version
|
|
27
|
+
|
|
28
|
+
def initialize(aggregate_id: nil, version: 1, **attributes)
|
|
29
|
+
@event_id = SecureRandom.uuid
|
|
30
|
+
@occurred_at = Time.now.utc
|
|
31
|
+
@aggregate_id = aggregate_id
|
|
32
|
+
@version = version
|
|
33
|
+
@attributes = attributes
|
|
34
|
+
|
|
35
|
+
validate_required_attributes
|
|
36
|
+
validate_attribute_types
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Get event type name
|
|
40
|
+
#
|
|
41
|
+
# @return [String] Event type name
|
|
42
|
+
def event_type
|
|
43
|
+
self.class.name.split('::').last
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Get event data as hash
|
|
47
|
+
#
|
|
48
|
+
# @return [Hash] Event data
|
|
49
|
+
def event_data
|
|
50
|
+
@attributes.dup
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Get specific attribute value
|
|
54
|
+
#
|
|
55
|
+
# @param name [Symbol] Attribute name
|
|
56
|
+
# @return [Object] Attribute value
|
|
57
|
+
def get_attribute(name)
|
|
58
|
+
@attributes[name]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Convert event to hash for serialization
|
|
62
|
+
#
|
|
63
|
+
# @return [Hash] Serialized event
|
|
64
|
+
def to_h
|
|
65
|
+
{
|
|
66
|
+
event_id: @event_id,
|
|
67
|
+
event_type: event_type,
|
|
68
|
+
occurred_at: @occurred_at.iso8601,
|
|
69
|
+
aggregate_id: @aggregate_id,
|
|
70
|
+
version: @version,
|
|
71
|
+
data: @attributes,
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Create event from hash
|
|
76
|
+
#
|
|
77
|
+
# @param hash [Hash] Serialized event data
|
|
78
|
+
# @return [BaseDomainEvent] Reconstructed event
|
|
79
|
+
def self.from_h(hash)
|
|
80
|
+
event = allocate
|
|
81
|
+
event.instance_variable_set(:@event_id, hash[:event_id] || hash['event_id'])
|
|
82
|
+
event.instance_variable_set(:@occurred_at,
|
|
83
|
+
Time.parse(hash[:occurred_at] || hash['occurred_at']))
|
|
84
|
+
event.instance_variable_set(:@aggregate_id, hash[:aggregate_id] || hash['aggregate_id'])
|
|
85
|
+
event.instance_variable_set(:@version, hash[:version] || hash['version'] || 1)
|
|
86
|
+
event.instance_variable_set(:@attributes, hash[:data] || hash['data'] || {})
|
|
87
|
+
event
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Define required attributes for the event
|
|
91
|
+
#
|
|
92
|
+
# @example
|
|
93
|
+
# class MyEvent < BaseDomainEvent
|
|
94
|
+
# required_attributes :user_id, :action
|
|
95
|
+
# end
|
|
96
|
+
def self.required_attributes(*attrs)
|
|
97
|
+
@required_attributes ||= []
|
|
98
|
+
@required_attributes.concat(attrs) if attrs.any?
|
|
99
|
+
@required_attributes
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Define typed attributes for the event
|
|
103
|
+
#
|
|
104
|
+
# @example
|
|
105
|
+
# class MyEvent < BaseDomainEvent
|
|
106
|
+
# typed_attributes user_id: String, count: Integer
|
|
107
|
+
# end
|
|
108
|
+
def self.typed_attributes(types = {})
|
|
109
|
+
@typed_attributes ||= {}
|
|
110
|
+
@typed_attributes.merge!(types) if types.any?
|
|
111
|
+
@typed_attributes
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Check if event is of specific type
|
|
115
|
+
#
|
|
116
|
+
# @param type [String, Symbol] Event type to check
|
|
117
|
+
# @return [Boolean] True if event is of specified type
|
|
118
|
+
def of_type?(type)
|
|
119
|
+
event_type == type.to_s
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# String representation of the event
|
|
123
|
+
#
|
|
124
|
+
# @return [String] String representation
|
|
125
|
+
def to_s
|
|
126
|
+
"#{event_type}(#{@event_id})[#{@occurred_at}]"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Equality comparison
|
|
130
|
+
#
|
|
131
|
+
# @param other [BaseDomainEvent] Other event to compare
|
|
132
|
+
# @return [Boolean] True if events are equal
|
|
133
|
+
def ==(other)
|
|
134
|
+
return false unless other.is_a?(BaseDomainEvent)
|
|
135
|
+
|
|
136
|
+
@event_id == other.event_id &&
|
|
137
|
+
event_type == other.event_type &&
|
|
138
|
+
@attributes == other.event_data
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
|
|
143
|
+
def validate_required_attributes
|
|
144
|
+
klass = self.class
|
|
145
|
+
required = klass.required_attributes
|
|
146
|
+
return unless required
|
|
147
|
+
|
|
148
|
+
missing = required - @attributes.keys
|
|
149
|
+
return if missing.empty?
|
|
150
|
+
|
|
151
|
+
raise ArgumentError, "Missing required attributes: #{missing.join(', ')}"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def validate_attribute_types
|
|
155
|
+
klass = self.class
|
|
156
|
+
typed = klass.typed_attributes
|
|
157
|
+
return unless typed
|
|
158
|
+
|
|
159
|
+
typed.each do |attr, type|
|
|
160
|
+
value = @attributes[attr]
|
|
161
|
+
next if value.nil? # Allow nil values
|
|
162
|
+
|
|
163
|
+
raise TypeError, "Attribute #{attr} must be of type #{type}, got #{value.class}" unless value.is_a?(type)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_domain_event'
|
|
4
|
+
|
|
5
|
+
module Shoko
|
|
6
|
+
module Core
|
|
7
|
+
module Events
|
|
8
|
+
# Common base for bookmark-related events
|
|
9
|
+
class BookmarkEventBase < BaseDomainEvent
|
|
10
|
+
required_attributes :book_path, :bookmark
|
|
11
|
+
typed_attributes book_path: String
|
|
12
|
+
|
|
13
|
+
def initialize(book_path:, bookmark:, **)
|
|
14
|
+
super(
|
|
15
|
+
aggregate_id: book_path,
|
|
16
|
+
book_path: book_path,
|
|
17
|
+
bookmark: bookmark,
|
|
18
|
+
**
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def book_path
|
|
23
|
+
get_attribute(:book_path)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def bookmark
|
|
27
|
+
get_attribute(:bookmark)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Domain event for bookmark creation
|
|
32
|
+
class BookmarkAdded < BookmarkEventBase; end
|
|
33
|
+
|
|
34
|
+
# Domain event for bookmark removal
|
|
35
|
+
class BookmarkRemoved < BookmarkEventBase; end
|
|
36
|
+
|
|
37
|
+
# Domain event for bookmark navigation
|
|
38
|
+
class BookmarkNavigated < BookmarkEventBase; end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_domain_event.rb'
|
|
4
|
+
|
|
5
|
+
module Shoko
|
|
6
|
+
module Core
|
|
7
|
+
module Events
|
|
8
|
+
# Domain event bus for publishing and subscribing to domain events.
|
|
9
|
+
#
|
|
10
|
+
# This provides a domain-specific event bus that sits on top of the
|
|
11
|
+
# infrastructure event bus and handles domain event serialization,
|
|
12
|
+
# routing, and subscription management.
|
|
13
|
+
#
|
|
14
|
+
# @example Publishing an event
|
|
15
|
+
# event_bus.publish(BookmarkAdded.new(book_path: path, bookmark: bookmark))
|
|
16
|
+
#
|
|
17
|
+
# @example Subscribing to events
|
|
18
|
+
# event_bus.subscribe(BookmarkAdded) { |event| handle_bookmark_added(event) }
|
|
19
|
+
class DomainEventBus
|
|
20
|
+
def initialize(infrastructure_event_bus)
|
|
21
|
+
@infrastructure_bus = infrastructure_event_bus
|
|
22
|
+
@subscribers = Hash.new { |h, k| h[k] = [] }
|
|
23
|
+
@middleware = []
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Publish a domain event
|
|
27
|
+
#
|
|
28
|
+
# @param event [Core::Events::BaseDomainEvent] Domain event to publish
|
|
29
|
+
def publish(event)
|
|
30
|
+
unless event.is_a?(Core::Events::BaseDomainEvent)
|
|
31
|
+
raise ArgumentError, 'Event must be a BaseDomainEvent'
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Apply middleware chain
|
|
35
|
+
processed_event = apply_middleware(event)
|
|
36
|
+
return if processed_event.nil? # Event was filtered out
|
|
37
|
+
|
|
38
|
+
# Publish through infrastructure bus
|
|
39
|
+
@infrastructure_bus.emit_event(
|
|
40
|
+
processed_event.event_type.to_sym,
|
|
41
|
+
event_data: processed_event.to_h
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Notify domain subscribers directly
|
|
45
|
+
notify_subscribers(processed_event)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Subscribe to a specific event type
|
|
49
|
+
#
|
|
50
|
+
# @param event_type [Class] Event class to subscribe to
|
|
51
|
+
# @param handler [Proc] Handler block or callable
|
|
52
|
+
# @yield [event] Block to handle the event
|
|
53
|
+
def subscribe(event_type, handler = nil, &)
|
|
54
|
+
handler = resolve_callable!(handler, &)
|
|
55
|
+
@subscribers[event_type] << handler
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Subscribe to multiple event types with the same handler
|
|
59
|
+
#
|
|
60
|
+
# @param event_types [Array<Class>] Event classes to subscribe to
|
|
61
|
+
# @param handler [Proc] Handler block or callable
|
|
62
|
+
# @yield [event] Block to handle the event
|
|
63
|
+
def subscribe_to_many(event_types, handler = nil, &)
|
|
64
|
+
handler = resolve_callable!(handler, &)
|
|
65
|
+
event_types.each { |type| subscribe(type, handler) }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Unsubscribe from an event type
|
|
69
|
+
#
|
|
70
|
+
# @param event_type [Class] Event class to unsubscribe from
|
|
71
|
+
# @param handler [Proc] Specific handler to remove (optional)
|
|
72
|
+
def unsubscribe(event_type, handler = nil)
|
|
73
|
+
if handler
|
|
74
|
+
@subscribers[event_type].delete(handler)
|
|
75
|
+
else
|
|
76
|
+
@subscribers.delete(event_type)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Add middleware to the event processing pipeline
|
|
81
|
+
#
|
|
82
|
+
# @param middleware [Proc] Middleware that processes events
|
|
83
|
+
# @yield [event] Block that processes the event and returns modified event or nil
|
|
84
|
+
def add_middleware(middleware = nil, &block)
|
|
85
|
+
middleware = block if block_given?
|
|
86
|
+
raise ArgumentError, 'Middleware must be provided' unless middleware
|
|
87
|
+
|
|
88
|
+
@middleware << middleware
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def resolve_callable!(callable, &block)
|
|
92
|
+
callable = block if block_given?
|
|
93
|
+
raise ArgumentError, 'Handler must be provided' unless callable
|
|
94
|
+
|
|
95
|
+
callable
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Get all subscribers for an event type
|
|
99
|
+
#
|
|
100
|
+
# @param event_type [Class] Event class
|
|
101
|
+
# @return [Array<Proc>] List of subscribers
|
|
102
|
+
def subscribers_for(event_type)
|
|
103
|
+
@subscribers[event_type].dup
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Check if there are subscribers for an event type
|
|
107
|
+
#
|
|
108
|
+
# @param event_type [Class] Event class
|
|
109
|
+
# @return [Boolean] True if there are subscribers
|
|
110
|
+
def subscribers?(event_type)
|
|
111
|
+
@subscribers[event_type].any?
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Clear all subscribers (useful for testing)
|
|
115
|
+
def clear_subscribers
|
|
116
|
+
@subscribers.clear
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Get subscriber count for an event type
|
|
120
|
+
#
|
|
121
|
+
# @param event_type [Class] Event class
|
|
122
|
+
# @return [Integer] Number of subscribers
|
|
123
|
+
def subscriber_count(event_type)
|
|
124
|
+
@subscribers[event_type].size
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Get total number of subscribers across all event types
|
|
128
|
+
#
|
|
129
|
+
# @return [Integer] Total subscriber count
|
|
130
|
+
def total_subscribers
|
|
131
|
+
@subscribers.values.sum(&:size)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
def apply_middleware(event)
|
|
137
|
+
@middleware.reduce(event) do |current_event, middleware|
|
|
138
|
+
next current_event if current_event.nil?
|
|
139
|
+
|
|
140
|
+
begin
|
|
141
|
+
middleware.call(current_event)
|
|
142
|
+
rescue StandardError => e
|
|
143
|
+
# Log middleware error but don't stop event processing
|
|
144
|
+
warn "Domain event middleware error: #{e.message}"
|
|
145
|
+
current_event
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def notify_subscribers(event)
|
|
151
|
+
event_type = event.class
|
|
152
|
+
|
|
153
|
+
@subscribers[event_type].each do |handler|
|
|
154
|
+
handler.call(event)
|
|
155
|
+
rescue StandardError => e
|
|
156
|
+
# Log subscriber error but continue with other subscribers
|
|
157
|
+
warn "Domain event subscriber error for #{event_type}: #{e.message}"
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_domain_event'
|
|
4
|
+
|
|
5
|
+
module Shoko
|
|
6
|
+
module Core
|
|
7
|
+
module Events
|
|
8
|
+
# Domain event for reading progress updates
|
|
9
|
+
class ProgressUpdated < BaseDomainEvent
|
|
10
|
+
required_attributes :book_path, :chapter_index, :line_offset
|
|
11
|
+
typed_attributes book_path: String, chapter_index: Integer, line_offset: Integer
|
|
12
|
+
|
|
13
|
+
def initialize(book_path:, chapter_index:, line_offset:, previous_chapter: nil,
|
|
14
|
+
previous_line: nil, **)
|
|
15
|
+
super(
|
|
16
|
+
aggregate_id: book_path,
|
|
17
|
+
book_path: book_path,
|
|
18
|
+
chapter_index: chapter_index,
|
|
19
|
+
line_offset: line_offset,
|
|
20
|
+
previous_chapter: previous_chapter,
|
|
21
|
+
previous_line: previous_line,
|
|
22
|
+
**
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def book_path
|
|
27
|
+
get_attribute(:book_path)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def chapter_index
|
|
31
|
+
get_attribute(:chapter_index)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def line_offset
|
|
35
|
+
get_attribute(:line_offset)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def previous_chapter
|
|
39
|
+
get_attribute(:previous_chapter)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def previous_line
|
|
43
|
+
get_attribute(:previous_line)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Check if this represents forward progress
|
|
47
|
+
def forward_progress?
|
|
48
|
+
return true if previous_chapter.nil? || previous_line.nil?
|
|
49
|
+
|
|
50
|
+
chapter_index > previous_chapter ||
|
|
51
|
+
(chapter_index == previous_chapter && line_offset > previous_line)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Domain event for reading session start
|
|
56
|
+
class ReadingSessionStarted < BaseDomainEvent
|
|
57
|
+
required_attributes :book_path
|
|
58
|
+
typed_attributes book_path: String
|
|
59
|
+
|
|
60
|
+
def initialize(book_path:, **)
|
|
61
|
+
super(
|
|
62
|
+
aggregate_id: book_path,
|
|
63
|
+
book_path: book_path,
|
|
64
|
+
**
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def book_path
|
|
69
|
+
get_attribute(:book_path)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Domain event for reading session end
|
|
74
|
+
class ReadingSessionEnded < BaseDomainEvent
|
|
75
|
+
required_attributes :book_path, :duration_seconds
|
|
76
|
+
typed_attributes book_path: String, duration_seconds: Integer
|
|
77
|
+
|
|
78
|
+
def initialize(book_path:, duration_seconds:, final_chapter: nil, final_line: nil,
|
|
79
|
+
**)
|
|
80
|
+
super(
|
|
81
|
+
aggregate_id: book_path,
|
|
82
|
+
book_path: book_path,
|
|
83
|
+
duration_seconds: duration_seconds,
|
|
84
|
+
final_chapter: final_chapter,
|
|
85
|
+
final_line: final_line,
|
|
86
|
+
**
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def book_path
|
|
91
|
+
get_attribute(:book_path)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def duration_seconds
|
|
95
|
+
get_attribute(:duration_seconds)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def final_chapter
|
|
99
|
+
get_attribute(:final_chapter)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def final_line
|
|
103
|
+
get_attribute(:final_line)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'time'
|
|
4
|
+
|
|
5
|
+
module Shoko
|
|
6
|
+
module Core
|
|
7
|
+
module Models
|
|
8
|
+
# Represents a bookmark within a document.
|
|
9
|
+
Bookmark = Struct.new(:chapter_index, :line_offset, :text_snippet, :created_at,
|
|
10
|
+
keyword_init: true) do
|
|
11
|
+
# Build from hash loaded from disk
|
|
12
|
+
# @param hash [Hash]
|
|
13
|
+
# @return [Bookmark]
|
|
14
|
+
def self.from_h(hash)
|
|
15
|
+
new(
|
|
16
|
+
chapter_index: hash['chapter'],
|
|
17
|
+
line_offset: hash['line_offset'],
|
|
18
|
+
text_snippet: hash['text'],
|
|
19
|
+
created_at: Time.parse(hash['timestamp'])
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Convert to hash for persistence
|
|
24
|
+
# @return [Hash]
|
|
25
|
+
def to_h
|
|
26
|
+
{
|
|
27
|
+
'chapter' => chapter_index,
|
|
28
|
+
'line_offset' => line_offset,
|
|
29
|
+
'text' => text_snippet,
|
|
30
|
+
'timestamp' => created_at.iso8601,
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shoko
|
|
4
|
+
module Core
|
|
5
|
+
module Models
|
|
6
|
+
# Represents a chapter within an EPUB document.
|
|
7
|
+
Chapter = Struct.new(:number, :title, :lines, :metadata, :blocks, :raw_content,
|
|
8
|
+
keyword_init: true) do
|
|
9
|
+
# Number of lines in the chapter
|
|
10
|
+
# @return [Integer]
|
|
11
|
+
def line_count
|
|
12
|
+
lines.size
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Estimated reading time in minutes
|
|
16
|
+
# @param wpm [Integer] words per minute
|
|
17
|
+
# @return [Integer]
|
|
18
|
+
def estimated_reading_time(wpm = 250)
|
|
19
|
+
word_count = lines.join(' ').split.size
|
|
20
|
+
(word_count / wpm.to_f).ceil
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shoko
|
|
4
|
+
module Core
|
|
5
|
+
module Models
|
|
6
|
+
# Represents a unit of formatted content (heading, paragraph, list item, etc.).
|
|
7
|
+
ContentBlock = Struct.new(:type, :segments, :level, :metadata, keyword_init: true) do
|
|
8
|
+
def text
|
|
9
|
+
segments.to_a.map { |segment| segment&.text.to_s }.join
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def heading_level
|
|
13
|
+
(metadata && metadata[:level]) || level
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Represents a contiguous run of text with associated inline styles.
|
|
18
|
+
TextSegment = Struct.new(:text, :styles, keyword_init: true) do
|
|
19
|
+
def initialize(text:, styles: nil)
|
|
20
|
+
super(text: text.to_s, styles: (styles || {}).freeze)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def length
|
|
24
|
+
text.to_s.length
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Represents a display-ready line produced by the formatting pipeline.
|
|
29
|
+
DisplayLine = Struct.new(:text, :segments, :metadata, keyword_init: true) do
|
|
30
|
+
def initialize(text:, segments:, metadata: nil)
|
|
31
|
+
super(text: text.to_s, segments: segments || [], metadata: metadata || {})
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def length
|
|
35
|
+
text.length
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def empty?
|
|
39
|
+
text.strip.empty?
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|