refactor: extract editor command persistence runtime
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
use crate::editor_runtime::persistence::{persist_document_state, PersistedDocumentIdentity};
|
||||
use leptos::prelude::{GetUntracked, ReadSignal, Set, Update, WriteSignal};
|
||||
use leptos_tiptap::{TiptapContent, TiptapEditorHandle};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
pub(crate) fn read_editor_snapshot(editor: TiptapEditorHandle) -> (String, Value, String) {
|
||||
let html = editor
|
||||
.get_html()
|
||||
.unwrap_or_else(|err| format!("读取 HTML 失败:{err}"));
|
||||
let json_value = editor.get_json().unwrap_or_else(|err| {
|
||||
json!({
|
||||
"type": "error",
|
||||
"message": format!("读取 JSON 失败:{err}"),
|
||||
})
|
||||
});
|
||||
let json_text = serde_json::to_string_pretty(&json_value)
|
||||
.unwrap_or_else(|err| format!("格式化 JSON 失败:{err}"));
|
||||
(html, json_value, json_text)
|
||||
}
|
||||
|
||||
pub(crate) fn sync_editor_outputs(
|
||||
editor: TiptapEditorHandle,
|
||||
set_html_output: WriteSignal<String>,
|
||||
set_document_json: WriteSignal<Value>,
|
||||
set_json_output: WriteSignal<String>,
|
||||
) -> Value {
|
||||
let (html, json_value, json_text) = read_editor_snapshot(editor);
|
||||
set_html_output.set(html);
|
||||
set_document_json.set(json_value.clone());
|
||||
set_json_output.set(json_text);
|
||||
json_value
|
||||
}
|
||||
|
||||
pub(crate) fn apply_document_update(
|
||||
editor: TiptapEditorHandle,
|
||||
persisted_identity: &PersistedDocumentIdentity,
|
||||
next_document: Value,
|
||||
set_dirty_count: WriteSignal<u32>,
|
||||
set_html_output: WriteSignal<String>,
|
||||
set_document_json: WriteSignal<Value>,
|
||||
set_json_output: WriteSignal<String>,
|
||||
title: ReadSignal<String>,
|
||||
set_command_feedback: WriteSignal<String>,
|
||||
success_message: impl Into<String>,
|
||||
) {
|
||||
let success_message = success_message.into();
|
||||
let next_payload = next_document
|
||||
.get("content")
|
||||
.and_then(Value::as_array)
|
||||
.cloned()
|
||||
.map(Value::Array)
|
||||
.unwrap_or_else(|| next_document.clone());
|
||||
match editor.set_content(TiptapContent::json(next_payload)) {
|
||||
Ok(()) => {
|
||||
persist_current_editor_snapshot(
|
||||
editor,
|
||||
persisted_identity,
|
||||
set_dirty_count,
|
||||
set_html_output,
|
||||
set_document_json,
|
||||
set_json_output,
|
||||
title,
|
||||
set_command_feedback,
|
||||
success_message,
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
set_command_feedback.set(format!("更新文档失败:{err}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn sync_persisted_editor_command(
|
||||
editor: TiptapEditorHandle,
|
||||
persisted_identity: &PersistedDocumentIdentity,
|
||||
set_dirty_count: WriteSignal<u32>,
|
||||
set_html_output: WriteSignal<String>,
|
||||
set_document_json: WriteSignal<Value>,
|
||||
set_json_output: WriteSignal<String>,
|
||||
title: ReadSignal<String>,
|
||||
set_command_feedback: WriteSignal<String>,
|
||||
success_message: impl Into<String>,
|
||||
) {
|
||||
persist_current_editor_snapshot(
|
||||
editor,
|
||||
persisted_identity,
|
||||
set_dirty_count,
|
||||
set_html_output,
|
||||
set_document_json,
|
||||
set_json_output,
|
||||
title,
|
||||
set_command_feedback,
|
||||
success_message.into(),
|
||||
);
|
||||
}
|
||||
|
||||
fn persist_current_editor_snapshot(
|
||||
editor: TiptapEditorHandle,
|
||||
persisted_identity: &PersistedDocumentIdentity,
|
||||
set_dirty_count: WriteSignal<u32>,
|
||||
set_html_output: WriteSignal<String>,
|
||||
set_document_json: WriteSignal<Value>,
|
||||
set_json_output: WriteSignal<String>,
|
||||
title: ReadSignal<String>,
|
||||
set_command_feedback: WriteSignal<String>,
|
||||
success_message: String,
|
||||
) {
|
||||
set_dirty_count.update(|count| *count += 1);
|
||||
let (html, snapshot, json_text) = read_editor_snapshot(editor);
|
||||
set_html_output.set(html.clone());
|
||||
set_document_json.set(snapshot.clone());
|
||||
set_json_output.set(json_text);
|
||||
match persist_document_state(
|
||||
persisted_identity,
|
||||
&title.get_untracked(),
|
||||
&snapshot,
|
||||
Some(html),
|
||||
) {
|
||||
Ok(()) => set_command_feedback.set(format!("{success_message},并已写入本地草稿")),
|
||||
Err(err) => set_command_feedback.set(format!("{success_message},但本地保存失败:{err}")),
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,4 @@
|
||||
pub(crate) mod attachment_links;
|
||||
pub(crate) mod command_sync;
|
||||
pub(crate) mod history_safe_commands;
|
||||
pub(crate) mod persistence;
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use web_sys::{window, Storage};
|
||||
|
||||
const SPIKE_STORAGE_KEY: &str = "mnote.leptos-tiptap-spike.document";
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct PersistedSpikeDocument {
|
||||
#[serde(default)]
|
||||
pub(crate) document_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub(crate) workspace_id: Option<String>,
|
||||
pub(crate) title: String,
|
||||
pub(crate) content: Value,
|
||||
#[serde(default)]
|
||||
pub(crate) html: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct PersistedDocumentIdentity {
|
||||
pub(crate) document_id: Option<String>,
|
||||
pub(crate) workspace_id: Option<String>,
|
||||
}
|
||||
|
||||
fn local_storage() -> Option<Storage> {
|
||||
window().and_then(|win| win.local_storage().ok().flatten())
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_identity_value(value: Option<String>) -> Option<String> {
|
||||
value.and_then(|value| {
|
||||
let trimmed = value.trim();
|
||||
if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(trimmed.to_string())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn persisted_document_identity(
|
||||
document_id: Option<String>,
|
||||
workspace_id: Option<String>,
|
||||
) -> PersistedDocumentIdentity {
|
||||
PersistedDocumentIdentity {
|
||||
document_id: normalize_identity_value(document_id),
|
||||
workspace_id: normalize_identity_value(workspace_id),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn persisted_document_storage_key(identity: &PersistedDocumentIdentity) -> String {
|
||||
match (&identity.workspace_id, &identity.document_id) {
|
||||
(Some(workspace_id), Some(document_id)) => {
|
||||
format!("{SPIKE_STORAGE_KEY}:{workspace_id}:{document_id}")
|
||||
}
|
||||
(None, Some(document_id)) => format!("{SPIKE_STORAGE_KEY}:{document_id}"),
|
||||
_ => SPIKE_STORAGE_KEY.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn persist_document_state(
|
||||
identity: &PersistedDocumentIdentity,
|
||||
title: &str,
|
||||
content: &Value,
|
||||
html: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
let payload = PersistedSpikeDocument {
|
||||
document_id: identity.document_id.clone(),
|
||||
workspace_id: identity.workspace_id.clone(),
|
||||
title: title.to_string(),
|
||||
content: content.clone(),
|
||||
html,
|
||||
};
|
||||
let raw = serde_json::to_string(&payload).map_err(|err| format!("序列化草稿失败:{err}"))?;
|
||||
local_storage()
|
||||
.ok_or_else(|| "浏览器 localStorage 不可用".to_string())?
|
||||
.set_item(&persisted_document_storage_key(identity), &raw)
|
||||
.map_err(|err| format!("写入 localStorage 失败:{err:?}"))
|
||||
}
|
||||
|
||||
pub(crate) fn load_persisted_document(
|
||||
identity: &PersistedDocumentIdentity,
|
||||
) -> Option<PersistedSpikeDocument> {
|
||||
let raw = local_storage()?
|
||||
.get_item(&persisted_document_storage_key(identity))
|
||||
.ok()
|
||||
.flatten()?;
|
||||
let document = serde_json::from_str::<PersistedSpikeDocument>(&raw).ok()?;
|
||||
let stored_identity =
|
||||
persisted_document_identity(document.document_id.clone(), document.workspace_id.clone());
|
||||
if stored_identity == *identity || identity.document_id.is_none() {
|
||||
Some(document)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user