core: extract the store and sync engine into a shared crate (M12 step 1)
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>
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
//! Serde shapes for the local store. Output structs mirror the JSON the frontend
|
||||
//! already consumes (frontend/src/stores/*.ts) so the same components render whether
|
||||
//! the data comes from REST or from these Tauri commands. Field names/optionality
|
||||
//! match the TypeScript interfaces exactly.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Note {
|
||||
pub id: String,
|
||||
pub title: Option<String>,
|
||||
/// title if set, else the note's first body line — always present, so body-only
|
||||
/// notes are still nameable and `[[link]]`-able. Derived, never stored.
|
||||
pub display_title: String,
|
||||
pub body: String,
|
||||
pub color: String,
|
||||
pub kind: String,
|
||||
pub position: i64,
|
||||
pub pinned: bool,
|
||||
pub archived: bool,
|
||||
pub trashed: bool,
|
||||
/// When it was trashed (null unless trashed). Named for the server's field so the
|
||||
/// shared frontend counts down the retention window identically either way.
|
||||
pub deleted_at: Option<String>,
|
||||
pub remind_at: Option<String>,
|
||||
pub recurrence: Option<String>,
|
||||
pub labels: Vec<NoteLabel>,
|
||||
pub items: Vec<ChecklistItem>,
|
||||
pub attachments: Vec<Attachment>,
|
||||
pub previews: Vec<LinkPreview>,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct NoteLabel {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub color: String,
|
||||
/// True when attached because of a `#tag` in the body (kept in sync with the text).
|
||||
pub via_tag: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ChecklistItem {
|
||||
pub id: String,
|
||||
pub text: String,
|
||||
pub checked: bool,
|
||||
pub position: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Attachment {
|
||||
pub id: String,
|
||||
pub url: String,
|
||||
pub filename: Option<String>,
|
||||
pub mime: String,
|
||||
pub size: Option<i64>,
|
||||
pub sha256: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct LinkPreview {
|
||||
pub id: String,
|
||||
pub url: String,
|
||||
pub title: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub image_url: Option<String>,
|
||||
pub site_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct NoteRevision {
|
||||
pub id: String,
|
||||
pub title: Option<String>,
|
||||
pub body: String,
|
||||
pub created_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Label {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub color: String,
|
||||
// Note count is only meaningful in listings; omitted elsewhere so the frontend
|
||||
// preserves the count it already holds (matches the REST single-label responses).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub count: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct TitleEntry {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Backlink {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct SavedFilter {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
/// Mirrors NoteFacets — stored as a JSON blob, round-tripped opaquely.
|
||||
pub params: serde_json::Value,
|
||||
pub position: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct PublicConfig {
|
||||
pub site_name: String,
|
||||
pub allow_registration: bool,
|
||||
pub version: String,
|
||||
pub enable_url_unfurl: bool,
|
||||
pub trash_retention_days: u32,
|
||||
}
|
||||
|
||||
/// The synthetic single user the offline core reports, so the app's auth-gated
|
||||
/// router resolves without a login. There is no real account offline.
|
||||
#[derive(Serialize)]
|
||||
pub struct User {
|
||||
pub id: String,
|
||||
pub email: String,
|
||||
pub display_name: String,
|
||||
pub email_verified: bool,
|
||||
pub is_admin: bool,
|
||||
}
|
||||
|
||||
fn default_color() -> String {
|
||||
"default".to_string()
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct NoteCreateInput {
|
||||
#[serde(default)]
|
||||
pub title: String,
|
||||
#[serde(default)]
|
||||
pub body: String,
|
||||
#[serde(default = "default_color")]
|
||||
pub color: String,
|
||||
#[serde(default)]
|
||||
pub kind: Option<String>,
|
||||
#[serde(default)]
|
||||
pub items: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// The board query (mirrors adapters/repo.ts NoteListQuery). `labelId` is camelCase
|
||||
/// on the wire; the facet fields are snake_case, matching NoteFacets.
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ListQuery {
|
||||
pub view: String,
|
||||
#[serde(default)]
|
||||
pub label_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub facets: Option<Facets>,
|
||||
#[serde(default)]
|
||||
pub sort: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Facets {
|
||||
#[serde(default)]
|
||||
pub q: Option<String>,
|
||||
#[serde(default)]
|
||||
pub color: Option<String>,
|
||||
#[serde(default)]
|
||||
pub kind: Option<String>,
|
||||
#[serde(default)]
|
||||
pub label: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub has_reminder: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub has_attachment: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub created_after: Option<String>,
|
||||
#[serde(default)]
|
||||
pub created_before: Option<String>,
|
||||
}
|
||||
Reference in New Issue
Block a user