feat: add fingerprint & stable_id, update iOS FFI bindings
This commit is contained in:
parent
f51e913e58
commit
38d6b60937
@ -8,12 +8,13 @@
|
|||||||
<key>BinaryPath</key>
|
<key>BinaryPath</key>
|
||||||
<string>libzx_document_ffi.a</string>
|
<string>libzx_document_ffi.a</string>
|
||||||
<key>LibraryIdentifier</key>
|
<key>LibraryIdentifier</key>
|
||||||
<string>ios-arm64-simulator</string>
|
<string>ios-arm64_x86_64-simulator</string>
|
||||||
<key>LibraryPath</key>
|
<key>LibraryPath</key>
|
||||||
<string>libzx_document_ffi.a</string>
|
<string>libzx_document_ffi.a</string>
|
||||||
<key>SupportedArchitectures</key>
|
<key>SupportedArchitectures</key>
|
||||||
<array>
|
<array>
|
||||||
<string>arm64</string>
|
<string>arm64</string>
|
||||||
|
<string>x86_64</string>
|
||||||
</array>
|
</array>
|
||||||
<key>SupportedPlatform</key>
|
<key>SupportedPlatform</key>
|
||||||
<string>ios</string>
|
<string>ios</string>
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
BIN
bindings/ios/device/libzx_document_ffi.a
Normal file
BIN
bindings/ios/device/libzx_document_ffi.a
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -248,6 +248,11 @@ typedef void (*UniffiForeignFutureCompleteVoid)(uint64_t, UniffiForeignFutureRes
|
|||||||
uint32_t uniffi_zx_document_ffi_fn_func_ack_events_v2(RustBuffer event_ids, RustCallStatus *_Nonnull out_status
|
uint32_t uniffi_zx_document_ffi_fn_func_ack_events_v2(RustBuffer event_ids, RustCallStatus *_Nonnull out_status
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_BUILD_REPRESENTATION_FINGERPRINT
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_BUILD_REPRESENTATION_FINGERPRINT
|
||||||
|
RustBuffer uniffi_zx_document_ffi_fn_func_build_representation_fingerprint(RustBuffer source_fingerprint, RustBuffer renderer_kind, RustBuffer parser_version, uint32_t schema_version, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLEANUP_STALE_SESSIONS_FFI
|
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLEANUP_STALE_SESSIONS_FFI
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLEANUP_STALE_SESSIONS_FFI
|
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLEANUP_STALE_SESSIONS_FFI
|
||||||
uint32_t uniffi_zx_document_ffi_fn_func_cleanup_stale_sessions_ffi(int64_t now_ms, int64_t max_age_ms, RustCallStatus *_Nonnull out_status
|
uint32_t uniffi_zx_document_ffi_fn_func_cleanup_stale_sessions_ffi(int64_t now_ms, int64_t max_age_ms, RustCallStatus *_Nonnull out_status
|
||||||
@ -260,7 +265,12 @@ void uniffi_zx_document_ffi_fn_func_clear_exported_events(uint32_t count, RustCa
|
|||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLOSE_READING_SESSION_V2
|
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLOSE_READING_SESSION_V2
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLOSE_READING_SESSION_V2
|
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLOSE_READING_SESSION_V2
|
||||||
void uniffi_zx_document_ffi_fn_func_close_reading_session_v2(RustBuffer session_id, RustCallStatus *_Nonnull out_status
|
void uniffi_zx_document_ffi_fn_func_close_reading_session_v2(RustBuffer session_id, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_COMPUTE_FILE_FINGERPRINT
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_COMPUTE_FILE_FINGERPRINT
|
||||||
|
RustBuffer uniffi_zx_document_ffi_fn_func_compute_file_fingerprint(RustBuffer file_path, RustCallStatus *_Nonnull out_status
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CREATE_NOTE_ANCHOR
|
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CREATE_NOTE_ANCHOR
|
||||||
@ -292,6 +302,17 @@ RustBuffer uniffi_zx_document_ffi_fn_func_export_pending_events_v2(uint32_t limi
|
|||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_EXTRACT_PDF_TEXT_FFI
|
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_EXTRACT_PDF_TEXT_FFI
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_EXTRACT_PDF_TEXT_FFI
|
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_EXTRACT_PDF_TEXT_FFI
|
||||||
RustBuffer uniffi_zx_document_ffi_fn_func_extract_pdf_text_ffi(RustBuffer file_path, RustCallStatus *_Nonnull out_status
|
RustBuffer uniffi_zx_document_ffi_fn_func_extract_pdf_text_ffi(RustBuffer file_path, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_GET_ACTIVE_SESSION_V2
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_GET_ACTIVE_SESSION_V2
|
||||||
|
RustBuffer uniffi_zx_document_ffi_fn_func_get_active_session_v2(RustBuffer material_id, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_GET_EVENT_BUFFER_STATE_V2
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_GET_EVENT_BUFFER_STATE_V2
|
||||||
|
RustBuffer uniffi_zx_document_ffi_fn_func_get_event_buffer_state_v2(RustCallStatus *_Nonnull out_status
|
||||||
|
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_GET_OFFICE_PREVIEW_CONFIG_FFI
|
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_GET_OFFICE_PREVIEW_CONFIG_FFI
|
||||||
@ -299,6 +320,11 @@ RustBuffer uniffi_zx_document_ffi_fn_func_extract_pdf_text_ffi(RustBuffer file_p
|
|||||||
RustBuffer uniffi_zx_document_ffi_fn_func_get_office_preview_config_ffi(RustBuffer material_type, uint64_t file_size, RustCallStatus *_Nonnull out_status
|
RustBuffer uniffi_zx_document_ffi_fn_func_get_office_preview_config_ffi(RustBuffer material_type, uint64_t file_size, RustCallStatus *_Nonnull out_status
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_GET_SESSION_V2_FFI
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_GET_SESSION_V2_FFI
|
||||||
|
RustBuffer uniffi_zx_document_ffi_fn_func_get_session_v2_ffi(RustBuffer session_id, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_IS_OFFICE_TYPE_FFI
|
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_IS_OFFICE_TYPE_FFI
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_IS_OFFICE_TYPE_FFI
|
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_IS_OFFICE_TYPE_FFI
|
||||||
int8_t uniffi_zx_document_ffi_fn_func_is_office_type_ffi(RustBuffer material_type, RustCallStatus *_Nonnull out_status
|
int8_t uniffi_zx_document_ffi_fn_func_is_office_type_ffi(RustBuffer material_type, RustCallStatus *_Nonnull out_status
|
||||||
@ -309,6 +335,16 @@ int8_t uniffi_zx_document_ffi_fn_func_is_office_type_ffi(RustBuffer material_typ
|
|||||||
uint32_t uniffi_zx_document_ffi_fn_func_mark_events_failed_v2(RustBuffer event_ids, RustCallStatus *_Nonnull out_status
|
uint32_t uniffi_zx_document_ffi_fn_func_mark_events_failed_v2(RustBuffer event_ids, RustCallStatus *_Nonnull out_status
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_MARK_SESSION_FAILED_V2
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_MARK_SESSION_FAILED_V2
|
||||||
|
void uniffi_zx_document_ffi_fn_func_mark_session_failed_v2(RustBuffer session_id, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_MARK_SESSION_INTERRUPTED_V2
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_MARK_SESSION_INTERRUPTED_V2
|
||||||
|
void uniffi_zx_document_ffi_fn_func_mark_session_interrupted_v2(RustBuffer session_id, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PARSE_MARKDOWN
|
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PARSE_MARKDOWN
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PARSE_MARKDOWN
|
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PARSE_MARKDOWN
|
||||||
RustBuffer uniffi_zx_document_ffi_fn_func_parse_markdown(RustBuffer content, RustCallStatus *_Nonnull out_status
|
RustBuffer uniffi_zx_document_ffi_fn_func_parse_markdown(RustBuffer content, RustCallStatus *_Nonnull out_status
|
||||||
@ -324,6 +360,16 @@ RustBuffer uniffi_zx_document_ffi_fn_func_parse_text(RustBuffer content, RustCal
|
|||||||
void uniffi_zx_document_ffi_fn_func_pause_reading_session_v2(RustBuffer session_id, RustCallStatus *_Nonnull out_status
|
void uniffi_zx_document_ffi_fn_func_pause_reading_session_v2(RustBuffer session_id, RustCallStatus *_Nonnull out_status
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_CHECKPOINT_CHANGED_V2
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_CHECKPOINT_CHANGED_V2
|
||||||
|
RustBuffer uniffi_zx_document_ffi_fn_func_push_checkpoint_changed_v2(RustBuffer session_id, RustBuffer material_id, RustBuffer checkpoint, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_HEARTBEAT_CHECKPOINT_V2
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_HEARTBEAT_CHECKPOINT_V2
|
||||||
|
RustBuffer uniffi_zx_document_ffi_fn_func_push_heartbeat_checkpoint_v2(RustBuffer session_id, RustBuffer material_id, uint32_t active_seconds_delta, RustBuffer checkpoint, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_HEARTBEAT_V2
|
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_HEARTBEAT_V2
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_HEARTBEAT_V2
|
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_HEARTBEAT_V2
|
||||||
RustBuffer uniffi_zx_document_ffi_fn_func_push_heartbeat_v2(RustBuffer session_id, RustBuffer material_id, uint32_t active_seconds_delta, RustBuffer position, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status
|
RustBuffer uniffi_zx_document_ffi_fn_func_push_heartbeat_v2(RustBuffer session_id, RustBuffer material_id, uint32_t active_seconds_delta, RustBuffer position, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status
|
||||||
@ -415,6 +461,11 @@ RustBuffer uniffi_zx_document_ffi_fn_func_search_pdf_pages(RustBuffer page_numbe
|
|||||||
RustBuffer uniffi_zx_document_ffi_fn_func_search_text_content(RustBuffer content, RustBuffer query, RustCallStatus *_Nonnull out_status
|
RustBuffer uniffi_zx_document_ffi_fn_func_search_text_content(RustBuffer content, RustBuffer query, RustCallStatus *_Nonnull out_status
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SET_SESSION_CLOSED_AT_MS_V2
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SET_SESSION_CLOSED_AT_MS_V2
|
||||||
|
void uniffi_zx_document_ffi_fn_func_set_session_closed_at_ms_v2(RustBuffer session_id, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_START_READING_SESSION_V2
|
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_START_READING_SESSION_V2
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_START_READING_SESSION_V2
|
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_START_READING_SESSION_V2
|
||||||
RustBuffer uniffi_zx_document_ffi_fn_func_start_reading_session_v2(RustBuffer material, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status
|
RustBuffer uniffi_zx_document_ffi_fn_func_start_reading_session_v2(RustBuffer material, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status
|
||||||
@ -689,6 +740,12 @@ void ffi_zx_document_ffi_rust_future_complete_void(uint64_t handle, RustCallStat
|
|||||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_ACK_EVENTS_V2
|
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_ACK_EVENTS_V2
|
||||||
uint16_t uniffi_zx_document_ffi_checksum_func_ack_events_v2(void
|
uint16_t uniffi_zx_document_ffi_checksum_func_ack_events_v2(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_BUILD_REPRESENTATION_FINGERPRINT
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_BUILD_REPRESENTATION_FINGERPRINT
|
||||||
|
uint16_t uniffi_zx_document_ffi_checksum_func_build_representation_fingerprint(void
|
||||||
|
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CLEANUP_STALE_SESSIONS_FFI
|
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CLEANUP_STALE_SESSIONS_FFI
|
||||||
@ -707,6 +764,12 @@ uint16_t uniffi_zx_document_ffi_checksum_func_clear_exported_events(void
|
|||||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CLOSE_READING_SESSION_V2
|
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CLOSE_READING_SESSION_V2
|
||||||
uint16_t uniffi_zx_document_ffi_checksum_func_close_reading_session_v2(void
|
uint16_t uniffi_zx_document_ffi_checksum_func_close_reading_session_v2(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_COMPUTE_FILE_FINGERPRINT
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_COMPUTE_FILE_FINGERPRINT
|
||||||
|
uint16_t uniffi_zx_document_ffi_checksum_func_compute_file_fingerprint(void
|
||||||
|
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CREATE_NOTE_ANCHOR
|
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CREATE_NOTE_ANCHOR
|
||||||
@ -743,12 +806,30 @@ uint16_t uniffi_zx_document_ffi_checksum_func_export_pending_events_v2(void
|
|||||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_EXTRACT_PDF_TEXT_FFI
|
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_EXTRACT_PDF_TEXT_FFI
|
||||||
uint16_t uniffi_zx_document_ffi_checksum_func_extract_pdf_text_ffi(void
|
uint16_t uniffi_zx_document_ffi_checksum_func_extract_pdf_text_ffi(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_GET_ACTIVE_SESSION_V2
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_GET_ACTIVE_SESSION_V2
|
||||||
|
uint16_t uniffi_zx_document_ffi_checksum_func_get_active_session_v2(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_GET_EVENT_BUFFER_STATE_V2
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_GET_EVENT_BUFFER_STATE_V2
|
||||||
|
uint16_t uniffi_zx_document_ffi_checksum_func_get_event_buffer_state_v2(void
|
||||||
|
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_GET_OFFICE_PREVIEW_CONFIG_FFI
|
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_GET_OFFICE_PREVIEW_CONFIG_FFI
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_GET_OFFICE_PREVIEW_CONFIG_FFI
|
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_GET_OFFICE_PREVIEW_CONFIG_FFI
|
||||||
uint16_t uniffi_zx_document_ffi_checksum_func_get_office_preview_config_ffi(void
|
uint16_t uniffi_zx_document_ffi_checksum_func_get_office_preview_config_ffi(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_GET_SESSION_V2_FFI
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_GET_SESSION_V2_FFI
|
||||||
|
uint16_t uniffi_zx_document_ffi_checksum_func_get_session_v2_ffi(void
|
||||||
|
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_IS_OFFICE_TYPE_FFI
|
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_IS_OFFICE_TYPE_FFI
|
||||||
@ -761,6 +842,18 @@ uint16_t uniffi_zx_document_ffi_checksum_func_is_office_type_ffi(void
|
|||||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_MARK_EVENTS_FAILED_V2
|
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_MARK_EVENTS_FAILED_V2
|
||||||
uint16_t uniffi_zx_document_ffi_checksum_func_mark_events_failed_v2(void
|
uint16_t uniffi_zx_document_ffi_checksum_func_mark_events_failed_v2(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_MARK_SESSION_FAILED_V2
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_MARK_SESSION_FAILED_V2
|
||||||
|
uint16_t uniffi_zx_document_ffi_checksum_func_mark_session_failed_v2(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_MARK_SESSION_INTERRUPTED_V2
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_MARK_SESSION_INTERRUPTED_V2
|
||||||
|
uint16_t uniffi_zx_document_ffi_checksum_func_mark_session_interrupted_v2(void
|
||||||
|
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PARSE_MARKDOWN
|
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PARSE_MARKDOWN
|
||||||
@ -779,6 +872,18 @@ uint16_t uniffi_zx_document_ffi_checksum_func_parse_text(void
|
|||||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PAUSE_READING_SESSION_V2
|
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PAUSE_READING_SESSION_V2
|
||||||
uint16_t uniffi_zx_document_ffi_checksum_func_pause_reading_session_v2(void
|
uint16_t uniffi_zx_document_ffi_checksum_func_pause_reading_session_v2(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_CHECKPOINT_CHANGED_V2
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_CHECKPOINT_CHANGED_V2
|
||||||
|
uint16_t uniffi_zx_document_ffi_checksum_func_push_checkpoint_changed_v2(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_HEARTBEAT_CHECKPOINT_V2
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_HEARTBEAT_CHECKPOINT_V2
|
||||||
|
uint16_t uniffi_zx_document_ffi_checksum_func_push_heartbeat_checkpoint_v2(void
|
||||||
|
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_HEARTBEAT_V2
|
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_HEARTBEAT_V2
|
||||||
@ -887,6 +992,12 @@ uint16_t uniffi_zx_document_ffi_checksum_func_search_pdf_pages(void
|
|||||||
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SEARCH_TEXT_CONTENT
|
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SEARCH_TEXT_CONTENT
|
||||||
uint16_t uniffi_zx_document_ffi_checksum_func_search_text_content(void
|
uint16_t uniffi_zx_document_ffi_checksum_func_search_text_content(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SET_SESSION_CLOSED_AT_MS_V2
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SET_SESSION_CLOSED_AT_MS_V2
|
||||||
|
uint16_t uniffi_zx_document_ffi_checksum_func_set_session_closed_at_ms_v2(void
|
||||||
|
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_START_READING_SESSION_V2
|
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_START_READING_SESSION_V2
|
||||||
|
|||||||
BIN
bindings/ios/simulator/libzx_document_ffi.a
Normal file
BIN
bindings/ios/simulator/libzx_document_ffi.a
Normal file
Binary file not shown.
BIN
bindings/ios/simulator/libzx_document_ffi_arm64.a
Normal file
BIN
bindings/ios/simulator/libzx_document_ffi_arm64.a
Normal file
Binary file not shown.
BIN
bindings/ios/simulator/libzx_document_ffi_x86_64.a
Normal file
BIN
bindings/ios/simulator/libzx_document_ffi_x86_64.a
Normal file
Binary file not shown.
@ -13,6 +13,7 @@ uuid = { version = "1", features = ["v4"] }
|
|||||||
zip = "2"
|
zip = "2"
|
||||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif"] }
|
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif"] }
|
||||||
uniffi = "0.31"
|
uniffi = "0.31"
|
||||||
|
sha2 = "0.10"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
uniffi = { version = "0.31", features = ["build"] }
|
uniffi = { version = "0.31", features = ["build"] }
|
||||||
|
|||||||
@ -56,6 +56,13 @@ pub enum NoteAnchor {
|
|||||||
impl NoteAnchor {
|
impl NoteAnchor {
|
||||||
pub fn from_position(material_id: &str, position: Option<&ReadingPosition>) -> Self {
|
pub fn from_position(material_id: &str, position: Option<&ReadingPosition>) -> Self {
|
||||||
match position {
|
match position {
|
||||||
|
Some(pos @ ReadingPosition::Block { block_id, .. }) => {
|
||||||
|
NoteAnchor::MarkdownBlock {
|
||||||
|
material_id: material_id.to_string(),
|
||||||
|
block_id: block_id.clone(),
|
||||||
|
position_snapshot: Some(pos.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
Some(pos @ ReadingPosition::Markdown { block_id, .. }) => {
|
Some(pos @ ReadingPosition::Markdown { block_id, .. }) => {
|
||||||
NoteAnchor::MarkdownBlock {
|
NoteAnchor::MarkdownBlock {
|
||||||
material_id: material_id.to_string(),
|
material_id: material_id.to_string(),
|
||||||
@ -77,7 +84,17 @@ impl NoteAnchor {
|
|||||||
position_snapshot: Some(pos.clone()),
|
position_snapshot: Some(pos.clone()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(pos @ ReadingPosition::Image { .. }) => NoteAnchor::Image {
|
Some(pos @ ReadingPosition::PdfViewport { page_number, .. }) => {
|
||||||
|
NoteAnchor::PdfPage {
|
||||||
|
material_id: material_id.to_string(),
|
||||||
|
page_number: *page_number,
|
||||||
|
position_snapshot: Some(pos.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(pos @ ReadingPosition::Image { .. })
|
||||||
|
| Some(pos @ ReadingPosition::ImageViewport { .. })
|
||||||
|
| Some(pos @ ReadingPosition::Slide { .. })
|
||||||
|
| Some(pos @ ReadingPosition::Workbook { .. }) => NoteAnchor::Image {
|
||||||
material_id: material_id.to_string(),
|
material_id: material_id.to_string(),
|
||||||
position_snapshot: Some(pos.clone()),
|
position_snapshot: Some(pos.clone()),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2,7 +2,7 @@ use std::sync::Mutex;
|
|||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::progress::ReadingPosition;
|
use crate::progress::{ReadingCheckpoint, ReadingPosition};
|
||||||
use crate::session_v2;
|
use crate::session_v2;
|
||||||
|
|
||||||
const MAX_BUFFER_SIZE: usize = 1000;
|
const MAX_BUFFER_SIZE: usize = 1000;
|
||||||
@ -50,6 +50,7 @@ pub struct ReadingEventV2 {
|
|||||||
pub material_id: String,
|
pub material_id: String,
|
||||||
pub event_type: ReadingEventTypeV2,
|
pub event_type: ReadingEventTypeV2,
|
||||||
pub position: Option<ReadingPosition>,
|
pub position: Option<ReadingPosition>,
|
||||||
|
pub checkpoint: Option<ReadingCheckpoint>,
|
||||||
pub active_seconds_delta: u32,
|
pub active_seconds_delta: u32,
|
||||||
pub timestamp_ms: i64,
|
pub timestamp_ms: i64,
|
||||||
pub sequence: u64,
|
pub sequence: u64,
|
||||||
@ -62,14 +63,16 @@ fn push_event(
|
|||||||
material_id: &str,
|
material_id: &str,
|
||||||
event_type: ReadingEventTypeV2,
|
event_type: ReadingEventTypeV2,
|
||||||
position: Option<ReadingPosition>,
|
position: Option<ReadingPosition>,
|
||||||
|
checkpoint: Option<ReadingCheckpoint>,
|
||||||
active_seconds_delta: u32,
|
active_seconds_delta: u32,
|
||||||
timestamp_ms: i64,
|
timestamp_ms: i64,
|
||||||
) -> Result<ReadingEventV2, session_v2::SessionError> {
|
) -> Result<ReadingEventV2, session_v2::SessionError> {
|
||||||
let seq = session_v2::record_session_event_v2(
|
let seq = session_v2::record_session_event_with_checkpoint_v2(
|
||||||
session_id,
|
session_id,
|
||||||
timestamp_ms,
|
timestamp_ms,
|
||||||
active_seconds_delta,
|
active_seconds_delta,
|
||||||
position.clone(),
|
position.clone(),
|
||||||
|
checkpoint.clone(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let event = ReadingEventV2 {
|
let event = ReadingEventV2 {
|
||||||
@ -78,6 +81,7 @@ fn push_event(
|
|||||||
material_id: material_id.to_string(),
|
material_id: material_id.to_string(),
|
||||||
event_type,
|
event_type,
|
||||||
position: position.map(|p| p.normalized()),
|
position: position.map(|p| p.normalized()),
|
||||||
|
checkpoint: checkpoint.map(|c| c.normalized()),
|
||||||
active_seconds_delta,
|
active_seconds_delta,
|
||||||
timestamp_ms,
|
timestamp_ms,
|
||||||
sequence: seq,
|
sequence: seq,
|
||||||
@ -115,7 +119,7 @@ pub fn push_material_opened_v2(
|
|||||||
material_id: &str,
|
material_id: &str,
|
||||||
timestamp_ms: i64,
|
timestamp_ms: i64,
|
||||||
) -> Result<ReadingEventV2, session_v2::SessionError> {
|
) -> Result<ReadingEventV2, session_v2::SessionError> {
|
||||||
push_event(session_id, material_id, ReadingEventTypeV2::MaterialOpened, None, 0, timestamp_ms)
|
push_event(session_id, material_id, ReadingEventTypeV2::MaterialOpened, None, None, 0, timestamp_ms)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push_material_closed_v2(
|
pub fn push_material_closed_v2(
|
||||||
@ -124,7 +128,7 @@ pub fn push_material_closed_v2(
|
|||||||
active_seconds_delta: u32,
|
active_seconds_delta: u32,
|
||||||
timestamp_ms: i64,
|
timestamp_ms: i64,
|
||||||
) -> Result<ReadingEventV2, session_v2::SessionError> {
|
) -> Result<ReadingEventV2, session_v2::SessionError> {
|
||||||
push_event(session_id, material_id, ReadingEventTypeV2::MaterialClosed, None, active_seconds_delta, timestamp_ms)
|
push_event(session_id, material_id, ReadingEventTypeV2::MaterialClosed, None, None, active_seconds_delta, timestamp_ms)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push_position_changed_v2(
|
pub fn push_position_changed_v2(
|
||||||
@ -133,7 +137,24 @@ pub fn push_position_changed_v2(
|
|||||||
position: ReadingPosition,
|
position: ReadingPosition,
|
||||||
timestamp_ms: i64,
|
timestamp_ms: i64,
|
||||||
) -> Result<ReadingEventV2, session_v2::SessionError> {
|
) -> Result<ReadingEventV2, session_v2::SessionError> {
|
||||||
push_event(session_id, material_id, ReadingEventTypeV2::PositionChanged, Some(position), 0, timestamp_ms)
|
push_event(session_id, material_id, ReadingEventTypeV2::PositionChanged, Some(position), None, 0, timestamp_ms)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_checkpoint_changed_v2(
|
||||||
|
session_id: &str,
|
||||||
|
material_id: &str,
|
||||||
|
checkpoint: ReadingCheckpoint,
|
||||||
|
timestamp_ms: i64,
|
||||||
|
) -> Result<ReadingEventV2, session_v2::SessionError> {
|
||||||
|
push_event(
|
||||||
|
session_id,
|
||||||
|
material_id,
|
||||||
|
ReadingEventTypeV2::PositionChanged,
|
||||||
|
Some(checkpoint.legacy_position()),
|
||||||
|
Some(checkpoint),
|
||||||
|
0,
|
||||||
|
timestamp_ms,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push_heartbeat_v2(
|
pub fn push_heartbeat_v2(
|
||||||
@ -143,7 +164,26 @@ pub fn push_heartbeat_v2(
|
|||||||
position: Option<ReadingPosition>,
|
position: Option<ReadingPosition>,
|
||||||
timestamp_ms: i64,
|
timestamp_ms: i64,
|
||||||
) -> Result<ReadingEventV2, session_v2::SessionError> {
|
) -> Result<ReadingEventV2, session_v2::SessionError> {
|
||||||
push_event(session_id, material_id, ReadingEventTypeV2::Heartbeat, position, active_seconds_delta, timestamp_ms)
|
push_event(session_id, material_id, ReadingEventTypeV2::Heartbeat, position, None, active_seconds_delta, timestamp_ms)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_heartbeat_checkpoint_v2(
|
||||||
|
session_id: &str,
|
||||||
|
material_id: &str,
|
||||||
|
active_seconds_delta: u32,
|
||||||
|
checkpoint: Option<ReadingCheckpoint>,
|
||||||
|
timestamp_ms: i64,
|
||||||
|
) -> Result<ReadingEventV2, session_v2::SessionError> {
|
||||||
|
let legacy_position = checkpoint.as_ref().map(ReadingCheckpoint::legacy_position);
|
||||||
|
push_event(
|
||||||
|
session_id,
|
||||||
|
material_id,
|
||||||
|
ReadingEventTypeV2::Heartbeat,
|
||||||
|
legacy_position,
|
||||||
|
checkpoint,
|
||||||
|
active_seconds_delta,
|
||||||
|
timestamp_ms,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push_marked_as_read_v2(
|
pub fn push_marked_as_read_v2(
|
||||||
@ -151,7 +191,7 @@ pub fn push_marked_as_read_v2(
|
|||||||
material_id: &str,
|
material_id: &str,
|
||||||
timestamp_ms: i64,
|
timestamp_ms: i64,
|
||||||
) -> Result<ReadingEventV2, session_v2::SessionError> {
|
) -> Result<ReadingEventV2, session_v2::SessionError> {
|
||||||
push_event(session_id, material_id, ReadingEventTypeV2::MarkedAsRead, None, 0, timestamp_ms)
|
push_event(session_id, material_id, ReadingEventTypeV2::MarkedAsRead, None, None, 0, timestamp_ms)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Buffer Management (V2 with ack) ──
|
// ── Buffer Management (V2 with ack) ──
|
||||||
|
|||||||
54
crates/zx_document_core/src/fingerprint.rs
Normal file
54
crates/zx_document_core/src/fingerprint.rs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
use crate::error::DocumentError;
|
||||||
|
use crate::progress::RendererKind;
|
||||||
|
|
||||||
|
pub const BLOCK_PARSER_VERSION: &str = "block-parser-v1";
|
||||||
|
pub const WORKBOOK_PARSER_VERSION: &str = "workbook-parser-v1";
|
||||||
|
pub const SLIDE_PARSER_VERSION: &str = "slide-parser-v1";
|
||||||
|
pub const PDF_LOCATOR_SCHEMA_VERSION: u32 = 1;
|
||||||
|
pub const IMAGE_LOCATOR_SCHEMA_VERSION: u32 = 1;
|
||||||
|
|
||||||
|
pub fn compute_file_fingerprint(file_path: &str) -> Result<String, DocumentError> {
|
||||||
|
let bytes = std::fs::read(file_path)?;
|
||||||
|
Ok(hex_sha256(&bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_representation_fingerprint(
|
||||||
|
source_fingerprint: &str,
|
||||||
|
renderer_kind: &RendererKind,
|
||||||
|
parser_version: &str,
|
||||||
|
schema_version: u32,
|
||||||
|
) -> String {
|
||||||
|
let payload = format!(
|
||||||
|
"{source_fingerprint}:{renderer_kind}:{parser_version}:schema-v{schema_version}"
|
||||||
|
);
|
||||||
|
hex_sha256(payload.as_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hex_sha256(bytes: &[u8]) -> String {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(bytes);
|
||||||
|
format!("{:x}", hasher.finalize())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn representation_fingerprint_is_stable() {
|
||||||
|
let a = build_representation_fingerprint("abc", &RendererKind::Block, BLOCK_PARSER_VERSION, 2);
|
||||||
|
let b = build_representation_fingerprint("abc", &RendererKind::Block, BLOCK_PARSER_VERSION, 2);
|
||||||
|
assert_eq!(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn representation_fingerprint_changes_on_inputs() {
|
||||||
|
let base = build_representation_fingerprint("abc", &RendererKind::Block, BLOCK_PARSER_VERSION, 2);
|
||||||
|
assert_ne!(base, build_representation_fingerprint("def", &RendererKind::Block, BLOCK_PARSER_VERSION, 2));
|
||||||
|
assert_ne!(base, build_representation_fingerprint("abc", &RendererKind::Pdf, BLOCK_PARSER_VERSION, 2));
|
||||||
|
assert_ne!(base, build_representation_fingerprint("abc", &RendererKind::Block, "block-parser-v2", 2));
|
||||||
|
assert_ne!(base, build_representation_fingerprint("abc", &RendererKind::Block, BLOCK_PARSER_VERSION, 3));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,6 +7,7 @@ pub mod epub;
|
|||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod events;
|
pub mod events;
|
||||||
pub mod events_v2;
|
pub mod events_v2;
|
||||||
|
pub mod fingerprint;
|
||||||
pub mod image_meta;
|
pub mod image_meta;
|
||||||
pub mod markdown;
|
pub mod markdown;
|
||||||
pub mod material_type;
|
pub mod material_type;
|
||||||
@ -16,5 +17,6 @@ pub mod progress;
|
|||||||
pub mod reading_material;
|
pub mod reading_material;
|
||||||
pub mod search;
|
pub mod search;
|
||||||
pub mod session_v2;
|
pub mod session_v2;
|
||||||
|
pub mod stable_id;
|
||||||
pub mod text;
|
pub mod text;
|
||||||
pub mod time_tracker;
|
pub mod time_tracker;
|
||||||
|
|||||||
@ -1,12 +1,19 @@
|
|||||||
use comrak::nodes::{AstNode, NodeValue};
|
use comrak::nodes::{AstNode, NodeValue};
|
||||||
use comrak::{Arena, ComrakOptions};
|
use comrak::{Arena, ComrakOptions};
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::blocks::DocumentBlock;
|
use crate::blocks::DocumentBlock;
|
||||||
use crate::error::DocumentError;
|
use crate::error::DocumentError;
|
||||||
|
use crate::stable_id::stable_content_id;
|
||||||
|
|
||||||
fn block_id() -> String {
|
/// Generate a stable, deterministic block ID from the block's text content
|
||||||
Uuid::new_v4().to_string()
|
/// and its zero-based position within the document.
|
||||||
|
///
|
||||||
|
/// Uses djb2 — a simple, fixed-algorithm hash — so the same `(text, position)`
|
||||||
|
/// pair always produces the same hex string **across app launches / processes**.
|
||||||
|
/// This is critical for reading-position restore: the saved `lastPosition.blockId`
|
||||||
|
/// must match the id of the same block when the document is re-parsed later.
|
||||||
|
fn block_id(text: &str, position: usize) -> String {
|
||||||
|
stable_content_id("markdown", position, text)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gfm_options() -> ComrakOptions<'static> {
|
fn gfm_options() -> ComrakOptions<'static> {
|
||||||
@ -20,12 +27,14 @@ fn gfm_options() -> ComrakOptions<'static> {
|
|||||||
|
|
||||||
/// Parse a Markdown string into a list of DocumentBlock.
|
/// Parse a Markdown string into a list of DocumentBlock.
|
||||||
pub fn parse_markdown(md_content: &str) -> Result<Vec<DocumentBlock>, DocumentError> {
|
pub fn parse_markdown(md_content: &str) -> Result<Vec<DocumentBlock>, DocumentError> {
|
||||||
|
let start = std::time::Instant::now();
|
||||||
let arena = Arena::new();
|
let arena = Arena::new();
|
||||||
let options = gfm_options();
|
let options = gfm_options();
|
||||||
let root = comrak::parse_document(&arena, md_content, &options);
|
let root = comrak::parse_document(&arena, md_content, &options);
|
||||||
|
|
||||||
let mut blocks = Vec::new();
|
let mut blocks = Vec::new();
|
||||||
collect_blocks(root, &mut blocks);
|
collect_blocks(root, &mut blocks);
|
||||||
|
eprintln!("[Rust] parse_markdown — len={} block_count={} elapsed_ms={}", md_content.len(), blocks.len(), start.elapsed().as_millis());
|
||||||
Ok(blocks)
|
Ok(blocks)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,14 +44,17 @@ fn collect_blocks<'a>(node: &'a AstNode<'a>, blocks: &mut Vec<DocumentBlock>) {
|
|||||||
let value = &data.value;
|
let value = &data.value;
|
||||||
|
|
||||||
let block = match value {
|
let block = match value {
|
||||||
NodeValue::Heading(heading) => Some(DocumentBlock::Heading {
|
NodeValue::Heading(heading) => {
|
||||||
id: block_id(),
|
let text = collect_text(child);
|
||||||
level: heading.level,
|
Some(DocumentBlock::Heading {
|
||||||
text: collect_text(child),
|
id: block_id(&text, blocks.len()),
|
||||||
}),
|
level: heading.level,
|
||||||
|
text,
|
||||||
|
})
|
||||||
|
}
|
||||||
NodeValue::Paragraph => {
|
NodeValue::Paragraph => {
|
||||||
// Check for inline image — extract as standalone Image block
|
// Check for inline image — extract as standalone Image block
|
||||||
if let Some(image_block) = extract_image_from_paragraph(child) {
|
if let Some(image_block) = extract_image_from_paragraph(child, blocks.len()) {
|
||||||
Some(image_block)
|
Some(image_block)
|
||||||
} else {
|
} else {
|
||||||
let text = collect_text(child);
|
let text = collect_text(child);
|
||||||
@ -50,7 +62,7 @@ fn collect_blocks<'a>(node: &'a AstNode<'a>, blocks: &mut Vec<DocumentBlock>) {
|
|||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(DocumentBlock::Paragraph {
|
Some(DocumentBlock::Paragraph {
|
||||||
id: block_id(),
|
id: block_id(&text, blocks.len()),
|
||||||
text,
|
text,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -71,14 +83,14 @@ fn collect_blocks<'a>(node: &'a AstNode<'a>, blocks: &mut Vec<DocumentBlock>) {
|
|||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(DocumentBlock::List {
|
Some(DocumentBlock::List {
|
||||||
id: block_id(),
|
id: block_id(&items.join("\n"), blocks.len()),
|
||||||
ordered: list.list_type == comrak::nodes::ListType::Ordered,
|
ordered: list.list_type == comrak::nodes::ListType::Ordered,
|
||||||
items,
|
items,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NodeValue::CodeBlock(code) => Some(DocumentBlock::CodeBlock {
|
NodeValue::CodeBlock(code) => Some(DocumentBlock::CodeBlock {
|
||||||
id: block_id(),
|
id: block_id(&code.literal, blocks.len()),
|
||||||
language: if code.info.is_empty() {
|
language: if code.info.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
@ -92,7 +104,7 @@ fn collect_blocks<'a>(node: &'a AstNode<'a>, blocks: &mut Vec<DocumentBlock>) {
|
|||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(DocumentBlock::Quote {
|
Some(DocumentBlock::Quote {
|
||||||
id: block_id(),
|
id: block_id(&text, blocks.len()),
|
||||||
text,
|
text,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -124,7 +136,7 @@ fn collect_blocks<'a>(node: &'a AstNode<'a>, blocks: &mut Vec<DocumentBlock>) {
|
|||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(DocumentBlock::Table {
|
Some(DocumentBlock::Table {
|
||||||
id: block_id(),
|
id: block_id(&headers.join("|"), blocks.len()),
|
||||||
headers,
|
headers,
|
||||||
rows,
|
rows,
|
||||||
})
|
})
|
||||||
@ -133,7 +145,7 @@ fn collect_blocks<'a>(node: &'a AstNode<'a>, blocks: &mut Vec<DocumentBlock>) {
|
|||||||
NodeValue::Image(image) => {
|
NodeValue::Image(image) => {
|
||||||
let alt_text = collect_text(child);
|
let alt_text = collect_text(child);
|
||||||
Some(DocumentBlock::Image {
|
Some(DocumentBlock::Image {
|
||||||
id: block_id(),
|
id: block_id(&image.url, blocks.len()),
|
||||||
src: image.url.clone(),
|
src: image.url.clone(),
|
||||||
alt: if alt_text.is_empty() {
|
alt: if alt_text.is_empty() {
|
||||||
None
|
None
|
||||||
@ -143,7 +155,7 @@ fn collect_blocks<'a>(node: &'a AstNode<'a>, blocks: &mut Vec<DocumentBlock>) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
NodeValue::ThematicBreak => Some(DocumentBlock::HorizontalRule {
|
NodeValue::ThematicBreak => Some(DocumentBlock::HorizontalRule {
|
||||||
id: block_id(),
|
id: block_id("---", blocks.len()),
|
||||||
}),
|
}),
|
||||||
// Recurse into containers: Document, Item, TableCell, TableRow, etc.
|
// Recurse into containers: Document, Item, TableCell, TableRow, etc.
|
||||||
NodeValue::HtmlBlock(block) => {
|
NodeValue::HtmlBlock(block) => {
|
||||||
@ -152,7 +164,7 @@ fn collect_blocks<'a>(node: &'a AstNode<'a>, blocks: &mut Vec<DocumentBlock>) {
|
|||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(DocumentBlock::CodeBlock {
|
Some(DocumentBlock::CodeBlock {
|
||||||
id: block_id(),
|
id: block_id(&html, blocks.len()),
|
||||||
language: Some("html".into()),
|
language: Some("html".into()),
|
||||||
code: html,
|
code: html,
|
||||||
})
|
})
|
||||||
@ -163,7 +175,7 @@ fn collect_blocks<'a>(node: &'a AstNode<'a>, blocks: &mut Vec<DocumentBlock>) {
|
|||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(DocumentBlock::CodeBlock {
|
Some(DocumentBlock::CodeBlock {
|
||||||
id: block_id(),
|
id: block_id(&content, blocks.len()),
|
||||||
language: Some("yaml".into()),
|
language: Some("yaml".into()),
|
||||||
code: content.clone(),
|
code: content.clone(),
|
||||||
})
|
})
|
||||||
@ -174,7 +186,7 @@ fn collect_blocks<'a>(node: &'a AstNode<'a>, blocks: &mut Vec<DocumentBlock>) {
|
|||||||
if text.is_empty() {
|
if text.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(DocumentBlock::Paragraph { id: block_id(), text })
|
Some(DocumentBlock::Paragraph { id: block_id(&text, blocks.len()), text })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NodeValue::Document
|
NodeValue::Document
|
||||||
@ -198,7 +210,7 @@ fn collect_blocks<'a>(node: &'a AstNode<'a>, blocks: &mut Vec<DocumentBlock>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a paragraph contains only an image, and extract it.
|
/// Check if a paragraph contains only an image, and extract it.
|
||||||
fn extract_image_from_paragraph<'a>(node: &'a AstNode<'a>) -> Option<DocumentBlock> {
|
fn extract_image_from_paragraph<'a>(node: &'a AstNode<'a>, position: usize) -> Option<DocumentBlock> {
|
||||||
let mut image_node: Option<&AstNode> = None;
|
let mut image_node: Option<&AstNode> = None;
|
||||||
let mut has_other_content = false;
|
let mut has_other_content = false;
|
||||||
|
|
||||||
@ -222,7 +234,7 @@ fn extract_image_from_paragraph<'a>(node: &'a AstNode<'a>) -> Option<DocumentBlo
|
|||||||
if let NodeValue::Image(image) = &data.value {
|
if let NodeValue::Image(image) = &data.value {
|
||||||
let alt_text = collect_text(img);
|
let alt_text = collect_text(img);
|
||||||
return Some(DocumentBlock::Image {
|
return Some(DocumentBlock::Image {
|
||||||
id: block_id(),
|
id: block_id(&image.url, position),
|
||||||
src: image.url.clone(),
|
src: image.url.clone(),
|
||||||
alt: if alt_text.is_empty() {
|
alt: if alt_text.is_empty() {
|
||||||
None
|
None
|
||||||
@ -260,6 +272,19 @@ fn collect_text_inner<'a>(node: &'a AstNode<'a>, buf: &mut String) {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
fn block_id_str(b: &DocumentBlock) -> &str {
|
||||||
|
match b {
|
||||||
|
DocumentBlock::Heading { id, .. } => id,
|
||||||
|
DocumentBlock::Paragraph { id, .. } => id,
|
||||||
|
DocumentBlock::List { id, .. } => id,
|
||||||
|
DocumentBlock::CodeBlock { id, .. } => id,
|
||||||
|
DocumentBlock::Quote { id, .. } => id,
|
||||||
|
DocumentBlock::Table { id, .. } => id,
|
||||||
|
DocumentBlock::Image { id, .. } => id,
|
||||||
|
DocumentBlock::HorizontalRule { id, .. } => id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_empty_markdown() {
|
fn test_empty_markdown() {
|
||||||
let blocks = parse_markdown("").unwrap();
|
let blocks = parse_markdown("").unwrap();
|
||||||
@ -516,6 +541,20 @@ mod tests {
|
|||||||
assert!(blocks.iter().any(|b| matches!(b, DocumentBlock::List { .. })));
|
assert!(blocks.iter().any(|b| matches!(b, DocumentBlock::List { .. })));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_block_ids_are_deterministic_across_parses() {
|
||||||
|
let md = "# Title\n\nHello world.\n\n- item 1\n- item 2\n\n```rust\nfn main() {}\n```\n\n> Quote";
|
||||||
|
let blocks1 = parse_markdown(md).unwrap();
|
||||||
|
let blocks2 = parse_markdown(md).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(blocks1.len(), blocks2.len());
|
||||||
|
for (a, b) in blocks1.iter().zip(blocks2.iter()) {
|
||||||
|
let id_a = block_id_str(a);
|
||||||
|
let id_b = block_id_str(b);
|
||||||
|
assert_eq!(id_a, id_b, "block_id mismatch: {:?} vs {:?}", a, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_all_eight_block_types() {
|
fn test_all_eight_block_types() {
|
||||||
let md = "# H\n\nPara.\n\n- item\n\n```rs\nlet x=1;\n```\n\n> Quote\n\n|A|B|\n|---|---|\n|1|2|\n\n\n\n---";
|
let md = "# H\n\nPara.\n\n- item\n\n```rs\nlet x=1;\n```\n\n> Quote\n\n|A|B|\n|---|---|\n|1|2|\n\n\n\n---";
|
||||||
|
|||||||
@ -2,14 +2,52 @@ use serde::{Deserialize, Serialize, Serializer};
|
|||||||
|
|
||||||
/// Clamp a progress value to 0..1. NaN→0, <0→0, >1→1.
|
/// Clamp a progress value to 0..1. NaN→0, <0→0, >1→1.
|
||||||
pub fn clamp_progress(v: f32) -> f32 {
|
pub fn clamp_progress(v: f32) -> f32 {
|
||||||
if v.is_nan() || v < 0.0 { return 0.0; }
|
if v.is_nan() || v < 0.0 {
|
||||||
if v > 1.0 { return 1.0; }
|
return 0.0;
|
||||||
|
}
|
||||||
|
if v > 1.0 {
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
v
|
v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, uniffi::Enum)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub enum RendererKind {
|
||||||
|
Block,
|
||||||
|
Pdf,
|
||||||
|
Image,
|
||||||
|
Workbook,
|
||||||
|
Slide,
|
||||||
|
Epub,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for RendererKind {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Block => write!(f, "block"),
|
||||||
|
Self::Pdf => write!(f, "pdf"),
|
||||||
|
Self::Image => write!(f, "image"),
|
||||||
|
Self::Workbook => write!(f, "workbook"),
|
||||||
|
Self::Slide => write!(f, "slide"),
|
||||||
|
Self::Epub => write!(f, "epub"),
|
||||||
|
Self::Unknown => write!(f, "unknown"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Deserialize, uniffi::Enum)]
|
#[derive(Debug, Clone, PartialEq, Deserialize, uniffi::Enum)]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
pub enum ReadingPosition {
|
pub enum ReadingPosition {
|
||||||
|
Block {
|
||||||
|
#[serde(rename = "blockId")]
|
||||||
|
block_id: String,
|
||||||
|
#[serde(rename = "blockProgress")]
|
||||||
|
block_progress: f32,
|
||||||
|
#[serde(rename = "overallProgress")]
|
||||||
|
overall_progress: f32,
|
||||||
|
},
|
||||||
Markdown {
|
Markdown {
|
||||||
#[serde(rename = "blockId")]
|
#[serde(rename = "blockId")]
|
||||||
block_id: String,
|
block_id: String,
|
||||||
@ -30,6 +68,18 @@ pub enum ReadingPosition {
|
|||||||
#[serde(rename = "overallProgress")]
|
#[serde(rename = "overallProgress")]
|
||||||
overall_progress: f32,
|
overall_progress: f32,
|
||||||
},
|
},
|
||||||
|
PdfViewport {
|
||||||
|
#[serde(rename = "pageNumber")]
|
||||||
|
page_number: u32,
|
||||||
|
#[serde(rename = "anchorX")]
|
||||||
|
anchor_x: f32,
|
||||||
|
#[serde(rename = "anchorY")]
|
||||||
|
anchor_y: f32,
|
||||||
|
#[serde(rename = "zoomScale")]
|
||||||
|
zoom_scale: f32,
|
||||||
|
#[serde(rename = "overallProgress")]
|
||||||
|
overall_progress: f32,
|
||||||
|
},
|
||||||
Image {
|
Image {
|
||||||
#[serde(rename = "zoomScale")]
|
#[serde(rename = "zoomScale")]
|
||||||
zoom_scale: f32,
|
zoom_scale: f32,
|
||||||
@ -38,6 +88,40 @@ pub enum ReadingPosition {
|
|||||||
#[serde(rename = "offsetY")]
|
#[serde(rename = "offsetY")]
|
||||||
offset_y: f32,
|
offset_y: f32,
|
||||||
},
|
},
|
||||||
|
ImageViewport {
|
||||||
|
#[serde(rename = "zoomScale")]
|
||||||
|
zoom_scale: f32,
|
||||||
|
#[serde(rename = "anchorX")]
|
||||||
|
anchor_x: f32,
|
||||||
|
#[serde(rename = "anchorY")]
|
||||||
|
anchor_y: f32,
|
||||||
|
},
|
||||||
|
Workbook {
|
||||||
|
#[serde(rename = "sheetId")]
|
||||||
|
sheet_id: String,
|
||||||
|
#[serde(rename = "topRow")]
|
||||||
|
top_row: u32,
|
||||||
|
#[serde(rename = "leftColumn")]
|
||||||
|
left_column: u32,
|
||||||
|
#[serde(rename = "selectedCell")]
|
||||||
|
selected_cell: Option<String>,
|
||||||
|
#[serde(rename = "zoomScale")]
|
||||||
|
zoom_scale: f32,
|
||||||
|
#[serde(rename = "overallProgress")]
|
||||||
|
overall_progress: Option<f32>,
|
||||||
|
},
|
||||||
|
Slide {
|
||||||
|
#[serde(rename = "slideIndex")]
|
||||||
|
slide_index: u32,
|
||||||
|
#[serde(rename = "zoomScale")]
|
||||||
|
zoom_scale: f32,
|
||||||
|
#[serde(rename = "anchorX")]
|
||||||
|
anchor_x: f32,
|
||||||
|
#[serde(rename = "anchorY")]
|
||||||
|
anchor_y: f32,
|
||||||
|
#[serde(rename = "overallProgress")]
|
||||||
|
overall_progress: f32,
|
||||||
|
},
|
||||||
Epub {
|
Epub {
|
||||||
#[serde(rename = "chapterId")]
|
#[serde(rename = "chapterId")]
|
||||||
chapter_id: String,
|
chapter_id: String,
|
||||||
@ -49,47 +133,127 @@ pub enum ReadingPosition {
|
|||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manual Serialize to enforce camelCase field names + clamped progress values.
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, uniffi::Record)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ReadingCheckpoint {
|
||||||
|
pub schema_version: u32,
|
||||||
|
pub material_id: String,
|
||||||
|
pub representation_fingerprint: String,
|
||||||
|
pub renderer_kind: RendererKind,
|
||||||
|
pub captured_at_ms: u64,
|
||||||
|
pub device_id: String,
|
||||||
|
pub device_sequence: u64,
|
||||||
|
pub position: ReadingPosition,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReadingCheckpoint {
|
||||||
|
pub fn normalized(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
schema_version: self.schema_version,
|
||||||
|
material_id: self.material_id.clone(),
|
||||||
|
representation_fingerprint: self.representation_fingerprint.clone(),
|
||||||
|
renderer_kind: self.renderer_kind.clone(),
|
||||||
|
captured_at_ms: self.captured_at_ms,
|
||||||
|
device_id: self.device_id.clone(),
|
||||||
|
device_sequence: self.device_sequence,
|
||||||
|
position: self.position.normalized(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn legacy_position(&self) -> ReadingPosition {
|
||||||
|
self.position.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual Serialize to enforce camelCase field names + normalized values.
|
||||||
impl Serialize for ReadingPosition {
|
impl Serialize for ReadingPosition {
|
||||||
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
|
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
|
||||||
use serde::ser::SerializeStruct;
|
use serde::ser::SerializeStruct;
|
||||||
match self {
|
|
||||||
|
match self.normalized() {
|
||||||
|
Self::Block { block_id, block_progress, overall_progress } => {
|
||||||
|
let mut st = s.serialize_struct("Block", 4)?;
|
||||||
|
st.serialize_field("type", "Block")?;
|
||||||
|
st.serialize_field("blockId", &block_id)?;
|
||||||
|
st.serialize_field("blockProgress", &block_progress)?;
|
||||||
|
st.serialize_field("overallProgress", &overall_progress)?;
|
||||||
|
st.end()
|
||||||
|
}
|
||||||
Self::Markdown { block_id, scroll_progress } => {
|
Self::Markdown { block_id, scroll_progress } => {
|
||||||
let mut st = s.serialize_struct("Markdown", 3)?;
|
let mut st = s.serialize_struct("Markdown", 3)?;
|
||||||
st.serialize_field("type", "Markdown")?;
|
st.serialize_field("type", "Markdown")?;
|
||||||
st.serialize_field("blockId", block_id)?;
|
st.serialize_field("blockId", &block_id)?;
|
||||||
st.serialize_field("scrollProgress", &clamp_progress(*scroll_progress))?;
|
st.serialize_field("scrollProgress", &scroll_progress)?;
|
||||||
st.end()
|
st.end()
|
||||||
}
|
}
|
||||||
Self::Text { line_number, scroll_progress } => {
|
Self::Text { line_number, scroll_progress } => {
|
||||||
let mut st = s.serialize_struct("Text", 3)?;
|
let mut st = s.serialize_struct("Text", 3)?;
|
||||||
st.serialize_field("type", "Text")?;
|
st.serialize_field("type", "Text")?;
|
||||||
st.serialize_field("lineNumber", line_number)?;
|
st.serialize_field("lineNumber", &line_number)?;
|
||||||
st.serialize_field("scrollProgress", &clamp_progress(*scroll_progress))?;
|
st.serialize_field("scrollProgress", &scroll_progress)?;
|
||||||
st.end()
|
st.end()
|
||||||
}
|
}
|
||||||
Self::Pdf { page_number, page_progress, overall_progress } => {
|
Self::Pdf { page_number, page_progress, overall_progress } => {
|
||||||
let mut st = s.serialize_struct("Pdf", 4)?;
|
let mut st = s.serialize_struct("Pdf", 4)?;
|
||||||
st.serialize_field("type", "Pdf")?;
|
st.serialize_field("type", "Pdf")?;
|
||||||
st.serialize_field("pageNumber", page_number)?;
|
st.serialize_field("pageNumber", &page_number)?;
|
||||||
st.serialize_field("pageProgress", &clamp_progress(*page_progress))?;
|
st.serialize_field("pageProgress", &page_progress)?;
|
||||||
st.serialize_field("overallProgress", &clamp_progress(*overall_progress))?;
|
st.serialize_field("overallProgress", &overall_progress)?;
|
||||||
|
st.end()
|
||||||
|
}
|
||||||
|
Self::PdfViewport { page_number, anchor_x, anchor_y, zoom_scale, overall_progress } => {
|
||||||
|
let mut st = s.serialize_struct("PdfViewport", 6)?;
|
||||||
|
st.serialize_field("type", "PdfViewport")?;
|
||||||
|
st.serialize_field("pageNumber", &page_number)?;
|
||||||
|
st.serialize_field("anchorX", &anchor_x)?;
|
||||||
|
st.serialize_field("anchorY", &anchor_y)?;
|
||||||
|
st.serialize_field("zoomScale", &zoom_scale)?;
|
||||||
|
st.serialize_field("overallProgress", &overall_progress)?;
|
||||||
st.end()
|
st.end()
|
||||||
}
|
}
|
||||||
Self::Image { zoom_scale, offset_x, offset_y } => {
|
Self::Image { zoom_scale, offset_x, offset_y } => {
|
||||||
let mut st = s.serialize_struct("Image", 4)?;
|
let mut st = s.serialize_struct("Image", 4)?;
|
||||||
st.serialize_field("type", "Image")?;
|
st.serialize_field("type", "Image")?;
|
||||||
st.serialize_field("zoomScale", zoom_scale)?;
|
st.serialize_field("zoomScale", &zoom_scale)?;
|
||||||
st.serialize_field("offsetX", offset_x)?;
|
st.serialize_field("offsetX", &offset_x)?;
|
||||||
st.serialize_field("offsetY", offset_y)?;
|
st.serialize_field("offsetY", &offset_y)?;
|
||||||
|
st.end()
|
||||||
|
}
|
||||||
|
Self::ImageViewport { zoom_scale, anchor_x, anchor_y } => {
|
||||||
|
let mut st = s.serialize_struct("ImageViewport", 4)?;
|
||||||
|
st.serialize_field("type", "ImageViewport")?;
|
||||||
|
st.serialize_field("zoomScale", &zoom_scale)?;
|
||||||
|
st.serialize_field("anchorX", &anchor_x)?;
|
||||||
|
st.serialize_field("anchorY", &anchor_y)?;
|
||||||
|
st.end()
|
||||||
|
}
|
||||||
|
Self::Workbook { sheet_id, top_row, left_column, selected_cell, zoom_scale, overall_progress } => {
|
||||||
|
let mut st = s.serialize_struct("Workbook", 7)?;
|
||||||
|
st.serialize_field("type", "Workbook")?;
|
||||||
|
st.serialize_field("sheetId", &sheet_id)?;
|
||||||
|
st.serialize_field("topRow", &top_row)?;
|
||||||
|
st.serialize_field("leftColumn", &left_column)?;
|
||||||
|
st.serialize_field("selectedCell", &selected_cell)?;
|
||||||
|
st.serialize_field("zoomScale", &zoom_scale)?;
|
||||||
|
st.serialize_field("overallProgress", &overall_progress)?;
|
||||||
|
st.end()
|
||||||
|
}
|
||||||
|
Self::Slide { slide_index, zoom_scale, anchor_x, anchor_y, overall_progress } => {
|
||||||
|
let mut st = s.serialize_struct("Slide", 6)?;
|
||||||
|
st.serialize_field("type", "Slide")?;
|
||||||
|
st.serialize_field("slideIndex", &slide_index)?;
|
||||||
|
st.serialize_field("zoomScale", &zoom_scale)?;
|
||||||
|
st.serialize_field("anchorX", &anchor_x)?;
|
||||||
|
st.serialize_field("anchorY", &anchor_y)?;
|
||||||
|
st.serialize_field("overallProgress", &overall_progress)?;
|
||||||
st.end()
|
st.end()
|
||||||
}
|
}
|
||||||
Self::Epub { chapter_id, chapter_progress, overall_progress } => {
|
Self::Epub { chapter_id, chapter_progress, overall_progress } => {
|
||||||
let mut st = s.serialize_struct("Epub", 4)?;
|
let mut st = s.serialize_struct("Epub", 4)?;
|
||||||
st.serialize_field("type", "Epub")?;
|
st.serialize_field("type", "Epub")?;
|
||||||
st.serialize_field("chapterId", chapter_id)?;
|
st.serialize_field("chapterId", &chapter_id)?;
|
||||||
st.serialize_field("chapterProgress", &clamp_progress(*chapter_progress))?;
|
st.serialize_field("chapterProgress", &chapter_progress)?;
|
||||||
st.serialize_field("overallProgress", &clamp_progress(*overall_progress))?;
|
st.serialize_field("overallProgress", &overall_progress)?;
|
||||||
st.end()
|
st.end()
|
||||||
}
|
}
|
||||||
Self::Unknown => {
|
Self::Unknown => {
|
||||||
@ -105,6 +269,11 @@ impl ReadingPosition {
|
|||||||
/// Return a normalized copy with all progress fields clamped to 0..1.
|
/// Return a normalized copy with all progress fields clamped to 0..1.
|
||||||
pub fn normalized(&self) -> Self {
|
pub fn normalized(&self) -> Self {
|
||||||
match self {
|
match self {
|
||||||
|
Self::Block { block_id, block_progress, overall_progress } => Self::Block {
|
||||||
|
block_id: block_id.clone(),
|
||||||
|
block_progress: clamp_progress(*block_progress),
|
||||||
|
overall_progress: clamp_progress(*overall_progress),
|
||||||
|
},
|
||||||
Self::Markdown { block_id, scroll_progress } => Self::Markdown {
|
Self::Markdown { block_id, scroll_progress } => Self::Markdown {
|
||||||
block_id: block_id.clone(),
|
block_id: block_id.clone(),
|
||||||
scroll_progress: clamp_progress(*scroll_progress),
|
scroll_progress: clamp_progress(*scroll_progress),
|
||||||
@ -118,8 +287,37 @@ impl ReadingPosition {
|
|||||||
page_progress: clamp_progress(*page_progress),
|
page_progress: clamp_progress(*page_progress),
|
||||||
overall_progress: clamp_progress(*overall_progress),
|
overall_progress: clamp_progress(*overall_progress),
|
||||||
},
|
},
|
||||||
|
Self::PdfViewport { page_number, anchor_x, anchor_y, zoom_scale, overall_progress } => Self::PdfViewport {
|
||||||
|
page_number: *page_number,
|
||||||
|
anchor_x: clamp_progress(*anchor_x),
|
||||||
|
anchor_y: clamp_progress(*anchor_y),
|
||||||
|
zoom_scale: *zoom_scale,
|
||||||
|
overall_progress: clamp_progress(*overall_progress),
|
||||||
|
},
|
||||||
Self::Image { zoom_scale, offset_x, offset_y } => Self::Image {
|
Self::Image { zoom_scale, offset_x, offset_y } => Self::Image {
|
||||||
zoom_scale: *zoom_scale, offset_x: *offset_x, offset_y: *offset_y,
|
zoom_scale: *zoom_scale,
|
||||||
|
offset_x: *offset_x,
|
||||||
|
offset_y: *offset_y,
|
||||||
|
},
|
||||||
|
Self::ImageViewport { zoom_scale, anchor_x, anchor_y } => Self::ImageViewport {
|
||||||
|
zoom_scale: *zoom_scale,
|
||||||
|
anchor_x: clamp_progress(*anchor_x),
|
||||||
|
anchor_y: clamp_progress(*anchor_y),
|
||||||
|
},
|
||||||
|
Self::Workbook { sheet_id, top_row, left_column, selected_cell, zoom_scale, overall_progress } => Self::Workbook {
|
||||||
|
sheet_id: sheet_id.clone(),
|
||||||
|
top_row: *top_row,
|
||||||
|
left_column: *left_column,
|
||||||
|
selected_cell: selected_cell.clone(),
|
||||||
|
zoom_scale: *zoom_scale,
|
||||||
|
overall_progress: overall_progress.map(clamp_progress),
|
||||||
|
},
|
||||||
|
Self::Slide { slide_index, zoom_scale, anchor_x, anchor_y, overall_progress } => Self::Slide {
|
||||||
|
slide_index: *slide_index,
|
||||||
|
zoom_scale: *zoom_scale,
|
||||||
|
anchor_x: clamp_progress(*anchor_x),
|
||||||
|
anchor_y: clamp_progress(*anchor_y),
|
||||||
|
overall_progress: clamp_progress(*overall_progress),
|
||||||
},
|
},
|
||||||
Self::Epub { chapter_id, chapter_progress, overall_progress } => Self::Epub {
|
Self::Epub { chapter_id, chapter_progress, overall_progress } => Self::Epub {
|
||||||
chapter_id: chapter_id.clone(),
|
chapter_id: chapter_id.clone(),
|
||||||
@ -135,10 +333,17 @@ impl ReadingPosition {
|
|||||||
use crate::material_type::MaterialType;
|
use crate::material_type::MaterialType;
|
||||||
matches!(
|
matches!(
|
||||||
(self, mt),
|
(self, mt),
|
||||||
(ReadingPosition::Markdown { .. }, MaterialType::Markdown)
|
(ReadingPosition::Block { .. }, MaterialType::Markdown)
|
||||||
|
| (ReadingPosition::Block { .. }, MaterialType::Text)
|
||||||
|
| (ReadingPosition::Block { .. }, MaterialType::Word)
|
||||||
|
| (ReadingPosition::Markdown { .. }, MaterialType::Markdown)
|
||||||
| (ReadingPosition::Text { .. }, MaterialType::Text)
|
| (ReadingPosition::Text { .. }, MaterialType::Text)
|
||||||
| (ReadingPosition::Pdf { .. }, MaterialType::Pdf)
|
| (ReadingPosition::Pdf { .. }, MaterialType::Pdf)
|
||||||
|
| (ReadingPosition::PdfViewport { .. }, MaterialType::Pdf)
|
||||||
| (ReadingPosition::Image { .. }, MaterialType::Image)
|
| (ReadingPosition::Image { .. }, MaterialType::Image)
|
||||||
|
| (ReadingPosition::ImageViewport { .. }, MaterialType::Image)
|
||||||
|
| (ReadingPosition::Workbook { .. }, MaterialType::Excel)
|
||||||
|
| (ReadingPosition::Slide { .. }, MaterialType::PowerPoint)
|
||||||
| (ReadingPosition::Epub { .. }, MaterialType::Epub)
|
| (ReadingPosition::Epub { .. }, MaterialType::Epub)
|
||||||
| (ReadingPosition::Unknown, _)
|
| (ReadingPosition::Unknown, _)
|
||||||
)
|
)
|
||||||
@ -146,11 +351,15 @@ impl ReadingPosition {
|
|||||||
|
|
||||||
pub fn progress_value(&self) -> Option<f32> {
|
pub fn progress_value(&self) -> Option<f32> {
|
||||||
match self {
|
match self {
|
||||||
|
Self::Block { overall_progress, .. } => Some(clamp_progress(*overall_progress)),
|
||||||
Self::Markdown { scroll_progress, .. } => Some(clamp_progress(*scroll_progress)),
|
Self::Markdown { scroll_progress, .. } => Some(clamp_progress(*scroll_progress)),
|
||||||
Self::Text { scroll_progress, .. } => Some(clamp_progress(*scroll_progress)),
|
Self::Text { scroll_progress, .. } => Some(clamp_progress(*scroll_progress)),
|
||||||
Self::Pdf { overall_progress, .. } => Some(clamp_progress(*overall_progress)),
|
Self::Pdf { overall_progress, .. } => Some(clamp_progress(*overall_progress)),
|
||||||
|
Self::PdfViewport { overall_progress, .. } => Some(clamp_progress(*overall_progress)),
|
||||||
|
Self::Workbook { overall_progress, .. } => overall_progress.map(clamp_progress),
|
||||||
|
Self::Slide { overall_progress, .. } => Some(clamp_progress(*overall_progress)),
|
||||||
Self::Epub { overall_progress, .. } => Some(clamp_progress(*overall_progress)),
|
Self::Epub { overall_progress, .. } => Some(clamp_progress(*overall_progress)),
|
||||||
Self::Image { .. } | Self::Unknown => None,
|
Self::Image { .. } | Self::ImageViewport { .. } | Self::Unknown => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -160,154 +369,89 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_markdown_camel_case() {
|
fn test_block_roundtrip() {
|
||||||
|
let pos = ReadingPosition::Block {
|
||||||
|
block_id: "b1".into(),
|
||||||
|
block_progress: 0.2,
|
||||||
|
overall_progress: 0.4,
|
||||||
|
};
|
||||||
|
let json = serde_json::to_string(&pos).unwrap();
|
||||||
|
assert!(json.contains("\"type\":\"Block\""));
|
||||||
|
assert!(json.contains("\"blockId\":\"b1\""));
|
||||||
|
let back: ReadingPosition = serde_json::from_str(&json).unwrap();
|
||||||
|
assert_eq!(back, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_pdf_viewport_clamps_anchor_and_progress() {
|
||||||
|
let pos = ReadingPosition::PdfViewport {
|
||||||
|
page_number: 1,
|
||||||
|
anchor_x: 2.0,
|
||||||
|
anchor_y: -1.0,
|
||||||
|
zoom_scale: 1.5,
|
||||||
|
overall_progress: f32::NAN,
|
||||||
|
};
|
||||||
|
let normalized = pos.normalized();
|
||||||
|
assert_eq!(
|
||||||
|
normalized,
|
||||||
|
ReadingPosition::PdfViewport {
|
||||||
|
page_number: 1,
|
||||||
|
anchor_x: 1.0,
|
||||||
|
anchor_y: 0.0,
|
||||||
|
zoom_scale: 1.5,
|
||||||
|
overall_progress: 0.0,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_legacy_markdown_camel_case() {
|
||||||
let pos = ReadingPosition::Markdown { block_id: "h1".into(), scroll_progress: 0.5 };
|
let pos = ReadingPosition::Markdown { block_id: "h1".into(), scroll_progress: 0.5 };
|
||||||
let json = serde_json::to_string(&pos).unwrap();
|
let json = serde_json::to_string(&pos).unwrap();
|
||||||
eprintln!("SERIALIZED: {json}");
|
assert!(json.contains("\"blockId\":\"h1\""));
|
||||||
assert!(json.contains("\"type\":\"Markdown\""), "missing type: {json}");
|
assert!(json.contains("\"scrollProgress\":0.5"));
|
||||||
assert!(json.contains("\"blockId\":\"h1\""), "missing blockId: {json}");
|
|
||||||
assert!(json.contains("\"scrollProgress\":0.5"), "missing progress: {json}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_pdf_camel_case() {
|
fn test_checkpoint_roundtrip() {
|
||||||
let pos = ReadingPosition::Pdf { page_number: 7, page_progress: 0.8, overall_progress: 0.35 };
|
let checkpoint = ReadingCheckpoint {
|
||||||
let json = serde_json::to_string(&pos).unwrap();
|
schema_version: 2,
|
||||||
assert!(json.contains("\"pageNumber\":7"));
|
material_id: "mat-1".into(),
|
||||||
assert!(json.contains("\"pageProgress\":0.8"));
|
representation_fingerprint: "fp-1".into(),
|
||||||
assert!(json.contains("\"overallProgress\":0.35"));
|
renderer_kind: RendererKind::Block,
|
||||||
|
captured_at_ms: 100,
|
||||||
|
device_id: "device-1".into(),
|
||||||
|
device_sequence: 7,
|
||||||
|
position: ReadingPosition::Block {
|
||||||
|
block_id: "b1".into(),
|
||||||
|
block_progress: 0.25,
|
||||||
|
overall_progress: 0.5,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let json = serde_json::to_string(&checkpoint).unwrap();
|
||||||
|
assert!(json.contains("\"rendererKind\":\"block\""));
|
||||||
|
let back: ReadingCheckpoint = serde_json::from_str(&json).unwrap();
|
||||||
|
assert_eq!(back, checkpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_clamp_nan() {
|
fn test_deserialize_legacy_and_new_json() {
|
||||||
let pos = ReadingPosition::Markdown { block_id: "h1".into(), scroll_progress: f32::NAN };
|
let md_json = r#"{"type":"Markdown","blockId":"h1","scrollProgress":0.5}"#;
|
||||||
let json = serde_json::to_string(&pos).unwrap();
|
let md: ReadingPosition = serde_json::from_str(md_json).unwrap();
|
||||||
assert!(json.contains("\"scrollProgress\":0.0"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_clamp_negative() {
|
|
||||||
let pos = ReadingPosition::Text { line_number: 1, scroll_progress: -0.5 };
|
|
||||||
let json = serde_json::to_string(&pos).unwrap();
|
|
||||||
assert!(json.contains("\"scrollProgress\":0.0"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_clamp_above_one() {
|
|
||||||
let pos = ReadingPosition::Pdf { page_number: 1, page_progress: 2.5, overall_progress: 10.0 };
|
|
||||||
let json = serde_json::to_string(&pos).unwrap();
|
|
||||||
assert!(json.contains("\"pageProgress\":1.0"));
|
|
||||||
assert!(json.contains("\"overallProgress\":1.0"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_is_compatible() {
|
|
||||||
use crate::material_type::MaterialType;
|
|
||||||
let md = ReadingPosition::Markdown { block_id: "h1".into(), scroll_progress: 0.5 };
|
|
||||||
assert!(md.is_compatible_with(&MaterialType::Markdown));
|
|
||||||
assert!(!md.is_compatible_with(&MaterialType::Pdf));
|
|
||||||
assert!(ReadingPosition::Unknown.is_compatible_with(&MaterialType::Pdf));
|
|
||||||
assert!(ReadingPosition::Unknown.is_compatible_with(&MaterialType::Markdown)); // Unknown = compatible with all
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_progress_value() {
|
|
||||||
assert_eq!(ReadingPosition::Unknown.progress_value(), None);
|
|
||||||
let md = ReadingPosition::Markdown { block_id: "h1".into(), scroll_progress: 0.75 };
|
|
||||||
assert_eq!(md.progress_value(), Some(0.75));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_roundtrip() {
|
|
||||||
let positions = vec![
|
|
||||||
ReadingPosition::Markdown { block_id: "h1".into(), scroll_progress: 0.5 },
|
|
||||||
ReadingPosition::Pdf { page_number: 7, page_progress: 0.8, overall_progress: 0.35 },
|
|
||||||
ReadingPosition::Unknown,
|
|
||||||
];
|
|
||||||
for pos in &positions {
|
|
||||||
let json = serde_json::to_string(pos).unwrap();
|
|
||||||
eprintln!("ROUNDTRIP JSON: {json}");
|
|
||||||
let back: ReadingPosition = serde_json::from_str(&json).unwrap();
|
|
||||||
assert_eq!(&back, pos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_deserialize_from_camel_case_json() {
|
|
||||||
// Simulate JSON from iOS app / API server: all camelCase field names
|
|
||||||
let json = r#"{"type":"Markdown","blockId":"h1","scrollProgress":0.5}"#;
|
|
||||||
let pos: ReadingPosition = serde_json::from_str(json).unwrap();
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
pos,
|
md,
|
||||||
ReadingPosition::Markdown { block_id: "h1".into(), scroll_progress: 0.5 }
|
ReadingPosition::Markdown { block_id: "h1".into(), scroll_progress: 0.5 }
|
||||||
);
|
);
|
||||||
|
|
||||||
let json = r#"{"type":"Text","lineNumber":5,"scrollProgress":0.3}"#;
|
let block_json = r#"{"type":"Block","blockId":"b1","blockProgress":0.3,"overallProgress":0.6}"#;
|
||||||
let pos: ReadingPosition = serde_json::from_str(json).unwrap();
|
let block: ReadingPosition = serde_json::from_str(block_json).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
pos,
|
block,
|
||||||
ReadingPosition::Text { line_number: 5, scroll_progress: 0.3 }
|
ReadingPosition::Block {
|
||||||
|
block_id: "b1".into(),
|
||||||
|
block_progress: 0.3,
|
||||||
|
overall_progress: 0.6,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
let json = r#"{"type":"Pdf","pageNumber":3,"pageProgress":0.8,"overallProgress":0.35}"#;
|
|
||||||
let pos: ReadingPosition = serde_json::from_str(json).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
pos,
|
|
||||||
ReadingPosition::Pdf { page_number: 3, page_progress: 0.8, overall_progress: 0.35 }
|
|
||||||
);
|
|
||||||
|
|
||||||
let json = r#"{"type":"Image","zoomScale":1.5,"offsetX":10.0,"offsetY":20.0}"#;
|
|
||||||
let pos: ReadingPosition = serde_json::from_str(json).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
pos,
|
|
||||||
ReadingPosition::Image { zoom_scale: 1.5, offset_x: 10.0, offset_y: 20.0 }
|
|
||||||
);
|
|
||||||
|
|
||||||
let json = r#"{"type":"Epub","chapterId":"ch3","chapterProgress":0.6,"overallProgress":0.3}"#;
|
|
||||||
let pos: ReadingPosition = serde_json::from_str(json).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
pos,
|
|
||||||
ReadingPosition::Epub { chapter_id: "ch3".into(), chapter_progress: 0.6, overall_progress: 0.3 }
|
|
||||||
);
|
|
||||||
|
|
||||||
let json = r#"{"type":"Unknown"}"#;
|
|
||||||
let pos: ReadingPosition = serde_json::from_str(json).unwrap();
|
|
||||||
assert_eq!(pos, ReadingPosition::Unknown);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_deserialize_serialize_roundtrip_all_variants() {
|
|
||||||
// Deserialize from camelCase → re-serialize → deserialize again → verify identical
|
|
||||||
let positions = vec![
|
|
||||||
r#"{"type":"Markdown","blockId":"intro","scrollProgress":0.25}"#,
|
|
||||||
r#"{"type":"Text","lineNumber":42,"scrollProgress":0.5}"#,
|
|
||||||
r#"{"type":"Pdf","pageNumber":7,"pageProgress":0.8,"overallProgress":0.35}"#,
|
|
||||||
r#"{"type":"Image","zoomScale":2.0,"offsetX":0.0,"offsetY":100.0}"#,
|
|
||||||
r#"{"type":"Epub","chapterId":"ch1","chapterProgress":0.6,"overallProgress":0.3}"#,
|
|
||||||
r#"{"type":"Unknown"}"#,
|
|
||||||
];
|
|
||||||
for json_str in &positions {
|
|
||||||
let pos1: ReadingPosition = serde_json::from_str(json_str).unwrap();
|
|
||||||
let re_serialized = serde_json::to_string(&pos1).unwrap();
|
|
||||||
let pos2: ReadingPosition = serde_json::from_str(&re_serialized).unwrap();
|
|
||||||
assert_eq!(pos1, pos2, "roundtrip failed for: {json_str}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_rename_attrs_not_overridden_by_uniffi() {
|
|
||||||
// Verify #[serde(rename)] works correctly alongside #[derive(uniffi::Enum)]:
|
|
||||||
// serde uses renamed camelCase names, not Rust snake_case names.
|
|
||||||
// If uniffi overrode the renames, "blockId" would NOT be recognized.
|
|
||||||
|
|
||||||
// 1. snake_case should FAIL (field is renamed)
|
|
||||||
let json_snake = r#"{"type":"Markdown","block_id":"h1","scroll_progress":0.5}"#;
|
|
||||||
let result: Result<ReadingPosition, _> = serde_json::from_str(json_snake);
|
|
||||||
assert!(result.is_err(), "snake_case should fail: rename=blockId means block_id is unknown");
|
|
||||||
|
|
||||||
// 2. camelCase should SUCCEED
|
|
||||||
let json_camel = r#"{"type":"Markdown","blockId":"h1","scrollProgress":0.5}"#;
|
|
||||||
let result: Result<ReadingPosition, _> = serde_json::from_str(json_camel);
|
|
||||||
assert!(result.is_ok(), "camelCase should succeed: rename=blockId is the expected name");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use crate::progress::ReadingPosition;
|
use crate::progress::{ReadingCheckpoint, ReadingPosition};
|
||||||
use crate::reading_material::ReadingMaterialRefV2;
|
use crate::reading_material::ReadingMaterialRefV2;
|
||||||
use crate::time_tracker::ActiveTimeTracker;
|
use crate::time_tracker::ActiveTimeTracker;
|
||||||
|
|
||||||
@ -36,6 +36,7 @@ pub struct ReadingSessionV2 {
|
|||||||
pub next_sequence: u64,
|
pub next_sequence: u64,
|
||||||
pub total_active_seconds: u32,
|
pub total_active_seconds: u32,
|
||||||
pub last_position: Option<ReadingPosition>,
|
pub last_position: Option<ReadingPosition>,
|
||||||
|
pub last_checkpoint: Option<ReadingCheckpoint>,
|
||||||
pub status: ReadingSessionStatus,
|
pub status: ReadingSessionStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,6 +59,7 @@ pub fn start_reading_session_v2(
|
|||||||
next_sequence: 1,
|
next_sequence: 1,
|
||||||
total_active_seconds: 0,
|
total_active_seconds: 0,
|
||||||
last_position: None,
|
last_position: None,
|
||||||
|
last_checkpoint: None,
|
||||||
status: ReadingSessionStatus::Active,
|
status: ReadingSessionStatus::Active,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -168,13 +170,23 @@ pub fn get_active_session_v2(material_id: &str) -> Option<ReadingSessionV2> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Record an event on this session: bumps sequence and updates last_event_at_ms.
|
|
||||||
/// Returns the sequence number that should be assigned to the event.
|
|
||||||
pub fn record_session_event_v2(
|
pub fn record_session_event_v2(
|
||||||
session_id: &str,
|
session_id: &str,
|
||||||
timestamp_ms: i64,
|
timestamp_ms: i64,
|
||||||
active_seconds_delta: u32,
|
active_seconds_delta: u32,
|
||||||
position: Option<ReadingPosition>,
|
position: Option<ReadingPosition>,
|
||||||
|
) -> Result<u64, SessionError> {
|
||||||
|
record_session_event_with_checkpoint_v2(session_id, timestamp_ms, active_seconds_delta, position, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Record an event on this session: bumps sequence and updates last_event_at_ms.
|
||||||
|
/// Returns the sequence number that should be assigned to the event.
|
||||||
|
pub fn record_session_event_with_checkpoint_v2(
|
||||||
|
session_id: &str,
|
||||||
|
timestamp_ms: i64,
|
||||||
|
active_seconds_delta: u32,
|
||||||
|
position: Option<ReadingPosition>,
|
||||||
|
checkpoint: Option<ReadingCheckpoint>,
|
||||||
) -> Result<u64, SessionError> {
|
) -> Result<u64, SessionError> {
|
||||||
let seq = with_session_mut(session_id, |s| {
|
let seq = with_session_mut(session_id, |s| {
|
||||||
if s.status == ReadingSessionStatus::Closed {
|
if s.status == ReadingSessionStatus::Closed {
|
||||||
@ -186,8 +198,12 @@ pub fn record_session_event_v2(
|
|||||||
let seq = s.next_sequence;
|
let seq = s.next_sequence;
|
||||||
s.next_sequence += 1;
|
s.next_sequence += 1;
|
||||||
s.last_event_at_ms = timestamp_ms;
|
s.last_event_at_ms = timestamp_ms;
|
||||||
if position.is_some() {
|
if let Some(position) = position {
|
||||||
s.last_position = position;
|
s.last_position = Some(position);
|
||||||
|
}
|
||||||
|
if let Some(checkpoint) = checkpoint {
|
||||||
|
s.last_position = Some(checkpoint.legacy_position());
|
||||||
|
s.last_checkpoint = Some(checkpoint);
|
||||||
}
|
}
|
||||||
Ok(seq)
|
Ok(seq)
|
||||||
})?;
|
})?;
|
||||||
@ -318,11 +334,26 @@ impl std::fmt::Display for SessionError {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use std::sync::{Mutex, MutexGuard, OnceLock};
|
||||||
|
|
||||||
fn test_material() -> ReadingMaterialRefV2 {
|
fn test_material() -> ReadingMaterialRefV2 {
|
||||||
ReadingMaterialRefV2::new("test_mat_001", "/tmp/test.md", "test.md", "markdown")
|
ReadingMaterialRefV2::new("test_mat_001", "/tmp/test.md", "test.md", "markdown")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn test_guard() -> MutexGuard<'static, ()> {
|
||||||
|
static GUARD: OnceLock<Mutex<()>> = OnceLock::new();
|
||||||
|
GUARD.get_or_init(|| Mutex::new(())).lock().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset_state() {
|
||||||
|
if let Ok(mut map) = sessions().lock() {
|
||||||
|
map.clear();
|
||||||
|
}
|
||||||
|
if let Ok(mut tmap) = trackers().lock() {
|
||||||
|
tmap.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn teardown(id: &str) {
|
fn teardown(id: &str) {
|
||||||
let _ = close_reading_session_v2(id, 0);
|
let _ = close_reading_session_v2(id, 0);
|
||||||
let _ = remove_session_v2(id);
|
let _ = remove_session_v2(id);
|
||||||
@ -330,6 +361,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_start_session() {
|
fn test_start_session() {
|
||||||
|
let _guard = test_guard();
|
||||||
|
reset_state();
|
||||||
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
||||||
assert!(!id.is_empty());
|
assert!(!id.is_empty());
|
||||||
let s = get_session_v2(&id).unwrap();
|
let s = get_session_v2(&id).unwrap();
|
||||||
@ -341,6 +374,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_pause_resume_close() {
|
fn test_pause_resume_close() {
|
||||||
|
let _guard = test_guard();
|
||||||
|
reset_state();
|
||||||
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
||||||
pause_reading_session_v2(&id).unwrap();
|
pause_reading_session_v2(&id).unwrap();
|
||||||
assert_eq!(get_session_v2(&id).unwrap().status, ReadingSessionStatus::Paused);
|
assert_eq!(get_session_v2(&id).unwrap().status, ReadingSessionStatus::Paused);
|
||||||
@ -353,6 +388,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_remove_refuses_active() {
|
fn test_remove_refuses_active() {
|
||||||
|
let _guard = test_guard();
|
||||||
|
reset_state();
|
||||||
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
||||||
assert!(remove_session_v2(&id).is_err()); // Not closed
|
assert!(remove_session_v2(&id).is_err()); // Not closed
|
||||||
teardown(&id);
|
teardown(&id);
|
||||||
@ -360,6 +397,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_sequence_increments() {
|
fn test_sequence_increments() {
|
||||||
|
let _guard = test_guard();
|
||||||
|
reset_state();
|
||||||
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
||||||
let s1 = record_session_event_v2(&id, 2000, 0, None).unwrap(); // tracker not used for 0 delta
|
let s1 = record_session_event_v2(&id, 2000, 0, None).unwrap(); // tracker not used for 0 delta
|
||||||
let s2 = record_session_event_v2(&id, 3000, 0, None).unwrap();
|
let s2 = record_session_event_v2(&id, 3000, 0, None).unwrap();
|
||||||
@ -370,6 +409,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_closed_rejects_events() {
|
fn test_closed_rejects_events() {
|
||||||
|
let _guard = test_guard();
|
||||||
|
reset_state();
|
||||||
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
||||||
close_reading_session_v2(&id, 0).unwrap();
|
close_reading_session_v2(&id, 0).unwrap();
|
||||||
assert!(record_session_event_v2(&id, 2000, 10, None).is_err());
|
assert!(record_session_event_v2(&id, 2000, 10, None).is_err());
|
||||||
@ -379,6 +420,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_paused_rejects_active_seconds() {
|
fn test_paused_rejects_active_seconds() {
|
||||||
|
let _guard = test_guard();
|
||||||
|
reset_state();
|
||||||
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
||||||
pause_reading_session_v2(&id).unwrap();
|
pause_reading_session_v2(&id).unwrap();
|
||||||
// PositionChanged (delta=0) should be allowed when paused
|
// PositionChanged (delta=0) should be allowed when paused
|
||||||
@ -391,12 +434,16 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_not_found() {
|
fn test_not_found() {
|
||||||
|
let _guard = test_guard();
|
||||||
|
reset_state();
|
||||||
assert!(get_session_v2("nonexistent").is_err());
|
assert!(get_session_v2("nonexistent").is_err());
|
||||||
assert!(close_reading_session_v2("nonexistent", 0).is_err());
|
assert!(close_reading_session_v2("nonexistent", 0).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_two_sessions_independent() {
|
fn test_two_sessions_independent() {
|
||||||
|
let _guard = test_guard();
|
||||||
|
reset_state();
|
||||||
let a = start_reading_session_v2(test_material(), 1000).unwrap();
|
let a = start_reading_session_v2(test_material(), 1000).unwrap();
|
||||||
let b = start_reading_session_v2(ReadingMaterialRefV2::new("mat_b", "/tmp/b.md", "b.md", "markdown"), 2000).unwrap();
|
let b = start_reading_session_v2(ReadingMaterialRefV2::new("mat_b", "/tmp/b.md", "b.md", "markdown"), 2000).unwrap();
|
||||||
assert_ne!(a, b);
|
assert_ne!(a, b);
|
||||||
@ -408,6 +455,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_total_active_seconds_accumulates() {
|
fn test_total_active_seconds_accumulates() {
|
||||||
|
let _guard = test_guard();
|
||||||
|
reset_state();
|
||||||
let id = start_reading_session_v2(test_material(), 0).unwrap();
|
let id = start_reading_session_v2(test_material(), 0).unwrap();
|
||||||
// Simulate iOS calling tick() and passing deltas to session events
|
// Simulate iOS calling tick() and passing deltas to session events
|
||||||
record_session_event_v2(&id, 15_000, 15, None).unwrap();
|
record_session_event_v2(&id, 15_000, 15, None).unwrap();
|
||||||
@ -420,6 +469,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_sequence_and_seconds_independent() {
|
fn test_sequence_and_seconds_independent() {
|
||||||
|
let _guard = test_guard();
|
||||||
|
reset_state();
|
||||||
let a = start_reading_session_v2(test_material(), 0).unwrap();
|
let a = start_reading_session_v2(test_material(), 0).unwrap();
|
||||||
let b = start_reading_session_v2(ReadingMaterialRefV2::new("mat_b", "/tmp/b.md", "b.md", "markdown"), 0).unwrap();
|
let b = start_reading_session_v2(ReadingMaterialRefV2::new("mat_b", "/tmp/b.md", "b.md", "markdown"), 0).unwrap();
|
||||||
record_session_event_v2(&a, 10_000, 10, None).unwrap();
|
record_session_event_v2(&a, 10_000, 10, None).unwrap();
|
||||||
@ -435,6 +486,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_last_position_updated() {
|
fn test_last_position_updated() {
|
||||||
|
let _guard = test_guard();
|
||||||
|
reset_state();
|
||||||
let id = start_reading_session_v2(test_material(), 0).unwrap();
|
let id = start_reading_session_v2(test_material(), 0).unwrap();
|
||||||
let pos1 = ReadingPosition::Markdown { block_id: "intro".into(), scroll_progress: 0.3 };
|
let pos1 = ReadingPosition::Markdown { block_id: "intro".into(), scroll_progress: 0.3 };
|
||||||
let pos2 = ReadingPosition::Markdown { block_id: "ch1".into(), scroll_progress: 0.8 };
|
let pos2 = ReadingPosition::Markdown { block_id: "ch1".into(), scroll_progress: 0.8 };
|
||||||
@ -446,6 +499,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cleanup_stale_removes_old_sessions() {
|
fn test_cleanup_stale_removes_old_sessions() {
|
||||||
|
let _guard = test_guard();
|
||||||
|
reset_state();
|
||||||
// Old session: last event at t=1000
|
// Old session: last event at t=1000
|
||||||
let old = start_reading_session_v2(test_material(), 0).unwrap();
|
let old = start_reading_session_v2(test_material(), 0).unwrap();
|
||||||
record_session_event_v2(&old, 1000, 0, None).unwrap();
|
record_session_event_v2(&old, 1000, 0, None).unwrap();
|
||||||
@ -468,6 +523,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cleanup_stale_removes_orphaned_active_session() {
|
fn test_cleanup_stale_removes_orphaned_active_session() {
|
||||||
|
let _guard = test_guard();
|
||||||
|
reset_state();
|
||||||
// Simulate a crash: active session not closed, last event at t=5000
|
// Simulate a crash: active session not closed, last event at t=5000
|
||||||
let orphaned = start_reading_session_v2(test_material(), 0).unwrap();
|
let orphaned = start_reading_session_v2(test_material(), 0).unwrap();
|
||||||
record_session_event_v2(&orphaned, 5000, 10, None).unwrap();
|
record_session_event_v2(&orphaned, 5000, 10, None).unwrap();
|
||||||
@ -482,6 +539,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cleanup_stale_zero_when_all_recent() {
|
fn test_cleanup_stale_zero_when_all_recent() {
|
||||||
|
let _guard = test_guard();
|
||||||
|
reset_state();
|
||||||
let active = start_reading_session_v2(test_material(), 0).unwrap();
|
let active = start_reading_session_v2(test_material(), 0).unwrap();
|
||||||
// Last event at t=9500, now at t=10000, max_age=1000
|
// Last event at t=9500, now at t=10000, max_age=1000
|
||||||
// 10000-9500=500 < 1000 → NOT stale
|
// 10000-9500=500 < 1000 → NOT stale
|
||||||
@ -498,6 +557,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_mark_interrupted() {
|
fn test_mark_interrupted() {
|
||||||
|
let _guard = test_guard();
|
||||||
|
reset_state();
|
||||||
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
||||||
mark_session_interrupted_v2(&id).unwrap();
|
mark_session_interrupted_v2(&id).unwrap();
|
||||||
assert_eq!(get_session_v2(&id).unwrap().status, ReadingSessionStatus::Interrupted);
|
assert_eq!(get_session_v2(&id).unwrap().status, ReadingSessionStatus::Interrupted);
|
||||||
@ -508,6 +569,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_mark_failed() {
|
fn test_mark_failed() {
|
||||||
|
let _guard = test_guard();
|
||||||
|
reset_state();
|
||||||
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
||||||
mark_session_failed_v2(&id).unwrap();
|
mark_session_failed_v2(&id).unwrap();
|
||||||
assert_eq!(get_session_v2(&id).unwrap().status, ReadingSessionStatus::Failed);
|
assert_eq!(get_session_v2(&id).unwrap().status, ReadingSessionStatus::Failed);
|
||||||
@ -518,6 +581,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_closed_rejects_interrupted() {
|
fn test_closed_rejects_interrupted() {
|
||||||
|
let _guard = test_guard();
|
||||||
|
reset_state();
|
||||||
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
||||||
close_reading_session_v2(&id, 0).unwrap();
|
close_reading_session_v2(&id, 0).unwrap();
|
||||||
assert!(mark_session_interrupted_v2(&id).is_err());
|
assert!(mark_session_interrupted_v2(&id).is_err());
|
||||||
@ -527,6 +592,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_close_sets_closed_at_ms() {
|
fn test_close_sets_closed_at_ms() {
|
||||||
|
let _guard = test_guard();
|
||||||
|
reset_state();
|
||||||
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
||||||
assert!(get_session_v2(&id).unwrap().closed_at_ms.is_none());
|
assert!(get_session_v2(&id).unwrap().closed_at_ms.is_none());
|
||||||
close_reading_session_v2(&id, 5000).unwrap();
|
close_reading_session_v2(&id, 5000).unwrap();
|
||||||
@ -536,6 +603,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_set_closed_at_ms_override() {
|
fn test_set_closed_at_ms_override() {
|
||||||
|
let _guard = test_guard();
|
||||||
|
reset_state();
|
||||||
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
||||||
close_reading_session_v2(&id, 5000).unwrap();
|
close_reading_session_v2(&id, 5000).unwrap();
|
||||||
// set_session_closed_at_ms can override if needed
|
// set_session_closed_at_ms can override if needed
|
||||||
@ -546,6 +615,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_get_active_session_by_material() {
|
fn test_get_active_session_by_material() {
|
||||||
|
let _guard = test_guard();
|
||||||
|
reset_state();
|
||||||
let mat_a = ReadingMaterialRefV2::new("mat_a", "/tmp/a.md", "a.md", "markdown");
|
let mat_a = ReadingMaterialRefV2::new("mat_a", "/tmp/a.md", "a.md", "markdown");
|
||||||
let mat_b = ReadingMaterialRefV2::new("mat_b", "/tmp/b.md", "b.md", "markdown");
|
let mat_b = ReadingMaterialRefV2::new("mat_b", "/tmp/b.md", "b.md", "markdown");
|
||||||
|
|
||||||
@ -565,6 +636,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_session_does_not_store_business_fields() {
|
fn test_session_does_not_store_business_fields() {
|
||||||
|
let _guard = test_guard();
|
||||||
|
reset_state();
|
||||||
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
||||||
let s = get_session_v2(&id).unwrap();
|
let s = get_session_v2(&id).unwrap();
|
||||||
// Verify the material ref has no business fields
|
// Verify the material ref has no business fields
|
||||||
@ -576,6 +649,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_sequence_monotonic_in_session() {
|
fn test_sequence_monotonic_in_session() {
|
||||||
|
let _guard = test_guard();
|
||||||
|
reset_state();
|
||||||
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
let id = start_reading_session_v2(test_material(), 1000).unwrap();
|
||||||
let s1 = record_session_event_v2(&id, 2000, 0, None).unwrap();
|
let s1 = record_session_event_v2(&id, 2000, 0, None).unwrap();
|
||||||
let s2 = record_session_event_v2(&id, 3000, 0, None).unwrap();
|
let s2 = record_session_event_v2(&id, 3000, 0, None).unwrap();
|
||||||
|
|||||||
32
crates/zx_document_core/src/stable_id.rs
Normal file
32
crates/zx_document_core/src/stable_id.rs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
pub fn stable_content_id(namespace: &str, index: usize, content: &str) -> String {
|
||||||
|
let mut hash: u64 = 5381;
|
||||||
|
|
||||||
|
for byte in namespace
|
||||||
|
.bytes()
|
||||||
|
.chain(index.to_string().bytes())
|
||||||
|
.chain(content.bytes())
|
||||||
|
{
|
||||||
|
hash = ((hash << 5).wrapping_add(hash)).wrapping_add(byte as u64);
|
||||||
|
}
|
||||||
|
|
||||||
|
format!("{namespace}_{hash:016x}")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::stable_content_id;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stable_content_id_is_deterministic() {
|
||||||
|
let a = stable_content_id("text", 3, "hello");
|
||||||
|
let b = stable_content_id("text", 3, "hello");
|
||||||
|
assert_eq!(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stable_content_id_changes_with_namespace_and_index() {
|
||||||
|
let base = stable_content_id("text", 3, "hello");
|
||||||
|
assert_ne!(base, stable_content_id("markdown", 3, "hello"));
|
||||||
|
assert_ne!(base, stable_content_id("text", 4, "hello"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
use crate::blocks::DocumentBlock;
|
use crate::blocks::DocumentBlock;
|
||||||
use crate::error::DocumentError;
|
use crate::error::DocumentError;
|
||||||
use uuid::Uuid;
|
use crate::stable_id::stable_content_id;
|
||||||
|
|
||||||
/// Read a TXT file and return its content as paragraph blocks.
|
/// Read a TXT file and return its content as paragraph blocks.
|
||||||
pub fn read_text(file_path: &str) -> Result<Vec<DocumentBlock>, DocumentError> {
|
pub fn read_text(file_path: &str) -> Result<Vec<DocumentBlock>, DocumentError> {
|
||||||
@ -18,11 +18,12 @@ pub fn parse_text_content(content: &str) -> Vec<DocumentBlock> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
blocks.push(DocumentBlock::Paragraph {
|
blocks.push(DocumentBlock::Paragraph {
|
||||||
id: Uuid::new_v4().to_string(),
|
id: stable_content_id("text", blocks.len(), &text),
|
||||||
text,
|
text,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eprintln!("[Rust] parse_text — len={} block_count={}", content.len(), blocks.len());
|
||||||
blocks
|
blocks
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,4 +92,11 @@ mod tests {
|
|||||||
let result = read_text("/nonexistent/file.txt");
|
let result = read_text("/nonexistent/file.txt");
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ids_are_stable() {
|
||||||
|
let first = parse_text_content("Hello\n\nWorld");
|
||||||
|
let second = parse_text_content("Hello\n\nWorld");
|
||||||
|
assert_eq!(first, second);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ pub use zx_document_core::image_meta::ImageMeta;
|
|||||||
pub use zx_document_core::text::TextStats;
|
pub use zx_document_core::text::TextStats;
|
||||||
pub use zx_document_core::search::SearchResult;
|
pub use zx_document_core::search::SearchResult;
|
||||||
pub use zx_document_core::anchors::NoteAnchor;
|
pub use zx_document_core::anchors::NoteAnchor;
|
||||||
pub use zx_document_core::progress::ReadingPosition;
|
pub use zx_document_core::progress::{ReadingCheckpoint, ReadingPosition, RendererKind};
|
||||||
pub use zx_document_core::events::ReadingEvent;
|
pub use zx_document_core::events::ReadingEvent;
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
pub use zx_document_core::reading_material::ReadingMaterialRef;
|
pub use zx_document_core::reading_material::ReadingMaterialRef;
|
||||||
@ -26,39 +26,42 @@ use zx_document_core::blocks as core_blocks;
|
|||||||
// ── V2 Reading Session FFI ──
|
// ── V2 Reading Session FFI ──
|
||||||
|
|
||||||
#[uniffi::export]
|
#[uniffi::export]
|
||||||
fn start_reading_session_v2(material: ReadingMaterialRefV2, timestamp_ms: i64) -> Result<String, String> {
|
fn start_reading_session_v2(material: ReadingMaterialRefV2, timestamp_ms: i64) -> Result<String, DocumentError> {
|
||||||
zx_document_core::session_v2::start_reading_session_v2(material, timestamp_ms)
|
let r = zx_document_core::session_v2::start_reading_session_v2(material, timestamp_ms)
|
||||||
.map_err(|e| e.to_string())
|
.map_err(Into::into);
|
||||||
|
match &r { Ok(sid) => eprintln!("[Rust] start_session OK — sid={}", sid), Err(e) => eprintln!("[Rust] start_session FAILED — {:?}", e) }
|
||||||
|
r
|
||||||
}
|
}
|
||||||
|
|
||||||
#[uniffi::export]
|
#[uniffi::export]
|
||||||
fn pause_reading_session_v2(session_id: String) -> Result<(), String> {
|
fn pause_reading_session_v2(session_id: String) -> Result<(), DocumentError> {
|
||||||
zx_document_core::session_v2::pause_reading_session_v2(&session_id).map_err(|e| e.to_string())
|
zx_document_core::session_v2::pause_reading_session_v2(&session_id).map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[uniffi::export]
|
#[uniffi::export]
|
||||||
fn resume_reading_session_v2(session_id: String) -> Result<(), String> {
|
fn resume_reading_session_v2(session_id: String) -> Result<(), DocumentError> {
|
||||||
zx_document_core::session_v2::resume_reading_session_v2(&session_id).map_err(|e| e.to_string())
|
zx_document_core::session_v2::resume_reading_session_v2(&session_id).map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[uniffi::export]
|
#[uniffi::export]
|
||||||
fn close_reading_session_v2(session_id: String, timestamp_ms: i64) -> Result<(), String> {
|
fn close_reading_session_v2(session_id: String, timestamp_ms: i64) -> Result<(), DocumentError> {
|
||||||
zx_document_core::session_v2::close_reading_session_v2(&session_id, timestamp_ms).map_err(|e| e.to_string())
|
eprintln!("[Rust] close_session — sid={}", session_id);
|
||||||
|
zx_document_core::session_v2::close_reading_session_v2(&session_id, timestamp_ms).map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[uniffi::export]
|
#[uniffi::export]
|
||||||
fn set_session_closed_at_ms_v2(session_id: String, timestamp_ms: i64) -> Result<(), String> {
|
fn set_session_closed_at_ms_v2(session_id: String, timestamp_ms: i64) -> Result<(), DocumentError> {
|
||||||
zx_document_core::session_v2::set_session_closed_at_ms(&session_id, timestamp_ms).map_err(|e| e.to_string())
|
zx_document_core::session_v2::set_session_closed_at_ms(&session_id, timestamp_ms).map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[uniffi::export]
|
#[uniffi::export]
|
||||||
fn mark_session_interrupted_v2(session_id: String) -> Result<(), String> {
|
fn mark_session_interrupted_v2(session_id: String) -> Result<(), DocumentError> {
|
||||||
zx_document_core::session_v2::mark_session_interrupted_v2(&session_id).map_err(|e| e.to_string())
|
zx_document_core::session_v2::mark_session_interrupted_v2(&session_id).map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[uniffi::export]
|
#[uniffi::export]
|
||||||
fn mark_session_failed_v2(session_id: String) -> Result<(), String> {
|
fn mark_session_failed_v2(session_id: String) -> Result<(), DocumentError> {
|
||||||
zx_document_core::session_v2::mark_session_failed_v2(&session_id).map_err(|e| e.to_string())
|
zx_document_core::session_v2::mark_session_failed_v2(&session_id).map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[uniffi::export]
|
#[uniffi::export]
|
||||||
@ -74,45 +77,59 @@ fn get_session_v2_ffi(session_id: String) -> Option<ReadingSessionV2> {
|
|||||||
// ── V2 Reading Event FFI ──
|
// ── V2 Reading Event FFI ──
|
||||||
|
|
||||||
#[uniffi::export]
|
#[uniffi::export]
|
||||||
fn push_material_opened_v2(session_id: String, material_id: String, timestamp_ms: i64) -> Result<ReadingEventV2, String> {
|
fn push_material_opened_v2(session_id: String, material_id: String, timestamp_ms: i64) -> Result<ReadingEventV2, DocumentError> {
|
||||||
zx_document_core::events_v2::push_material_opened_v2(&session_id, &material_id, timestamp_ms)
|
eprintln!("[Rust] push_material_opened — mid={}", material_id);
|
||||||
.map_err(|e| e.to_string())
|
zx_document_core::events_v2::push_material_opened_v2(&session_id, &material_id, timestamp_ms).map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[uniffi::export]
|
#[uniffi::export]
|
||||||
fn push_material_closed_v2(session_id: String, material_id: String, active_seconds_delta: u32, timestamp_ms: i64) -> Result<ReadingEventV2, String> {
|
fn push_material_closed_v2(session_id: String, material_id: String, active_seconds_delta: u32, timestamp_ms: i64) -> Result<ReadingEventV2, DocumentError> {
|
||||||
zx_document_core::events_v2::push_material_closed_v2(&session_id, &material_id, active_seconds_delta, timestamp_ms)
|
eprintln!("[Rust] push_material_closed — mid={} active_s={}", material_id, active_seconds_delta);
|
||||||
.map_err(|e| e.to_string())
|
zx_document_core::events_v2::push_material_closed_v2(&session_id, &material_id, active_seconds_delta, timestamp_ms).map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[uniffi::export]
|
#[uniffi::export]
|
||||||
fn push_position_changed_v2(session_id: String, material_id: String, position: ReadingPosition, timestamp_ms: i64) -> Result<ReadingEventV2, String> {
|
fn push_position_changed_v2(session_id: String, material_id: String, position: ReadingPosition, timestamp_ms: i64) -> Result<ReadingEventV2, DocumentError> {
|
||||||
zx_document_core::events_v2::push_position_changed_v2(&session_id, &material_id, position, timestamp_ms)
|
eprintln!("[Rust] push_position_changed — mid={}", material_id);
|
||||||
.map_err(|e| e.to_string())
|
zx_document_core::events_v2::push_position_changed_v2(&session_id, &material_id, position, timestamp_ms).map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[uniffi::export]
|
#[uniffi::export]
|
||||||
fn push_heartbeat_v2(session_id: String, material_id: String, active_seconds_delta: u32, position: Option<ReadingPosition>, timestamp_ms: i64) -> Result<ReadingEventV2, String> {
|
fn push_heartbeat_v2(session_id: String, material_id: String, active_seconds_delta: u32, position: Option<ReadingPosition>, timestamp_ms: i64) -> Result<ReadingEventV2, DocumentError> {
|
||||||
zx_document_core::events_v2::push_heartbeat_v2(&session_id, &material_id, active_seconds_delta, position, timestamp_ms)
|
zx_document_core::events_v2::push_heartbeat_v2(&session_id, &material_id, active_seconds_delta, position, timestamp_ms).map_err(Into::into)
|
||||||
.map_err(|e| e.to_string())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[uniffi::export]
|
#[uniffi::export]
|
||||||
fn push_marked_as_read_v2(session_id: String, material_id: String, timestamp_ms: i64) -> Result<ReadingEventV2, String> {
|
fn push_checkpoint_changed_v2(session_id: String, material_id: String, checkpoint: ReadingCheckpoint, timestamp_ms: i64) -> Result<ReadingEventV2, DocumentError> {
|
||||||
zx_document_core::events_v2::push_marked_as_read_v2(&session_id, &material_id, timestamp_ms)
|
eprintln!("[Rust] push_checkpoint_changed — mid={}", material_id);
|
||||||
.map_err(|e| e.to_string())
|
zx_document_core::events_v2::push_checkpoint_changed_v2(&session_id, &material_id, checkpoint, timestamp_ms).map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[uniffi::export]
|
||||||
|
fn push_heartbeat_checkpoint_v2(session_id: String, material_id: String, active_seconds_delta: u32, checkpoint: Option<ReadingCheckpoint>, timestamp_ms: i64) -> Result<ReadingEventV2, DocumentError> {
|
||||||
|
zx_document_core::events_v2::push_heartbeat_checkpoint_v2(&session_id, &material_id, active_seconds_delta, checkpoint, timestamp_ms).map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[uniffi::export]
|
||||||
|
fn push_marked_as_read_v2(session_id: String, material_id: String, timestamp_ms: i64) -> Result<ReadingEventV2, DocumentError> {
|
||||||
|
eprintln!("[Rust] push_marked_as_read — mid={}", material_id);
|
||||||
|
zx_document_core::events_v2::push_marked_as_read_v2(&session_id, &material_id, timestamp_ms).map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── V2 Buffer Management FFI ──
|
// ── V2 Buffer Management FFI ──
|
||||||
|
|
||||||
#[uniffi::export]
|
#[uniffi::export]
|
||||||
fn export_pending_events_v2(limit: u32, timestamp_ms: i64) -> Vec<ReadingEventV2> {
|
fn export_pending_events_v2(limit: u32, timestamp_ms: i64) -> Vec<ReadingEventV2> {
|
||||||
zx_document_core::events_v2::export_pending_events_v2(limit, timestamp_ms)
|
let events = zx_document_core::events_v2::export_pending_events_v2(limit, timestamp_ms);
|
||||||
|
if !events.is_empty() { eprintln!("[Rust] export_events — count={}", events.len()); }
|
||||||
|
events
|
||||||
}
|
}
|
||||||
|
|
||||||
#[uniffi::export]
|
#[uniffi::export]
|
||||||
fn reload_stale_events_v2() -> u32 {
|
fn reload_stale_events_v2() -> u32 {
|
||||||
zx_document_core::events_v2::reload_stale_events_v2()
|
let n = zx_document_core::events_v2::reload_stale_events_v2();
|
||||||
|
if n > 0 { eprintln!("[Rust] reload_stale — count={}", n); }
|
||||||
|
n
|
||||||
}
|
}
|
||||||
|
|
||||||
#[uniffi::export]
|
#[uniffi::export]
|
||||||
@ -122,14 +139,26 @@ fn get_event_buffer_state_v2() -> EventBufferStateV2 {
|
|||||||
|
|
||||||
#[uniffi::export]
|
#[uniffi::export]
|
||||||
fn ack_events_v2(event_ids: Vec<String>) -> u32 {
|
fn ack_events_v2(event_ids: Vec<String>) -> u32 {
|
||||||
|
eprintln!("[Rust] ack_events — count={}", event_ids.len());
|
||||||
zx_document_core::events_v2::ack_events_v2(&event_ids)
|
zx_document_core::events_v2::ack_events_v2(&event_ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[uniffi::export]
|
#[uniffi::export]
|
||||||
fn mark_events_failed_v2(event_ids: Vec<String>) -> u32 {
|
fn mark_events_failed_v2(event_ids: Vec<String>) -> u32 {
|
||||||
|
eprintln!("[Rust] mark_failed — count={}", event_ids.len());
|
||||||
zx_document_core::events_v2::mark_events_failed_v2(&event_ids)
|
zx_document_core::events_v2::mark_events_failed_v2(&event_ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[uniffi::export]
|
||||||
|
fn compute_file_fingerprint(file_path: String) -> Result<String, DocumentError> {
|
||||||
|
zx_document_core::fingerprint::compute_file_fingerprint(&file_path).map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[uniffi::export]
|
||||||
|
fn build_representation_fingerprint(source_fingerprint: String, renderer_kind: RendererKind, parser_version: String, schema_version: u32) -> String {
|
||||||
|
zx_document_core::fingerprint::build_representation_fingerprint(&source_fingerprint, &renderer_kind, &parser_version, schema_version)
|
||||||
|
}
|
||||||
|
|
||||||
// FFI-compatible DocumentBlock (tuple variants, UniFFI proc-macro)
|
// FFI-compatible DocumentBlock (tuple variants, UniFFI proc-macro)
|
||||||
#[derive(Debug, uniffi::Enum)]
|
#[derive(Debug, uniffi::Enum)]
|
||||||
pub enum DocumentBlock {
|
pub enum DocumentBlock {
|
||||||
@ -181,6 +210,11 @@ pub enum DocumentError {
|
|||||||
ParseError,
|
ParseError,
|
||||||
InvalidEncoding,
|
InvalidEncoding,
|
||||||
IoError,
|
IoError,
|
||||||
|
SessionNotFound,
|
||||||
|
SessionAlreadyActive,
|
||||||
|
SessionNotActive,
|
||||||
|
BufferOverflow,
|
||||||
|
Unsupported,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for DocumentError {
|
impl std::fmt::Display for DocumentError {
|
||||||
@ -191,6 +225,11 @@ impl std::fmt::Display for DocumentError {
|
|||||||
Self::ParseError => write!(f, "Parse error"),
|
Self::ParseError => write!(f, "Parse error"),
|
||||||
Self::InvalidEncoding => write!(f, "Invalid encoding"),
|
Self::InvalidEncoding => write!(f, "Invalid encoding"),
|
||||||
Self::IoError => write!(f, "IO error"),
|
Self::IoError => write!(f, "IO error"),
|
||||||
|
Self::SessionNotFound => write!(f, "Session not found"),
|
||||||
|
Self::SessionAlreadyActive => write!(f, "Session already active"),
|
||||||
|
Self::SessionNotActive => write!(f, "Session not active"),
|
||||||
|
Self::BufferOverflow => write!(f, "Buffer overflow"),
|
||||||
|
Self::Unsupported => write!(f, "Unsupported"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -209,6 +248,20 @@ impl From<zx_document_core::error::DocumentError> for DocumentError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<zx_document_core::session_v2::SessionError> for DocumentError {
|
||||||
|
fn from(e: zx_document_core::session_v2::SessionError) -> Self {
|
||||||
|
match e {
|
||||||
|
zx_document_core::session_v2::SessionError::NotFound => Self::SessionNotFound,
|
||||||
|
zx_document_core::session_v2::SessionError::AlreadyClosed
|
||||||
|
| zx_document_core::session_v2::SessionError::AlreadyInterrupted
|
||||||
|
| zx_document_core::session_v2::SessionError::AlreadyFailed => Self::SessionAlreadyActive,
|
||||||
|
zx_document_core::session_v2::SessionError::NotActive
|
||||||
|
| zx_document_core::session_v2::SessionError::NotClosed => Self::SessionNotActive,
|
||||||
|
zx_document_core::session_v2::SessionError::LockPoisoned => Self::Unsupported,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[uniffi::export]
|
#[uniffi::export]
|
||||||
fn detect_material_type(file_path: String) -> Result<MaterialType, DocumentError> {
|
fn detect_material_type(file_path: String) -> Result<MaterialType, DocumentError> {
|
||||||
zx_document_core::material_type::detect_material_type(&file_path).map_err(Into::into)
|
zx_document_core::material_type::detect_material_type(&file_path).map_err(Into::into)
|
||||||
|
|||||||
@ -49,7 +49,7 @@ namespace zx_document {
|
|||||||
|
|
||||||
// ── V2 Reading Session ──
|
// ── V2 Reading Session ──
|
||||||
[Throws=DocumentError]
|
[Throws=DocumentError]
|
||||||
string start_reading_session_v2(ReadingMaterialRef material, i64 timestamp_ms);
|
string start_reading_session_v2(ReadingMaterialRefV2 material, i64 timestamp_ms);
|
||||||
|
|
||||||
[Throws=DocumentError]
|
[Throws=DocumentError]
|
||||||
void pause_reading_session_v2([ByRef] string session_id);
|
void pause_reading_session_v2([ByRef] string session_id);
|
||||||
@ -58,7 +58,20 @@ namespace zx_document {
|
|||||||
void resume_reading_session_v2([ByRef] string session_id);
|
void resume_reading_session_v2([ByRef] string session_id);
|
||||||
|
|
||||||
[Throws=DocumentError]
|
[Throws=DocumentError]
|
||||||
void close_reading_session_v2([ByRef] string session_id);
|
void close_reading_session_v2([ByRef] string session_id, i64 timestamp_ms);
|
||||||
|
|
||||||
|
ReadingSessionV2? get_active_session_v2([ByRef] string material_id);
|
||||||
|
|
||||||
|
ReadingSessionV2? get_session_v2_ffi([ByRef] string session_id);
|
||||||
|
|
||||||
|
[Throws=DocumentError]
|
||||||
|
void mark_session_interrupted_v2([ByRef] string session_id);
|
||||||
|
|
||||||
|
[Throws=DocumentError]
|
||||||
|
void mark_session_failed_v2([ByRef] string session_id);
|
||||||
|
|
||||||
|
[Throws=DocumentError]
|
||||||
|
void set_session_closed_at_ms_v2([ByRef] string session_id, i64 timestamp_ms);
|
||||||
|
|
||||||
// ── V2 Reading Events ──
|
// ── V2 Reading Events ──
|
||||||
[Throws=DocumentError]
|
[Throws=DocumentError]
|
||||||
@ -73,6 +86,12 @@ namespace zx_document {
|
|||||||
[Throws=DocumentError]
|
[Throws=DocumentError]
|
||||||
ReadingEventV2 push_heartbeat_v2([ByRef] string session_id, [ByRef] string material_id, u32 active_seconds_delta, ReadingPosition? position, i64 timestamp_ms);
|
ReadingEventV2 push_heartbeat_v2([ByRef] string session_id, [ByRef] string material_id, u32 active_seconds_delta, ReadingPosition? position, i64 timestamp_ms);
|
||||||
|
|
||||||
|
[Throws=DocumentError]
|
||||||
|
ReadingEventV2 push_checkpoint_changed_v2([ByRef] string session_id, [ByRef] string material_id, ReadingCheckpoint checkpoint, i64 timestamp_ms);
|
||||||
|
|
||||||
|
[Throws=DocumentError]
|
||||||
|
ReadingEventV2 push_heartbeat_checkpoint_v2([ByRef] string session_id, [ByRef] string material_id, u32 active_seconds_delta, ReadingCheckpoint? checkpoint, i64 timestamp_ms);
|
||||||
|
|
||||||
[Throws=DocumentError]
|
[Throws=DocumentError]
|
||||||
ReadingEventV2 push_marked_as_read_v2([ByRef] string session_id, [ByRef] string material_id, i64 timestamp_ms);
|
ReadingEventV2 push_marked_as_read_v2([ByRef] string session_id, [ByRef] string material_id, i64 timestamp_ms);
|
||||||
|
|
||||||
@ -83,8 +102,15 @@ namespace zx_document {
|
|||||||
|
|
||||||
u32 mark_events_failed_v2(sequence<string> event_ids);
|
u32 mark_events_failed_v2(sequence<string> event_ids);
|
||||||
|
|
||||||
|
[Throws=DocumentError]
|
||||||
|
string compute_file_fingerprint([ByRef] string file_path);
|
||||||
|
|
||||||
|
string build_representation_fingerprint([ByRef] string source_fingerprint, RendererKind renderer_kind, [ByRef] string parser_version, u32 schema_version);
|
||||||
|
|
||||||
u32 reload_stale_events_v2();
|
u32 reload_stale_events_v2();
|
||||||
|
|
||||||
|
EventBufferStateV2 get_event_buffer_state_v2();
|
||||||
|
|
||||||
// ── V1 (deprecated) ──
|
// ── V1 (deprecated) ──
|
||||||
void push_reading_event(ReadingEvent event);
|
void push_reading_event(ReadingEvent event);
|
||||||
void update_reading_position([ByRef] string material_id, ReadingPosition position);
|
void update_reading_position([ByRef] string material_id, ReadingPosition position);
|
||||||
@ -99,6 +125,11 @@ enum DocumentError {
|
|||||||
"ParseError",
|
"ParseError",
|
||||||
"InvalidEncoding",
|
"InvalidEncoding",
|
||||||
"IoError",
|
"IoError",
|
||||||
|
"SessionNotFound",
|
||||||
|
"SessionAlreadyActive",
|
||||||
|
"SessionNotActive",
|
||||||
|
"BufferOverflow",
|
||||||
|
"Unsupported",
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── V2 Types ──
|
// ── V2 Types ──
|
||||||
@ -107,24 +138,46 @@ dictionary ReadingMaterialRef {
|
|||||||
string material_id;
|
string material_id;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
dictionary ReadingMaterialRefV2 {
|
||||||
|
string material_id;
|
||||||
|
string file_path;
|
||||||
|
string file_name;
|
||||||
|
string file_type;
|
||||||
|
string? mime_type;
|
||||||
|
u64? file_size;
|
||||||
|
string? content_hash;
|
||||||
|
i64? created_at_ms;
|
||||||
|
};
|
||||||
|
|
||||||
[Enum]
|
[Enum]
|
||||||
interface ReadingSessionStatus {
|
interface ReadingSessionStatus {
|
||||||
Active();
|
Active();
|
||||||
Paused();
|
Paused();
|
||||||
Closed();
|
Closed();
|
||||||
|
Interrupted();
|
||||||
|
Failed();
|
||||||
};
|
};
|
||||||
|
|
||||||
dictionary ReadingSessionV2 {
|
dictionary ReadingSessionV2 {
|
||||||
string client_session_id;
|
string client_session_id;
|
||||||
ReadingMaterialRef material;
|
ReadingMaterialRefV2 material;
|
||||||
i64 started_at_ms;
|
i64 started_at_ms;
|
||||||
|
i64? closed_at_ms;
|
||||||
i64 last_event_at_ms;
|
i64 last_event_at_ms;
|
||||||
u64 next_sequence;
|
u64 next_sequence;
|
||||||
u32 total_active_seconds;
|
u32 total_active_seconds;
|
||||||
ReadingPosition? last_position;
|
ReadingPosition? last_position;
|
||||||
|
ReadingCheckpoint? last_checkpoint;
|
||||||
ReadingSessionStatus status;
|
ReadingSessionStatus status;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
dictionary EventBufferStateV2 {
|
||||||
|
u32 total;
|
||||||
|
u32 pending;
|
||||||
|
u32 exported;
|
||||||
|
u32 failed;
|
||||||
|
};
|
||||||
|
|
||||||
[Enum]
|
[Enum]
|
||||||
interface ReadingEventTypeV2 {
|
interface ReadingEventTypeV2 {
|
||||||
MaterialOpened();
|
MaterialOpened();
|
||||||
@ -140,11 +193,34 @@ dictionary ReadingEventV2 {
|
|||||||
string material_id;
|
string material_id;
|
||||||
ReadingEventTypeV2 event_type;
|
ReadingEventTypeV2 event_type;
|
||||||
ReadingPosition? position;
|
ReadingPosition? position;
|
||||||
|
ReadingCheckpoint? checkpoint;
|
||||||
u32 active_seconds_delta;
|
u32 active_seconds_delta;
|
||||||
i64 timestamp_ms;
|
i64 timestamp_ms;
|
||||||
u64 sequence;
|
u64 sequence;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
[Enum]
|
||||||
|
interface RendererKind {
|
||||||
|
Block();
|
||||||
|
Pdf();
|
||||||
|
Image();
|
||||||
|
Workbook();
|
||||||
|
Slide();
|
||||||
|
Epub();
|
||||||
|
Unknown();
|
||||||
|
};
|
||||||
|
|
||||||
|
dictionary ReadingCheckpoint {
|
||||||
|
u32 schema_version;
|
||||||
|
string material_id;
|
||||||
|
string representation_fingerprint;
|
||||||
|
RendererKind renderer_kind;
|
||||||
|
u64 captured_at_ms;
|
||||||
|
string device_id;
|
||||||
|
u64 device_sequence;
|
||||||
|
ReadingPosition position;
|
||||||
|
};
|
||||||
|
|
||||||
[Enum]
|
[Enum]
|
||||||
interface MaterialType {
|
interface MaterialType {
|
||||||
Markdown();
|
Markdown();
|
||||||
@ -188,10 +264,15 @@ dictionary DocumentInfo {
|
|||||||
|
|
||||||
[Enum]
|
[Enum]
|
||||||
interface ReadingPosition {
|
interface ReadingPosition {
|
||||||
|
Block(string block_id, f32 block_progress, f32 overall_progress);
|
||||||
Markdown(string block_id, f32 scroll_progress);
|
Markdown(string block_id, f32 scroll_progress);
|
||||||
Text(u32 line_number, f32 scroll_progress);
|
Text(u32 line_number, f32 scroll_progress);
|
||||||
Pdf(u32 page_number, f32 page_progress, f32 overall_progress);
|
Pdf(u32 page_number, f32 page_progress, f32 overall_progress);
|
||||||
|
PdfViewport(u32 page_number, f32 anchor_x, f32 anchor_y, f32 zoom_scale, f32 overall_progress);
|
||||||
Image(f32 zoom_scale, f32 offset_x, f32 offset_y);
|
Image(f32 zoom_scale, f32 offset_x, f32 offset_y);
|
||||||
|
ImageViewport(f32 zoom_scale, f32 anchor_x, f32 anchor_y);
|
||||||
|
Workbook(string sheet_id, u32 top_row, u32 left_column, string? selected_cell, f32 zoom_scale, f32? overall_progress);
|
||||||
|
Slide(u32 slide_index, f32 zoom_scale, f32 anchor_x, f32 anchor_y, f32 overall_progress);
|
||||||
Epub(string chapter_id, f32 chapter_progress, f32 overall_progress);
|
Epub(string chapter_id, f32 chapter_progress, f32 overall_progress);
|
||||||
Unknown();
|
Unknown();
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,7 +7,7 @@ RUST_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|||||||
OUT_DIR="$RUST_DIR/bindings/ios"
|
OUT_DIR="$RUST_DIR/bindings/ios"
|
||||||
LIB_NAME="libzx_document_ffi.a"
|
LIB_NAME="libzx_document_ffi.a"
|
||||||
|
|
||||||
rustup target add aarch64-apple-ios aarch64-apple-ios-sim
|
rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios
|
||||||
|
|
||||||
echo "==> Building for iOS device (arm64)..."
|
echo "==> Building for iOS device (arm64)..."
|
||||||
cargo build --release --target aarch64-apple-ios -p zx_document_ffi
|
cargo build --release --target aarch64-apple-ios -p zx_document_ffi
|
||||||
@ -15,11 +15,21 @@ cargo build --release --target aarch64-apple-ios -p zx_document_ffi
|
|||||||
echo "==> Building for iOS simulator (arm64)..."
|
echo "==> Building for iOS simulator (arm64)..."
|
||||||
cargo build --release --target aarch64-apple-ios-sim -p zx_document_ffi
|
cargo build --release --target aarch64-apple-ios-sim -p zx_document_ffi
|
||||||
|
|
||||||
|
echo "==> Building for iOS simulator (x86_64)..."
|
||||||
|
cargo build --release --target x86_64-apple-ios -p zx_document_ffi
|
||||||
|
|
||||||
rm -rf "$OUT_DIR/ZxDocumentRuntime.xcframework"
|
rm -rf "$OUT_DIR/ZxDocumentRuntime.xcframework"
|
||||||
mkdir -p "$OUT_DIR/device" "$OUT_DIR/simulator" "$OUT_DIR/generated"
|
mkdir -p "$OUT_DIR/device" "$OUT_DIR/simulator" "$OUT_DIR/generated"
|
||||||
|
|
||||||
cp "target/aarch64-apple-ios/release/$LIB_NAME" "$OUT_DIR/device/"
|
cp "target/aarch64-apple-ios/release/$LIB_NAME" "$OUT_DIR/device/"
|
||||||
cp "target/aarch64-apple-ios-sim/release/$LIB_NAME" "$OUT_DIR/simulator/"
|
cp "target/aarch64-apple-ios-sim/release/$LIB_NAME" "$OUT_DIR/simulator/libzx_document_ffi_arm64.a"
|
||||||
|
cp "target/x86_64-apple-ios/release/$LIB_NAME" "$OUT_DIR/simulator/libzx_document_ffi_x86_64.a"
|
||||||
|
|
||||||
|
echo "==> Creating universal iOS simulator static library..."
|
||||||
|
lipo -create \
|
||||||
|
"$OUT_DIR/simulator/libzx_document_ffi_arm64.a" \
|
||||||
|
"$OUT_DIR/simulator/libzx_document_ffi_x86_64.a" \
|
||||||
|
-output "$OUT_DIR/simulator/$LIB_NAME"
|
||||||
|
|
||||||
echo "==> Generating Swift bindings (UDL bindgen)..."
|
echo "==> Generating Swift bindings (UDL bindgen)..."
|
||||||
rm -f "$OUT_DIR/generated/zx_document.swift"
|
rm -f "$OUT_DIR/generated/zx_document.swift"
|
||||||
|
|||||||
@ -38,6 +38,27 @@ check uniffi_zx_document_ffi_fn_func_update_reading_position
|
|||||||
check uniffi_zx_document_ffi_fn_func_export_pending_events
|
check uniffi_zx_document_ffi_fn_func_export_pending_events
|
||||||
check uniffi_zx_document_ffi_fn_func_clear_exported_events
|
check uniffi_zx_document_ffi_fn_func_clear_exported_events
|
||||||
|
|
||||||
|
# V2 session lifecycle
|
||||||
|
check uniffi_zx_document_ffi_fn_func_start_reading_session_v2
|
||||||
|
check uniffi_zx_document_ffi_fn_func_pause_reading_session_v2
|
||||||
|
check uniffi_zx_document_ffi_fn_func_resume_reading_session_v2
|
||||||
|
check uniffi_zx_document_ffi_fn_func_close_reading_session_v2
|
||||||
|
|
||||||
|
# V2 event push
|
||||||
|
check uniffi_zx_document_ffi_fn_func_push_material_opened_v2
|
||||||
|
check uniffi_zx_document_ffi_fn_func_push_material_closed_v2
|
||||||
|
check uniffi_zx_document_ffi_fn_func_push_position_changed_v2
|
||||||
|
check uniffi_zx_document_ffi_fn_func_push_heartbeat_v2
|
||||||
|
check uniffi_zx_document_ffi_fn_func_push_marked_as_read_v2
|
||||||
|
|
||||||
|
# V2 buffer management
|
||||||
|
check uniffi_zx_document_ffi_fn_func_export_pending_events_v2
|
||||||
|
check uniffi_zx_document_ffi_fn_func_ack_events_v2
|
||||||
|
check uniffi_zx_document_ffi_fn_func_mark_events_failed_v2
|
||||||
|
check uniffi_zx_document_ffi_fn_func_reload_stale_events_v2
|
||||||
|
check uniffi_zx_document_ffi_fn_func_get_event_buffer_state_v2
|
||||||
|
check uniffi_zx_document_ffi_fn_func_cleanup_stale_sessions_ffi
|
||||||
|
|
||||||
# Buffer management
|
# Buffer management
|
||||||
check ffi_zx_document_ffi_rustbuffer_alloc
|
check ffi_zx_document_ffi_rustbuffer_alloc
|
||||||
check ffi_zx_document_ffi_rustbuffer_from_bytes
|
check ffi_zx_document_ffi_rustbuffer_from_bytes
|
||||||
|
|||||||
@ -12,11 +12,13 @@ c = c.replace(
|
|||||||
"fileprivate extension Data {\n init(rustBuffer: RustBuffer) {\n let buf = UnsafeRawBufferPointer(start: rustBuffer.data, count: Int(rustBuffer.len))\n self.init(buf)\n }\n}"
|
"fileprivate extension Data {\n init(rustBuffer: RustBuffer) {\n let buf = UnsafeRawBufferPointer(start: rustBuffer.data, count: Int(rustBuffer.len))\n self.init(buf)\n }\n}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Patch 2: Fix UnsafeMutableRawPointer cast for iOS 26 SDK
|
# Patch 2: removed — `RustBuffer.data` is now `UnsafeMutablePointer<UInt8>?` on current SDKs,
|
||||||
c = c.replace(
|
# so `.assumingMemoryBound(to:)` is not needed (and would cause a compile error).
|
||||||
"let bytes = UnsafeBufferPointer<UInt8>(start: value.data!, count: Int(value.len))",
|
# Kept as comment in case an older SDK needs the raw-pointer cast.
|
||||||
"let bytes = UnsafeBufferPointer<UInt8>(start: value.data!.assumingMemoryBound(to: UInt8.self), count: Int(value.len))"
|
# c = c.replace(
|
||||||
)
|
# "let bytes = UnsafeBufferPointer<UInt8>(start: value.data!, count: Int(value.len))",
|
||||||
|
# "let bytes = UnsafeBufferPointer<UInt8>(start: value.data!.assumingMemoryBound(to: UInt8.self), count: Int(value.len))"
|
||||||
|
# )
|
||||||
|
|
||||||
# Patch 3: Remove checksum initialization
|
# Patch 3: Remove checksum initialization
|
||||||
# UniFFI 0.28 UDL scaffolding does not export checksum functions as #[no_mangle],
|
# UniFFI 0.28 UDL scaffolding does not export checksum functions as #[no_mangle],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user