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,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'time'
|
|
4
|
+
|
|
5
|
+
require_relative 'base_repository'
|
|
6
|
+
require_relative 'storage/progress_file_store'
|
|
7
|
+
|
|
8
|
+
module Shoko
|
|
9
|
+
module Adapters::Storage::Repositories
|
|
10
|
+
# Repository for reading progress persistence, abstracting the underlying storage mechanism.
|
|
11
|
+
#
|
|
12
|
+
# This repository provides a clean domain interface for progress operations,
|
|
13
|
+
# hiding the file-based persistence details from domain services.
|
|
14
|
+
#
|
|
15
|
+
# @example Saving progress
|
|
16
|
+
# repo = ProgressRepository.new(dependencies)
|
|
17
|
+
# repo.save_for_book('/path/to/book.epub', chapter_index: 3, line_offset: 150)
|
|
18
|
+
#
|
|
19
|
+
# @example Loading progress
|
|
20
|
+
# progress = repo.find_by_book_path('/path/to/book.epub')
|
|
21
|
+
class ProgressRepository < BaseRepository
|
|
22
|
+
# Progress data structure
|
|
23
|
+
ProgressData = Struct.new(:chapter_index, :line_offset, :timestamp, keyword_init: true) do
|
|
24
|
+
def to_h
|
|
25
|
+
{
|
|
26
|
+
chapter: chapter_index,
|
|
27
|
+
line_offset: line_offset,
|
|
28
|
+
timestamp: timestamp,
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.from_h(hash)
|
|
33
|
+
return nil unless hash
|
|
34
|
+
|
|
35
|
+
new(
|
|
36
|
+
chapter_index: hash['chapter'] || hash[:chapter],
|
|
37
|
+
line_offset: hash['line_offset'] || hash[:line_offset],
|
|
38
|
+
timestamp: hash['timestamp'] || hash[:timestamp]
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def initialize(dependencies)
|
|
44
|
+
super
|
|
45
|
+
file_writer = dependencies.resolve(:file_writer)
|
|
46
|
+
@storage = Storage::ProgressFileStore.new(file_writer:)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Save reading progress for a specific book
|
|
50
|
+
#
|
|
51
|
+
# @param book_path [String] Path to the EPUB file
|
|
52
|
+
# @param chapter_index [Integer] Chapter index (0-based)
|
|
53
|
+
# @param line_offset [Integer] Line offset within the chapter
|
|
54
|
+
# @return [ProgressData] The saved progress data
|
|
55
|
+
def save_for_book(book_path, chapter_index:, line_offset:)
|
|
56
|
+
validate_required_params(
|
|
57
|
+
{ book_path: book_path, chapter_index: chapter_index, line_offset: line_offset },
|
|
58
|
+
%i[book_path chapter_index line_offset]
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
begin
|
|
62
|
+
@storage.save(book_path, chapter_index, line_offset)
|
|
63
|
+
|
|
64
|
+
# Return the progress data that was saved
|
|
65
|
+
ProgressData.new(
|
|
66
|
+
chapter_index: chapter_index,
|
|
67
|
+
line_offset: line_offset,
|
|
68
|
+
timestamp: Time.now.iso8601
|
|
69
|
+
)
|
|
70
|
+
rescue StandardError => e
|
|
71
|
+
handle_storage_error(e, "saving progress for #{book_path}")
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Find reading progress for a specific book
|
|
76
|
+
#
|
|
77
|
+
# @param book_path [String] Path to the EPUB file
|
|
78
|
+
# @return [ProgressData, nil] Progress data for the book, or nil if none exists
|
|
79
|
+
def find_by_book_path(book_path)
|
|
80
|
+
validate_required_params({ book_path: book_path }, [:book_path])
|
|
81
|
+
|
|
82
|
+
begin
|
|
83
|
+
progress_hash = @storage.load(book_path)
|
|
84
|
+
ProgressData.from_h(progress_hash)
|
|
85
|
+
rescue StandardError => e
|
|
86
|
+
handle_storage_error(e, "loading progress for #{book_path}")
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Find all reading progress across all books
|
|
91
|
+
#
|
|
92
|
+
# @return [Hash<String, ProgressData>] Hash mapping book paths to progress data
|
|
93
|
+
def find_all
|
|
94
|
+
all_progress = @storage.load_all
|
|
95
|
+
all_progress.transform_values { |progress_hash| ProgressData.from_h(progress_hash) }
|
|
96
|
+
rescue StandardError => e
|
|
97
|
+
handle_storage_error(e, 'loading all progress data')
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Check if progress exists for a book
|
|
101
|
+
#
|
|
102
|
+
# @param book_path [String] Path to the EPUB file
|
|
103
|
+
# @return [Boolean] True if progress data exists for the book
|
|
104
|
+
def exists_for_book?(book_path)
|
|
105
|
+
!find_by_book_path(book_path).nil?
|
|
106
|
+
rescue StandardError => e
|
|
107
|
+
handle_storage_error(e, "checking progress existence for #{book_path}")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Get the timestamp of the last progress update for a book
|
|
111
|
+
#
|
|
112
|
+
# @param book_path [String] Path to the EPUB file
|
|
113
|
+
# @return [Time, nil] Last update timestamp, or nil if no progress exists
|
|
114
|
+
def last_updated_at(book_path)
|
|
115
|
+
progress = find_by_book_path(book_path)
|
|
116
|
+
ts = progress&.timestamp
|
|
117
|
+
return nil unless ts
|
|
118
|
+
|
|
119
|
+
Time.parse(ts)
|
|
120
|
+
rescue StandardError => e
|
|
121
|
+
handle_storage_error(e, "getting last update time for #{book_path}")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Get books ordered by most recently read
|
|
125
|
+
#
|
|
126
|
+
# @param limit [Integer, nil] Maximum number of books to return
|
|
127
|
+
# @return [Array<String>] Array of book paths ordered by recency
|
|
128
|
+
def recent_books(limit: nil)
|
|
129
|
+
all_progress = find_all
|
|
130
|
+
sorted_paths = all_progress.sort_by do |_path, progress|
|
|
131
|
+
ts = progress.timestamp
|
|
132
|
+
ts ? Time.parse(ts) : Time.at(0)
|
|
133
|
+
end.reverse.map(&:first)
|
|
134
|
+
|
|
135
|
+
limit ? sorted_paths.take(limit) : sorted_paths
|
|
136
|
+
rescue StandardError => e
|
|
137
|
+
handle_storage_error(e, 'getting recent books')
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Update progress only if the new position is further than current
|
|
141
|
+
#
|
|
142
|
+
# @param book_path [String] Path to the EPUB file
|
|
143
|
+
# @param chapter_index [Integer] Chapter index (0-based)
|
|
144
|
+
# @param line_offset [Integer] Line offset within the chapter
|
|
145
|
+
# @return [ProgressData] The saved progress data
|
|
146
|
+
def save_if_further(book_path, chapter_index:, line_offset:)
|
|
147
|
+
current_progress = find_by_book_path(book_path)
|
|
148
|
+
|
|
149
|
+
should_save = if current_progress.nil?
|
|
150
|
+
true
|
|
151
|
+
else
|
|
152
|
+
cur_ch = current_progress.chapter_index
|
|
153
|
+
cur_off = current_progress.line_offset
|
|
154
|
+
chapter_index > cur_ch ||
|
|
155
|
+
(chapter_index == cur_ch && line_offset > cur_off)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
return save_for_book(book_path, chapter_index: chapter_index, line_offset: line_offset) if should_save
|
|
159
|
+
|
|
160
|
+
current_progress
|
|
161
|
+
rescue StandardError => e
|
|
162
|
+
handle_storage_error(e, "conditionally saving progress for #{book_path}")
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'time'
|
|
6
|
+
|
|
7
|
+
require_relative '../../../output/terminal/terminal_sanitizer.rb'
|
|
8
|
+
require_relative 'file_store_utils'
|
|
9
|
+
require_relative '../../config_paths'
|
|
10
|
+
# Domain storage helpers should operate via injected services to avoid reaching into infrastructure.
|
|
11
|
+
|
|
12
|
+
module Shoko
|
|
13
|
+
module Adapters::Storage::Repositories::Storage
|
|
14
|
+
# File-backed annotation storage under Domain.
|
|
15
|
+
# Persists annotations to ${XDG_CONFIG_HOME:-~/.config}/shoko/annotations.json
|
|
16
|
+
class AnnotationFileStore
|
|
17
|
+
FILE_NAME = 'annotations.json'
|
|
18
|
+
|
|
19
|
+
def initialize(file_writer:)
|
|
20
|
+
@file_writer = file_writer
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def all
|
|
24
|
+
sanitize_all(load_all)
|
|
25
|
+
rescue StandardError
|
|
26
|
+
{}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def get(path)
|
|
30
|
+
sanitize_list(load_all[path.to_s] || []).dup
|
|
31
|
+
rescue StandardError
|
|
32
|
+
[]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def add(path, text, note, range, chapter_index, page_meta = nil)
|
|
36
|
+
data = load_all
|
|
37
|
+
key = path.to_s
|
|
38
|
+
list = data[key] || []
|
|
39
|
+
now = Time.now
|
|
40
|
+
ann = {
|
|
41
|
+
'id' => now.to_f.to_s,
|
|
42
|
+
'text' => sanitize_body(text),
|
|
43
|
+
'note' => sanitize_body(note),
|
|
44
|
+
'range' => range,
|
|
45
|
+
'chapter_index' => chapter_index,
|
|
46
|
+
'created_at' => now.iso8601,
|
|
47
|
+
}
|
|
48
|
+
if page_meta.is_a?(Hash)
|
|
49
|
+
ann['page_current'] = page_meta[:current] || page_meta['current']
|
|
50
|
+
ann['page_total'] = page_meta[:total] || page_meta['total']
|
|
51
|
+
ann['page_mode'] = page_meta[:type] || page_meta['type']
|
|
52
|
+
end
|
|
53
|
+
list << ann
|
|
54
|
+
data[key] = list
|
|
55
|
+
save_all(data)
|
|
56
|
+
true
|
|
57
|
+
rescue StandardError
|
|
58
|
+
false
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def update(path, id, note)
|
|
62
|
+
data = load_all
|
|
63
|
+
key = path.to_s
|
|
64
|
+
list = data[key] || []
|
|
65
|
+
ann = list.find { |a| a['id'] == id }
|
|
66
|
+
return false unless ann
|
|
67
|
+
|
|
68
|
+
ann['note'] = sanitize_body(note)
|
|
69
|
+
ann['updated_at'] = Time.now.iso8601
|
|
70
|
+
data[key] = list
|
|
71
|
+
save_all(data)
|
|
72
|
+
true
|
|
73
|
+
rescue StandardError
|
|
74
|
+
false
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def delete(path, id)
|
|
78
|
+
data = load_all
|
|
79
|
+
key = path.to_s
|
|
80
|
+
list = data[key] || []
|
|
81
|
+
list.reject! { |a| a['id'] == id }
|
|
82
|
+
list.empty? ? data.delete(key) : data[key] = list
|
|
83
|
+
save_all(data)
|
|
84
|
+
true
|
|
85
|
+
rescue StandardError
|
|
86
|
+
false
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
attr_reader :file_writer
|
|
92
|
+
|
|
93
|
+
def load_all
|
|
94
|
+
FileStoreUtils.load_json_or_empty(file_path)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def sanitize_all(data)
|
|
98
|
+
return {} unless data.is_a?(Hash)
|
|
99
|
+
|
|
100
|
+
data.transform_values { |list| sanitize_list(list) }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def sanitize_list(list)
|
|
104
|
+
Array(list).map do |ann|
|
|
105
|
+
next ann unless ann.is_a?(Hash)
|
|
106
|
+
|
|
107
|
+
safe = ann.dup
|
|
108
|
+
safe['text'] = sanitize_body(safe['text'])
|
|
109
|
+
safe['note'] = sanitize_body(safe['note'])
|
|
110
|
+
safe
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def sanitize_body(text)
|
|
115
|
+
Shoko::Adapters::Output::Terminal::TerminalSanitizer.sanitize(text.to_s, preserve_newlines: true, preserve_tabs: true)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def save_all(data)
|
|
119
|
+
payload = JSON.pretty_generate(data)
|
|
120
|
+
file_writer.write(file_path, payload)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def file_path
|
|
124
|
+
Adapters::Storage::ConfigPaths.config_path(FILE_NAME)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'time'
|
|
6
|
+
|
|
7
|
+
require_relative '../../../output/terminal/terminal_sanitizer.rb'
|
|
8
|
+
require_relative '../../../../core/models/bookmark.rb'
|
|
9
|
+
require_relative '../../../../core/models/bookmark_data.rb'
|
|
10
|
+
require_relative 'file_store_utils'
|
|
11
|
+
require_relative '../../config_paths'
|
|
12
|
+
# Domain storage helpers should operate via injected services to avoid reaching into infrastructure.
|
|
13
|
+
|
|
14
|
+
module Shoko
|
|
15
|
+
module Adapters::Storage::Repositories::Storage
|
|
16
|
+
# File-backed bookmark storage isolated under Domain.
|
|
17
|
+
# Persists bookmarks to ${XDG_CONFIG_HOME:-~/.config}/shoko/bookmarks.json
|
|
18
|
+
class BookmarkFileStore
|
|
19
|
+
FILE_NAME = 'bookmarks.json'
|
|
20
|
+
|
|
21
|
+
def initialize(file_writer:)
|
|
22
|
+
@file_writer = file_writer
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def add(bookmark_data)
|
|
26
|
+
unless bookmark_data.is_a?(Shoko::Core::Models::BookmarkData)
|
|
27
|
+
raise ArgumentError, 'bookmark_data must be BookmarkData'
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
all = load_all
|
|
31
|
+
path = bookmark_data.path.to_s
|
|
32
|
+
list = all[path] || []
|
|
33
|
+
entry = {
|
|
34
|
+
'chapter' => bookmark_data.chapter,
|
|
35
|
+
'line_offset' => bookmark_data.line_offset,
|
|
36
|
+
'text' => sanitize_text(bookmark_data.text),
|
|
37
|
+
'timestamp' => Time.now.iso8601,
|
|
38
|
+
}
|
|
39
|
+
list << entry
|
|
40
|
+
all[path] = list
|
|
41
|
+
save_all(all)
|
|
42
|
+
entry
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def get(path)
|
|
46
|
+
all = load_all
|
|
47
|
+
list = all[path.to_s] || []
|
|
48
|
+
list.map do |h|
|
|
49
|
+
safe = h.is_a?(Hash) ? h.dup : {}
|
|
50
|
+
safe['text'] = sanitize_text(safe['text'])
|
|
51
|
+
Shoko::Core::Models::Bookmark.from_h(safe)
|
|
52
|
+
end
|
|
53
|
+
rescue StandardError
|
|
54
|
+
[]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def delete(path, bookmark)
|
|
58
|
+
all = load_all
|
|
59
|
+
key = path.to_s
|
|
60
|
+
list = all[key] || []
|
|
61
|
+
# Delete by matching serialized representation
|
|
62
|
+
predicate = if bookmark.respond_to?(:to_h)
|
|
63
|
+
target = bookmark.to_h
|
|
64
|
+
->(stored_entry) { equivalent?(stored_entry, target) }
|
|
65
|
+
else
|
|
66
|
+
# Best-effort: match by position
|
|
67
|
+
chapter = bookmark.respond_to?(:chapter_index) ? bookmark.chapter_index : bookmark[:chapter_index]
|
|
68
|
+
offset = bookmark.respond_to?(:line_offset) ? bookmark.line_offset : bookmark[:line_offset]
|
|
69
|
+
lambda { |stored_entry|
|
|
70
|
+
stored_entry['chapter'] == chapter && stored_entry['line_offset'] == offset
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
list.reject!(&predicate)
|
|
74
|
+
list.empty? ? all.delete(key) : all[key] = list
|
|
75
|
+
save_all(all)
|
|
76
|
+
true
|
|
77
|
+
rescue StandardError
|
|
78
|
+
false
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
attr_reader :file_writer
|
|
84
|
+
|
|
85
|
+
def equivalent?(stored_entry, target)
|
|
86
|
+
stored_entry['chapter'] == target['chapter'] &&
|
|
87
|
+
stored_entry['line_offset'] == target['line_offset'] &&
|
|
88
|
+
(stored_entry['text'].to_s == target['text'].to_s)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def sanitize_text(text)
|
|
92
|
+
Shoko::Adapters::Output::Terminal::TerminalSanitizer.sanitize(text.to_s, preserve_newlines: false, preserve_tabs: false)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def load_all
|
|
96
|
+
FileStoreUtils.load_json_or_empty(file_path)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def save_all(data)
|
|
100
|
+
payload = JSON.pretty_generate(data)
|
|
101
|
+
file_writer.write(file_path, payload)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def file_path
|
|
105
|
+
Adapters::Storage::ConfigPaths.config_path(FILE_NAME)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Shoko
|
|
6
|
+
module Adapters::Storage::Repositories::Storage
|
|
7
|
+
# Small, shared helpers for file-backed JSON stores.
|
|
8
|
+
module FileStoreUtils
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def load_json_or_empty(file_path)
|
|
12
|
+
return {} unless File.exist?(file_path)
|
|
13
|
+
|
|
14
|
+
JSON.parse(File.read(file_path))
|
|
15
|
+
rescue StandardError
|
|
16
|
+
{}
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'time'
|
|
6
|
+
require_relative 'file_store_utils'
|
|
7
|
+
require_relative '../../config_paths'
|
|
8
|
+
# Domain storage helpers should operate via injected services to avoid reaching into infrastructure.
|
|
9
|
+
|
|
10
|
+
module Shoko
|
|
11
|
+
module Adapters::Storage::Repositories::Storage
|
|
12
|
+
# File-backed progress storage under Domain.
|
|
13
|
+
# Persists progress to ${XDG_CONFIG_HOME:-~/.config}/shoko/progress.json
|
|
14
|
+
class ProgressFileStore
|
|
15
|
+
FILE_NAME = 'progress.json'
|
|
16
|
+
|
|
17
|
+
def initialize(file_writer:)
|
|
18
|
+
@file_writer = file_writer
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def save(path, chapter_index, line_offset)
|
|
22
|
+
all = load_all
|
|
23
|
+
all[path.to_s] = {
|
|
24
|
+
'chapter' => chapter_index,
|
|
25
|
+
'line_offset' => line_offset,
|
|
26
|
+
'timestamp' => Time.now.iso8601,
|
|
27
|
+
}
|
|
28
|
+
save_all(all)
|
|
29
|
+
true
|
|
30
|
+
rescue StandardError
|
|
31
|
+
false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def load(path)
|
|
35
|
+
all = load_all
|
|
36
|
+
all[path.to_s]
|
|
37
|
+
rescue StandardError
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def load_all
|
|
42
|
+
FileStoreUtils.load_json_or_empty(file_path)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
attr_reader :file_writer
|
|
48
|
+
|
|
49
|
+
def save_all(data)
|
|
50
|
+
payload = JSON.pretty_generate(data)
|
|
51
|
+
file_writer.write(file_path, payload)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def file_path
|
|
55
|
+
Adapters::Storage::ConfigPaths.config_path(FILE_NAME)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'state/actions/update_selection_action.rb'
|
|
4
|
+
require_relative '../application/selectors/reader_selectors'
|
|
5
|
+
|
|
6
|
+
module Shoko
|
|
7
|
+
module Application
|
|
8
|
+
# Adapter exposing the annotation editor overlay through the same interface as
|
|
9
|
+
# the legacy screen component so Application commands can drive it via the input
|
|
10
|
+
# dispatcher.
|
|
11
|
+
class AnnotationEditorOverlaySession
|
|
12
|
+
def initialize(state, dependencies, ui_controller)
|
|
13
|
+
@state = state
|
|
14
|
+
@dependencies = dependencies
|
|
15
|
+
@ui_controller = ui_controller
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def active?
|
|
19
|
+
overlay = current_overlay
|
|
20
|
+
overlay.respond_to?(:visible?) && overlay.visible?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def save_annotation
|
|
24
|
+
overlay = current_overlay
|
|
25
|
+
return :pass unless overlay
|
|
26
|
+
|
|
27
|
+
service, book_path = resolve_save_context
|
|
28
|
+
return cancel_and_pass unless service && book_path
|
|
29
|
+
|
|
30
|
+
persist_annotation(overlay, service, book_path)
|
|
31
|
+
:handled
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def cancel_annotation
|
|
35
|
+
close_overlay
|
|
36
|
+
clear_selection
|
|
37
|
+
@ui_controller.set_message('Annotation cancelled', 2)
|
|
38
|
+
:handled
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def cancel_and_pass
|
|
42
|
+
cancel_annotation
|
|
43
|
+
:pass
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def handle_backspace
|
|
47
|
+
overlay = current_overlay
|
|
48
|
+
overlay&.handle_backspace
|
|
49
|
+
:handled
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def handle_enter
|
|
53
|
+
overlay = current_overlay
|
|
54
|
+
overlay&.handle_enter if overlay.respond_to?(:handle_enter)
|
|
55
|
+
:handled
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def handle_character(char)
|
|
59
|
+
overlay = current_overlay
|
|
60
|
+
overlay&.handle_character(char) if overlay.respond_to?(:handle_character)
|
|
61
|
+
:handled
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def current_overlay
|
|
67
|
+
Shoko::Application::Selectors::ReaderSelectors.annotation_editor_overlay(@state)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def resolve_save_context
|
|
71
|
+
service = resolve_annotation_service
|
|
72
|
+
book_path = current_book_path
|
|
73
|
+
[service, book_path]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def current_book_path
|
|
77
|
+
return unless @ui_controller.respond_to?(:current_book_path)
|
|
78
|
+
|
|
79
|
+
@ui_controller.current_book_path
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def persist_annotation(overlay, service, book_path)
|
|
83
|
+
save_overlay(overlay, service, book_path)
|
|
84
|
+
refresh_annotations
|
|
85
|
+
rescue StandardError => e
|
|
86
|
+
@ui_controller.set_message("Save failed: #{e.message}", 3)
|
|
87
|
+
ensure
|
|
88
|
+
close_overlay
|
|
89
|
+
clear_selection
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def save_overlay(overlay, service, book_path)
|
|
93
|
+
if overlay.annotation_id
|
|
94
|
+
service.update(book_path, overlay.annotation_id, overlay.note)
|
|
95
|
+
@ui_controller.set_message('Annotation updated', 2)
|
|
96
|
+
else
|
|
97
|
+
service.add(book_path,
|
|
98
|
+
overlay.selected_text,
|
|
99
|
+
overlay.note,
|
|
100
|
+
overlay.selection_range,
|
|
101
|
+
overlay.chapter_index,
|
|
102
|
+
nil)
|
|
103
|
+
@ui_controller.set_message('Annotation saved!', 2)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def refresh_annotations
|
|
108
|
+
return unless @ui_controller.respond_to?(:refresh_annotations)
|
|
109
|
+
|
|
110
|
+
@ui_controller.refresh_annotations
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def resolve_annotation_service
|
|
114
|
+
return unless @dependencies.respond_to?(:resolve)
|
|
115
|
+
|
|
116
|
+
@dependencies.resolve(:annotation_service)
|
|
117
|
+
rescue StandardError
|
|
118
|
+
nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def close_overlay
|
|
122
|
+
return unless @ui_controller
|
|
123
|
+
|
|
124
|
+
begin
|
|
125
|
+
@ui_controller.send(:close_annotation_editor_overlay)
|
|
126
|
+
rescue StandardError
|
|
127
|
+
nil
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def clear_selection
|
|
132
|
+
@state.dispatch(Shoko::Application::Actions::ClearSelectionAction.new)
|
|
133
|
+
rescue StandardError
|
|
134
|
+
nil
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|