Android becomes a native Kotlin client over this same code (Scribe note 2730), so
the local store and sync engine stop being modules of the desktop app and become
`thoughtsync-core`, a crate with no UI framework in it at all.
This is a move, not a rewrite, and the measurement is why: every file in local/
and sync/ already carried ZERO Tauri references — 4,980 of 6,372 lines. The
coupling was 473 lines of command shim, which stays behind in the desktop crate
as src/commands/. Kept as git renames so history follows the files.
The desktop imports them under their old names (`use thoughtsync_core::{local,
sync}`) so every call site reads exactly as before. What moved is where they
live, not what they are.
Two things a workspace changes that are easy to miss, both caught before pushing:
[profile.release] now lives at the workspace ROOT. Cargo silently ignores
profiles declared by a non-root member — leaving it in the desktop crate would
have dropped lto/strip/opt-level from every release build with only a warning.
And a workspace shares ONE target dir, so the bundles moved from
desktop/src-tauri/target to target/. Thirteen references across publish-release,
debundle-graphics, verify.sh, package-prebuilt and the workflow now point there.
Pinning target-dir back would have been the smaller diff, but the Android lane
also produces Rust artifacts and they do not belong under desktop/.
Also retires the Tauri Android lane in the same push rather than leaving a path
that is being replaced: gen/android, android.yml and docs/android-dev.md are
gone, the mobile_entry_point attribute with them, and the lib drops to rlib —
staticlib/cdylib existed for Tauri mobile, and the .so Android loads will be
built from the core crate instead. Rule 22, no parallel path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
167 lines
4.5 KiB
Rust
167 lines
4.5 KiB
Rust
//! The delta-feed JSON shapes, exactly as `GET /api/sync/changes` sends them.
|
|
//!
|
|
//! Mirrors the server's serializers (`notes/serialize.py` + `serialize.py`) — see
|
|
//! `docs/sync.md` for the contract. Every field is `#[serde(default)]` or `Option`
|
|
//! so a NEWER server adding fields, or an older one omitting one, degrades to a
|
|
//! partial note rather than failing the whole page. Losing one attribute is
|
|
//! recoverable; refusing a page stalls sync permanently at that cursor.
|
|
|
|
use serde::Deserialize;
|
|
|
|
#[derive(Debug, Clone, Deserialize, Default)]
|
|
pub struct ChangesPage {
|
|
#[serde(default)]
|
|
pub notes: Vec<Note>,
|
|
#[serde(default)]
|
|
pub labels: Vec<Label>,
|
|
#[serde(default)]
|
|
pub cursor: i64,
|
|
#[serde(default)]
|
|
pub has_more: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct Note {
|
|
pub id: String,
|
|
#[serde(default)]
|
|
pub title: Option<String>,
|
|
#[serde(default)]
|
|
pub body: String,
|
|
#[serde(default = "default_color")]
|
|
pub color: String,
|
|
#[serde(default = "default_kind")]
|
|
pub kind: String,
|
|
#[serde(default)]
|
|
pub position: i64,
|
|
#[serde(default)]
|
|
pub pinned: bool,
|
|
#[serde(default)]
|
|
pub archived: bool,
|
|
/// The server derives this from `deleted_at` — trash, NOT a tombstone.
|
|
#[serde(default)]
|
|
pub trashed: bool,
|
|
/// WHEN it was trashed. The trash-retention clock runs from here, so it has to be
|
|
/// the server's timestamp rather than anything this device invents. Absent from an
|
|
/// older server, which is why it's optional rather than required.
|
|
#[serde(default)]
|
|
pub deleted_at: Option<String>,
|
|
#[serde(default)]
|
|
pub remind_at: Option<String>,
|
|
#[serde(default)]
|
|
pub recurrence: Option<String>,
|
|
#[serde(default)]
|
|
pub created_at: Option<String>,
|
|
#[serde(default)]
|
|
pub updated_at: Option<String>,
|
|
#[serde(default)]
|
|
pub sync_revision: i64,
|
|
/// Set means the row was permanently purged: a content-less tombstone whose only
|
|
/// job is to tell clients to delete their copy.
|
|
#[serde(default)]
|
|
pub purged_at: Option<String>,
|
|
#[serde(default)]
|
|
pub labels: Vec<NoteLabel>,
|
|
#[serde(default)]
|
|
pub items: Vec<Item>,
|
|
#[serde(default)]
|
|
pub attachments: Vec<Attachment>,
|
|
#[serde(default)]
|
|
pub previews: Vec<Preview>,
|
|
}
|
|
|
|
impl Note {
|
|
pub fn is_tombstone(&self) -> bool {
|
|
self.purged_at.is_some()
|
|
}
|
|
}
|
|
|
|
/// A label as it appears attached to a note. Carries enough to materialize the label
|
|
/// row itself, which is what lets a membership be applied even if the label's own
|
|
/// delta hasn't arrived (see `pull::apply_page`).
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct NoteLabel {
|
|
pub id: String,
|
|
#[serde(default)]
|
|
pub name: String,
|
|
#[serde(default = "default_color")]
|
|
pub color: String,
|
|
/// True when the membership came from a `#tag` in the body rather than a manual
|
|
/// assignment. Applied verbatim rather than re-derived — see `pull::apply_page`.
|
|
#[serde(default)]
|
|
pub via_tag: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct Item {
|
|
pub id: String,
|
|
#[serde(default)]
|
|
pub text: String,
|
|
#[serde(default)]
|
|
pub checked: bool,
|
|
#[serde(default)]
|
|
pub position: i64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct Attachment {
|
|
pub id: String,
|
|
#[serde(default)]
|
|
pub url: String,
|
|
#[serde(default)]
|
|
pub filename: Option<String>,
|
|
#[serde(default = "default_mime")]
|
|
pub mime: String,
|
|
#[serde(default)]
|
|
pub size: Option<i64>,
|
|
#[serde(default)]
|
|
pub sha256: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct Preview {
|
|
pub id: String,
|
|
#[serde(default)]
|
|
pub url: String,
|
|
#[serde(default)]
|
|
pub title: Option<String>,
|
|
#[serde(default)]
|
|
pub description: Option<String>,
|
|
#[serde(default)]
|
|
pub image_url: Option<String>,
|
|
#[serde(default)]
|
|
pub site_name: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct Label {
|
|
pub id: String,
|
|
#[serde(default)]
|
|
pub name: String,
|
|
#[serde(default = "default_color")]
|
|
pub color: String,
|
|
#[serde(default)]
|
|
pub sync_revision: i64,
|
|
#[serde(default)]
|
|
pub purged_at: Option<String>,
|
|
#[serde(default)]
|
|
pub created_at: Option<String>,
|
|
}
|
|
|
|
impl Label {
|
|
pub fn is_tombstone(&self) -> bool {
|
|
self.purged_at.is_some()
|
|
}
|
|
}
|
|
|
|
fn default_color() -> String {
|
|
"default".to_string()
|
|
}
|
|
|
|
fn default_kind() -> String {
|
|
"text".to_string()
|
|
}
|
|
|
|
fn default_mime() -> String {
|
|
"application/octet-stream".to_string()
|
|
}
|