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,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shoko
|
|
4
|
+
module Adapters::Storage::Repositories
|
|
5
|
+
# Base class for all repository implementations in the domain layer.
|
|
6
|
+
#
|
|
7
|
+
# Repositories provide an abstraction layer between domain services and
|
|
8
|
+
# infrastructure storage mechanisms, following the Repository pattern from
|
|
9
|
+
# Domain-Driven Design.
|
|
10
|
+
#
|
|
11
|
+
# All repositories should:
|
|
12
|
+
# - Provide domain-focused methods (not storage-focused)
|
|
13
|
+
# - Return domain objects or primitives
|
|
14
|
+
# - Handle storage-specific errors and convert to domain errors
|
|
15
|
+
# - Use dependency injection for storage implementations
|
|
16
|
+
#
|
|
17
|
+
# @example Implementing a repository
|
|
18
|
+
# class MyRepository < BaseRepository
|
|
19
|
+
# def find_by_id(id)
|
|
20
|
+
# storage_result = @storage.find(id)
|
|
21
|
+
# convert_to_domain_object(storage_result)
|
|
22
|
+
# rescue Storage::NotFoundError => e
|
|
23
|
+
# raise EntityNotFoundError, e.message
|
|
24
|
+
# end
|
|
25
|
+
# end
|
|
26
|
+
class BaseRepository
|
|
27
|
+
# Repository-specific errors
|
|
28
|
+
class RepositoryError < StandardError; end
|
|
29
|
+
class EntityNotFoundError < RepositoryError; end
|
|
30
|
+
class ValidationError < RepositoryError; end
|
|
31
|
+
class PersistenceError < RepositoryError; end
|
|
32
|
+
|
|
33
|
+
def initialize(dependencies)
|
|
34
|
+
@dependencies = dependencies
|
|
35
|
+
@logger = dependencies.resolve(:logger)
|
|
36
|
+
setup_repository_dependencies
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
protected
|
|
40
|
+
|
|
41
|
+
attr_reader :dependencies, :logger
|
|
42
|
+
|
|
43
|
+
# Template method for subclasses to set up their specific dependencies
|
|
44
|
+
def setup_repository_dependencies
|
|
45
|
+
# Override in subclasses to resolve storage dependencies
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Helper to handle common storage errors
|
|
49
|
+
def handle_storage_error(error, context = nil)
|
|
50
|
+
msg = error.message
|
|
51
|
+
message = context ? "#{context}: #{msg}" : msg
|
|
52
|
+
logger.error("Repository error - #{message}")
|
|
53
|
+
|
|
54
|
+
case error
|
|
55
|
+
when NoMethodError, ArgumentError
|
|
56
|
+
raise ValidationError, message
|
|
57
|
+
else
|
|
58
|
+
raise PersistenceError, message
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Helper to validate required parameters
|
|
63
|
+
def validate_required_params(params, required_keys)
|
|
64
|
+
missing_keys = required_keys.select do |key|
|
|
65
|
+
val = params[key]
|
|
66
|
+
!params.key?(key) || val.nil? || (val.respond_to?(:empty?) && val.empty?)
|
|
67
|
+
end
|
|
68
|
+
return if missing_keys.empty?
|
|
69
|
+
|
|
70
|
+
raise ValidationError, "Missing required parameters: #{missing_keys.join(', ')}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Helper to ensure entity exists before operations
|
|
74
|
+
def ensure_entity_exists(entity, entity_name = 'Entity')
|
|
75
|
+
return if entity
|
|
76
|
+
|
|
77
|
+
raise EntityNotFoundError, "#{entity_name} not found"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_repository'
|
|
4
|
+
require_relative '../../../core/models/bookmark_data.rb'
|
|
5
|
+
require_relative 'storage/bookmark_file_store'
|
|
6
|
+
|
|
7
|
+
module Shoko
|
|
8
|
+
module Adapters::Storage::Repositories
|
|
9
|
+
# Repository for bookmark persistence, abstracting the underlying storage mechanism.
|
|
10
|
+
#
|
|
11
|
+
# This repository provides a clean domain interface for bookmark operations,
|
|
12
|
+
# hiding the file-based persistence details from domain services.
|
|
13
|
+
#
|
|
14
|
+
# @example Adding a bookmark
|
|
15
|
+
# repo = BookmarkRepository.new(dependencies)
|
|
16
|
+
# repo.add_for_book('/path/to/book.epub', chapter: 2, line: 50, text: 'Important quote')
|
|
17
|
+
#
|
|
18
|
+
# @example Getting bookmarks for a book
|
|
19
|
+
# bookmarks = repo.find_by_book_path('/path/to/book.epub')
|
|
20
|
+
class BookmarkRepository < BaseRepository
|
|
21
|
+
def initialize(dependencies)
|
|
22
|
+
super
|
|
23
|
+
file_writer = dependencies.resolve(:file_writer)
|
|
24
|
+
@storage = Storage::BookmarkFileStore.new(file_writer:)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Add a bookmark for a specific book
|
|
28
|
+
#
|
|
29
|
+
# @param book_path [String] Path to the EPUB file
|
|
30
|
+
# @param chapter_index [Integer] Chapter index (0-based)
|
|
31
|
+
# @param line_offset [Integer] Line offset within the chapter
|
|
32
|
+
# @param text_snippet [String] Text snippet for the bookmark
|
|
33
|
+
# @return [Models::Bookmark] The created bookmark
|
|
34
|
+
def add_for_book(book_path, chapter_index:, line_offset:, text_snippet:)
|
|
35
|
+
validate_required_params(
|
|
36
|
+
{ book_path: book_path, chapter_index: chapter_index, line_offset: line_offset },
|
|
37
|
+
%i[book_path chapter_index line_offset]
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
bookmark_data = Core::Models::BookmarkData.new(
|
|
41
|
+
path: book_path,
|
|
42
|
+
chapter: chapter_index,
|
|
43
|
+
line_offset: line_offset,
|
|
44
|
+
text: text_snippet || ''
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
begin
|
|
48
|
+
@storage.add(bookmark_data)
|
|
49
|
+
|
|
50
|
+
# Return the bookmark object that was created
|
|
51
|
+
bookmarks = find_by_book_path(book_path)
|
|
52
|
+
# Find the most recently added bookmark (by timestamp)
|
|
53
|
+
bookmarks.max_by(&:created_at)
|
|
54
|
+
rescue StandardError => e
|
|
55
|
+
handle_storage_error(e, "adding bookmark for #{book_path}")
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Find all bookmarks for a specific book
|
|
60
|
+
#
|
|
61
|
+
# @param book_path [String] Path to the EPUB file
|
|
62
|
+
# @return [Array<Models::Bookmark>] Array of bookmarks for the book
|
|
63
|
+
def find_by_book_path(book_path)
|
|
64
|
+
validate_required_params({ book_path: book_path }, [:book_path])
|
|
65
|
+
|
|
66
|
+
begin
|
|
67
|
+
@storage.get(book_path) || []
|
|
68
|
+
rescue StandardError => e
|
|
69
|
+
handle_storage_error(e, "loading bookmarks for #{book_path}")
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Delete a specific bookmark
|
|
74
|
+
#
|
|
75
|
+
# @param book_path [String] Path to the EPUB file
|
|
76
|
+
# @param bookmark [Models::Bookmark] The bookmark to delete
|
|
77
|
+
# @return [Boolean] True if deleted successfully
|
|
78
|
+
def delete_for_book(book_path, bookmark)
|
|
79
|
+
# Ensure entity existence takes precedence for clearer error semantics
|
|
80
|
+
ensure_entity_exists(bookmark, 'Bookmark')
|
|
81
|
+
validate_required_params({ book_path: book_path }, %i[book_path])
|
|
82
|
+
|
|
83
|
+
begin
|
|
84
|
+
@storage.delete(book_path, bookmark)
|
|
85
|
+
true
|
|
86
|
+
rescue StandardError => e
|
|
87
|
+
handle_storage_error(e, "deleting bookmark for #{book_path}")
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Check if a bookmark exists at the given position
|
|
92
|
+
#
|
|
93
|
+
# @param book_path [String] Path to the EPUB file
|
|
94
|
+
# @param chapter_index [Integer] Chapter index
|
|
95
|
+
# @param line_offset [Integer] Line offset within the chapter
|
|
96
|
+
# @return [Boolean] True if a bookmark exists at this position
|
|
97
|
+
def exists_at_position?(book_path, chapter_index, line_offset)
|
|
98
|
+
bookmarks = find_by_book_path(book_path)
|
|
99
|
+
bookmarks.any? do |bookmark|
|
|
100
|
+
bookmark.chapter_index == chapter_index && bookmark.line_offset == line_offset
|
|
101
|
+
end
|
|
102
|
+
rescue StandardError => e
|
|
103
|
+
handle_storage_error(e, "checking bookmark existence for #{book_path}")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Get bookmark count for a book
|
|
107
|
+
#
|
|
108
|
+
# @param book_path [String] Path to the EPUB file
|
|
109
|
+
# @return [Integer] Number of bookmarks for the book
|
|
110
|
+
def count_for_book(book_path)
|
|
111
|
+
find_by_book_path(book_path).size
|
|
112
|
+
rescue StandardError => e
|
|
113
|
+
handle_storage_error(e, "counting bookmarks for #{book_path}")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Find bookmark at a specific position
|
|
117
|
+
#
|
|
118
|
+
# @param book_path [String] Path to the EPUB file
|
|
119
|
+
# @param chapter_index [Integer] Chapter index
|
|
120
|
+
# @param line_offset [Integer] Line offset within the chapter
|
|
121
|
+
# @return [Models::Bookmark, nil] The bookmark at this position, or nil
|
|
122
|
+
def find_at_position(book_path, chapter_index, line_offset)
|
|
123
|
+
bookmarks = find_by_book_path(book_path)
|
|
124
|
+
bookmarks.find do |bookmark|
|
|
125
|
+
bookmark.chapter_index == chapter_index && bookmark.line_offset == line_offset
|
|
126
|
+
end
|
|
127
|
+
rescue StandardError => e
|
|
128
|
+
handle_storage_error(e, "finding bookmark at position for #{book_path}")
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'time'
|
|
5
|
+
|
|
6
|
+
require_relative '../cache_paths'
|
|
7
|
+
require_relative '../json_cache_store'
|
|
8
|
+
require_relative '../cache_pointer_manager'
|
|
9
|
+
require_relative '../epub_cache'
|
|
10
|
+
require_relative '../../output/terminal/terminal_sanitizer.rb'
|
|
11
|
+
|
|
12
|
+
module Shoko
|
|
13
|
+
module Adapters::Storage::Repositories
|
|
14
|
+
# Provides read-only access to cached library metadata on disk.
|
|
15
|
+
class CachedLibraryRepository
|
|
16
|
+
def initialize(cache_root: Adapters::Storage::CachePaths.cache_root, store: nil)
|
|
17
|
+
@cache_root = cache_root
|
|
18
|
+
@cache_store = store || Adapters::Storage::JsonCacheStore.new(cache_root:)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def list_entries
|
|
22
|
+
rows = fetch_manifest_rows
|
|
23
|
+
rows = fetch_rows if rows.empty?
|
|
24
|
+
return [] if rows.empty?
|
|
25
|
+
|
|
26
|
+
rows.filter_map { |row| build_entry_from_row(row) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def fetch_rows
|
|
32
|
+
@cache_store.list_books
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def fetch_manifest_rows
|
|
36
|
+
Adapters::Storage::JsonCacheStore.manifest_rows(@cache_root)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def build_entry_from_row(row)
|
|
40
|
+
sha = row.is_a?(Hash) ? (row['source_sha'] || row[:source_sha]) : nil
|
|
41
|
+
pointer_path = Adapters::Storage::EpubCache.cache_path_for_sha(sha, cache_root: @cache_root)
|
|
42
|
+
return nil unless pointer_path
|
|
43
|
+
|
|
44
|
+
ensure_pointer_file(row, pointer_path)
|
|
45
|
+
|
|
46
|
+
metadata = parse_json_object(row['metadata_json'])
|
|
47
|
+
authors = parse_json_array(row['authors_json']).map { |name| sanitize_display(name.to_s) }
|
|
48
|
+
|
|
49
|
+
{
|
|
50
|
+
title: sanitize_display(present_or_default(row['title'], 'Unknown')),
|
|
51
|
+
authors: authors.join(', '),
|
|
52
|
+
year: extract_year(metadata),
|
|
53
|
+
size_bytes: (row['cache_size_bytes'] || safe_file_size(pointer_path)).to_i,
|
|
54
|
+
open_path: pointer_path,
|
|
55
|
+
epub_path: row['source_path'].to_s,
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def ensure_pointer_file(row, path)
|
|
60
|
+
return path if File.exist?(path) || row['source_sha'].to_s.empty?
|
|
61
|
+
|
|
62
|
+
generated_at = begin
|
|
63
|
+
raw = row['generated_at']
|
|
64
|
+
raw ? Time.at(raw.to_f).utc.iso8601 : Time.now.utc.iso8601
|
|
65
|
+
rescue StandardError
|
|
66
|
+
Time.now.utc.iso8601
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
metadata = {
|
|
70
|
+
'format' => Adapters::Storage::CachePointerManager::POINTER_FORMAT,
|
|
71
|
+
'version' => Adapters::Storage::CachePointerManager::POINTER_VERSION,
|
|
72
|
+
'sha256' => row['source_sha'],
|
|
73
|
+
'source_path' => row['source_path'],
|
|
74
|
+
'generated_at' => generated_at,
|
|
75
|
+
'engine' => Adapters::Storage::JsonCacheStore::ENGINE,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
Adapters::Storage::CachePointerManager.new(path).write(metadata)
|
|
79
|
+
path
|
|
80
|
+
rescue StandardError
|
|
81
|
+
path
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def parse_json_object(value)
|
|
85
|
+
return {} unless value
|
|
86
|
+
return value if value.is_a?(Hash)
|
|
87
|
+
|
|
88
|
+
parsed = JSON.parse(value)
|
|
89
|
+
parsed.is_a?(Hash) ? parsed : {}
|
|
90
|
+
rescue JSON::ParserError, TypeError
|
|
91
|
+
{}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def parse_json_array(value)
|
|
95
|
+
return [] unless value
|
|
96
|
+
return value if value.is_a?(Array)
|
|
97
|
+
|
|
98
|
+
parsed = JSON.parse(value)
|
|
99
|
+
parsed.is_a?(Array) ? parsed : []
|
|
100
|
+
rescue JSON::ParserError, TypeError
|
|
101
|
+
[]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def extract_year(metadata)
|
|
105
|
+
return '' unless metadata.respond_to?(:[])
|
|
106
|
+
|
|
107
|
+
year = metadata['year'] || metadata[:year]
|
|
108
|
+
year ? year.to_s : ''
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def present_or_default(value, fallback)
|
|
112
|
+
str = value.to_s.strip
|
|
113
|
+
str.empty? ? fallback : value
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def safe_file_size(path)
|
|
117
|
+
File.size(path)
|
|
118
|
+
rescue StandardError
|
|
119
|
+
0
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def sanitize_display(text)
|
|
123
|
+
Shoko::Adapters::Output::Terminal::TerminalSanitizer.sanitize(text.to_s, preserve_newlines: false, preserve_tabs: false)
|
|
124
|
+
rescue StandardError
|
|
125
|
+
text.to_s
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_repository'
|
|
4
|
+
|
|
5
|
+
module Shoko
|
|
6
|
+
module Adapters::Storage::Repositories
|
|
7
|
+
# Repository for configuration persistence, abstracting the underlying storage mechanism.
|
|
8
|
+
#
|
|
9
|
+
# This repository provides a clean domain interface for configuration operations,
|
|
10
|
+
# using the StateStore as the single source of truth for all configuration data.
|
|
11
|
+
#
|
|
12
|
+
# @example Getting configuration
|
|
13
|
+
# repo = ConfigRepository.new(dependencies)
|
|
14
|
+
# view_mode = repo.view_mode
|
|
15
|
+
# line_spacing = repo.line_spacing
|
|
16
|
+
#
|
|
17
|
+
# @example Updating configuration
|
|
18
|
+
# repo.update_view_mode(:split)
|
|
19
|
+
# repo.update_line_spacing(:relaxed)
|
|
20
|
+
class ConfigRepository < BaseRepository
|
|
21
|
+
LINE_SPACING_ALIASES = {
|
|
22
|
+
tight: :compact,
|
|
23
|
+
wide: :relaxed,
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
LINE_SPACING_ALLOWED = %i[compact normal relaxed].freeze
|
|
27
|
+
|
|
28
|
+
# Default configuration values
|
|
29
|
+
DEFAULT_CONFIG = {
|
|
30
|
+
view_mode: :split,
|
|
31
|
+
page_numbering_mode: :dynamic,
|
|
32
|
+
show_page_numbers: true,
|
|
33
|
+
line_spacing: :compact,
|
|
34
|
+
highlight_quotes: true,
|
|
35
|
+
input_debounce_ms: 100,
|
|
36
|
+
search_highlight_timeout: 2000,
|
|
37
|
+
auto_save_interval: 30,
|
|
38
|
+
theme: :default,
|
|
39
|
+
}.freeze
|
|
40
|
+
|
|
41
|
+
def initialize(dependencies)
|
|
42
|
+
super
|
|
43
|
+
@state_store = dependencies.resolve(:global_state)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Get the current view mode
|
|
47
|
+
#
|
|
48
|
+
# @return [Symbol] Current view mode (:split or :single)
|
|
49
|
+
def view_mode
|
|
50
|
+
get_config_value(:view_mode, DEFAULT_CONFIG[:view_mode])
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Update the view mode
|
|
54
|
+
#
|
|
55
|
+
# @param mode [Symbol] New view mode (:split or :single)
|
|
56
|
+
# @return [Boolean] True if updated successfully
|
|
57
|
+
def update_view_mode(mode)
|
|
58
|
+
validate_enum_value(:view_mode, mode, %i[split single])
|
|
59
|
+
update_config_value(:view_mode, mode)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Get the current page numbering mode
|
|
63
|
+
#
|
|
64
|
+
# @return [Symbol] Current page numbering mode (:absolute or :dynamic)
|
|
65
|
+
def page_numbering_mode
|
|
66
|
+
get_config_value(:page_numbering_mode, DEFAULT_CONFIG[:page_numbering_mode])
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Update the page numbering mode
|
|
70
|
+
#
|
|
71
|
+
# @param mode [Symbol] New page numbering mode (:absolute or :dynamic)
|
|
72
|
+
# @return [Boolean] True if updated successfully
|
|
73
|
+
def update_page_numbering_mode(mode)
|
|
74
|
+
validate_enum_value(:page_numbering_mode, mode, %i[absolute dynamic])
|
|
75
|
+
update_config_value(:page_numbering_mode, mode)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Get whether page numbers are shown
|
|
79
|
+
#
|
|
80
|
+
# @return [Boolean] True if page numbers should be shown
|
|
81
|
+
def show_page_numbers?
|
|
82
|
+
get_config_value(:show_page_numbers, DEFAULT_CONFIG[:show_page_numbers])
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Update whether to show page numbers
|
|
86
|
+
#
|
|
87
|
+
# @param show [Boolean] Whether to show page numbers
|
|
88
|
+
# @return [Boolean] True if updated successfully
|
|
89
|
+
def update_show_page_numbers(show)
|
|
90
|
+
validate_boolean_value(:show_page_numbers, show)
|
|
91
|
+
update_config_value(:show_page_numbers, show)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Get the current line spacing
|
|
95
|
+
#
|
|
96
|
+
# @return [Symbol] Current line spacing (:compact, :normal, or :relaxed)
|
|
97
|
+
def line_spacing
|
|
98
|
+
raw = get_config_value(:line_spacing, DEFAULT_CONFIG[:line_spacing])
|
|
99
|
+
normalized = normalize_line_spacing(raw)
|
|
100
|
+
update_config_value(:line_spacing, normalized) if raw != normalized
|
|
101
|
+
normalized
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Update the line spacing
|
|
105
|
+
#
|
|
106
|
+
# @param spacing [Symbol] New line spacing (:compact, :normal, or :relaxed)
|
|
107
|
+
# @return [Boolean] True if updated successfully
|
|
108
|
+
def update_line_spacing(spacing)
|
|
109
|
+
normalized = normalize_line_spacing(spacing)
|
|
110
|
+
validate_enum_value(:line_spacing, normalized, LINE_SPACING_ALLOWED)
|
|
111
|
+
update_config_value(:line_spacing, normalized)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Get the input debounce time in milliseconds
|
|
115
|
+
#
|
|
116
|
+
# @return [Integer] Input debounce time in milliseconds
|
|
117
|
+
def input_debounce_ms
|
|
118
|
+
get_config_value(:input_debounce_ms, DEFAULT_CONFIG[:input_debounce_ms])
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Update the input debounce time
|
|
122
|
+
#
|
|
123
|
+
# @param ms [Integer] New debounce time in milliseconds
|
|
124
|
+
# @return [Boolean] True if updated successfully
|
|
125
|
+
def update_input_debounce_ms(milliseconds)
|
|
126
|
+
validate_positive_integer(:input_debounce_ms, milliseconds)
|
|
127
|
+
update_config_value(:input_debounce_ms, milliseconds)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Get all configuration as a hash
|
|
131
|
+
#
|
|
132
|
+
# @return [Hash] All configuration values
|
|
133
|
+
def all_config
|
|
134
|
+
config_state = @state_store.get(%i[config]) || {}
|
|
135
|
+
DEFAULT_CONFIG.merge(config_state)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Update multiple configuration values at once
|
|
139
|
+
#
|
|
140
|
+
# @param config_hash [Hash] Hash of configuration key-value pairs
|
|
141
|
+
# @return [Boolean] True if updated successfully
|
|
142
|
+
def update_multiple(config_hash)
|
|
143
|
+
validate_required_params({ config_hash: config_hash }, [:config_hash])
|
|
144
|
+
|
|
145
|
+
begin
|
|
146
|
+
# Validate each value before applying any updates
|
|
147
|
+
normalized = {}
|
|
148
|
+
config_hash.each do |key, value|
|
|
149
|
+
coerced = key == :line_spacing ? normalize_line_spacing(value) : value
|
|
150
|
+
validate_config_key_value(key, coerced)
|
|
151
|
+
normalized[key] = coerced
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Apply updates as a single state transaction
|
|
155
|
+
@state_store.update(normalized.transform_keys { |k| [:config, k] })
|
|
156
|
+
true
|
|
157
|
+
rescue StandardError => e
|
|
158
|
+
# Bubble up validation errors directly; wrap others
|
|
159
|
+
raise e if e.is_a?(BaseRepository::ValidationError)
|
|
160
|
+
|
|
161
|
+
handle_storage_error(e, 'updating multiple config values')
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Reset configuration to defaults
|
|
166
|
+
#
|
|
167
|
+
# @return [Boolean] True if reset successfully
|
|
168
|
+
def reset_to_defaults
|
|
169
|
+
@state_store.update(DEFAULT_CONFIG.transform_keys { |k| [:config, k] })
|
|
170
|
+
true
|
|
171
|
+
rescue StandardError => e
|
|
172
|
+
handle_storage_error(e, 'resetting configuration to defaults')
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Check if a configuration key has been customized from default
|
|
176
|
+
#
|
|
177
|
+
# @param key [Symbol] Configuration key to check
|
|
178
|
+
# @return [Boolean] True if the value differs from default
|
|
179
|
+
def customized?(key)
|
|
180
|
+
current_value = get_config_value(key, nil)
|
|
181
|
+
default_value = DEFAULT_CONFIG[key]
|
|
182
|
+
current_value != default_value
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
private
|
|
186
|
+
|
|
187
|
+
# Get a configuration value with fallback to default
|
|
188
|
+
def get_config_value(key, default_value)
|
|
189
|
+
value = @state_store.get([:config, key])
|
|
190
|
+
value.nil? ? default_value : value
|
|
191
|
+
rescue StandardError => e
|
|
192
|
+
handle_storage_error(e, "getting config value #{key}")
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Update a single configuration value
|
|
196
|
+
def update_config_value(key, value)
|
|
197
|
+
stored_value = key == :line_spacing ? normalize_line_spacing(value) : value
|
|
198
|
+
@state_store.update({ [:config, key] => stored_value })
|
|
199
|
+
true
|
|
200
|
+
rescue StandardError => e
|
|
201
|
+
handle_storage_error(e, "updating config value #{key}")
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Validate that a value is one of the allowed enum values
|
|
205
|
+
def validate_enum_value(key, value, allowed_values)
|
|
206
|
+
return if allowed_values.include?(value)
|
|
207
|
+
|
|
208
|
+
raise ValidationError,
|
|
209
|
+
"Invalid #{key}: #{value}. Must be one of: #{allowed_values.join(', ')}"
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Validate that a value is a boolean
|
|
213
|
+
def validate_boolean_value(key, value)
|
|
214
|
+
return if value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
215
|
+
|
|
216
|
+
raise ValidationError, "Invalid #{key}: #{value}. Must be true or false"
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Validate that a value is a positive integer
|
|
220
|
+
def validate_positive_integer(key, value)
|
|
221
|
+
return if value.is_a?(Integer) && value.positive?
|
|
222
|
+
|
|
223
|
+
raise ValidationError, "Invalid #{key}: #{value}. Must be a positive integer"
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Validate a specific config key-value pair
|
|
227
|
+
def validate_config_key_value(key, value)
|
|
228
|
+
case key
|
|
229
|
+
when :view_mode
|
|
230
|
+
validate_enum_value(key, value, %i[split single])
|
|
231
|
+
when :page_numbering_mode
|
|
232
|
+
validate_enum_value(key, value, %i[absolute dynamic])
|
|
233
|
+
when :line_spacing
|
|
234
|
+
normalized = normalize_line_spacing(value)
|
|
235
|
+
validate_enum_value(key, normalized, LINE_SPACING_ALLOWED)
|
|
236
|
+
when :show_page_numbers
|
|
237
|
+
validate_boolean_value(key, value)
|
|
238
|
+
when :input_debounce_ms, :search_highlight_timeout, :auto_save_interval
|
|
239
|
+
validate_positive_integer(key, value)
|
|
240
|
+
when :theme
|
|
241
|
+
# Allow any symbol for theme for extensibility
|
|
242
|
+
unless value.is_a?(Symbol)
|
|
243
|
+
raise ValidationError,
|
|
244
|
+
"Invalid #{key}: #{value}. Must be a symbol"
|
|
245
|
+
end
|
|
246
|
+
else
|
|
247
|
+
# Unknown keys are allowed for forward compatibility
|
|
248
|
+
logger.debug("Unknown config key: #{key}")
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def normalize_line_spacing(value)
|
|
253
|
+
sym = begin
|
|
254
|
+
value.is_a?(String) ? value.downcase.to_sym : value&.to_sym
|
|
255
|
+
rescue StandardError
|
|
256
|
+
nil
|
|
257
|
+
end
|
|
258
|
+
LINE_SPACING_ALIASES.fetch(sym, sym || DEFAULT_CONFIG[:line_spacing])
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|