Files
docmost-rust/src/app.rs
2026-04-11 21:04:49 -06:00

345 lines
9.1 KiB
Rust

use serde::{Deserialize, Serialize};
// ─── Domain types ─────────────────────────────────────────────────────────────
#[derive(Debug, Clone, Deserialize)]
pub struct Space {
pub id: String,
pub name: String,
pub icon: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Page {
pub id: String,
pub title: Option<String>,
pub icon: Option<String>,
}
#[derive(Deserialize)]
pub struct DataWrapper<T> {
pub items: Vec<T>,
}
#[derive(Deserialize)]
#[serde(untagged)]
pub enum ApiList<T> {
Wrapped { data: DataWrapper<T> },
Paginated { items: Vec<T> },
Plain(Vec<T>),
}
impl<T> ApiList<T> {
pub fn into_items(self) -> Vec<T> {
match self {
ApiList::Wrapped { data } => data.items,
ApiList::Paginated { items } => items,
ApiList::Plain(v) => v,
}
}
}
// ─── App state ────────────────────────────────────────────────────────────────
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum AppState {
Login,
Main,
Editor,
Search,
}
// ─── Login form ───────────────────────────────────────────────────────────────
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LoginField {
Url,
Email,
Password,
}
impl LoginField {
pub fn next(self) -> Self {
match self {
LoginField::Url => LoginField::Email,
LoginField::Email => LoginField::Password,
LoginField::Password => LoginField::Url,
}
}
pub fn prev(self) -> Self {
match self {
LoginField::Url => LoginField::Password,
LoginField::Email => LoginField::Url,
LoginField::Password => LoginField::Email,
}
}
}
pub struct LoginForm {
pub url: String,
pub email: String,
pub password: String,
pub active_field: LoginField,
pub error: Option<String>,
pub submitting: bool,
}
impl LoginForm {
pub fn new() -> Self {
Self {
url: std::env::var("DOCMOST_URL")
.unwrap_or_else(|_| "http://localhost:3000".to_string()),
email: std::env::var("DOCMOST_EMAIL").unwrap_or_default(),
password: String::new(),
active_field: if std::env::var("DOCMOST_EMAIL").is_ok() {
LoginField::Password
} else {
LoginField::Email
},
error: None,
submitting: false,
}
}
pub fn active_field_value_mut(&mut self) -> &mut String {
match self.active_field {
LoginField::Url => &mut self.url,
LoginField::Email => &mut self.email,
LoginField::Password => &mut self.password,
}
}
}
// ─── Main view ────────────────────────────────────────────────────────────────
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Panel {
Spaces,
Pages,
}
pub struct NewPageDialog {
pub title: String,
pub creating: bool,
pub error: Option<String>,
}
impl NewPageDialog {
pub fn new() -> Self {
Self { title: String::new(), creating: false, error: None }
}
}
pub struct NewSpaceDialog {
pub name: String,
pub creating: bool,
pub error: Option<String>,
}
impl NewSpaceDialog {
pub fn new() -> Self {
Self { name: String::new(), creating: false, error: None }
}
}
#[derive(Debug, Clone)]
pub enum DeleteTarget {
Space { id: String, name: String },
Page { id: String, title: String },
}
impl DeleteTarget {
pub fn display_name(&self) -> &str {
match self {
DeleteTarget::Space { name, .. } => name,
DeleteTarget::Page { title, .. } => title,
}
}
pub fn kind(&self) -> &str {
match self {
DeleteTarget::Space { .. } => "space",
DeleteTarget::Page { .. } => "page",
}
}
}
pub struct DeleteConfirmDialog {
pub target: DeleteTarget,
pub deleting: bool,
pub error: Option<String>,
}
impl DeleteConfirmDialog {
pub fn new(target: DeleteTarget) -> Self {
Self { target, deleting: false, error: None }
}
}
pub struct MainView {
pub spaces: Vec<Space>,
pub pages: Vec<Page>,
pub selected_space: usize,
pub selected_page: usize,
pub focus: Panel,
pub loading_spaces: bool,
pub loading_pages: bool,
pub loading_page_content: bool,
pub error: Option<String>,
pub new_page_dialog: Option<NewPageDialog>,
pub new_space_dialog: Option<NewSpaceDialog>,
pub delete_dialog: Option<DeleteConfirmDialog>,
}
impl MainView {
pub fn new() -> Self {
Self {
spaces: vec![],
pages: vec![],
selected_space: 0,
selected_page: 0,
focus: Panel::Spaces,
loading_spaces: true,
loading_pages: false,
loading_page_content: false,
error: None,
new_page_dialog: None,
new_space_dialog: None,
delete_dialog: None,
}
}
}
// ─── Editor view ─────────────────────────────────────────────────────────────
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum EditorFocus {
Title,
Content,
}
pub struct EditorView {
pub page_id: String,
pub page_title: String,
pub focus: EditorFocus,
pub saving: bool,
pub status: Option<EditorStatus>,
}
#[derive(Debug, Clone)]
pub enum EditorStatus {
Saved,
Error(String),
}
impl EditorView {
pub fn new(page_id: String, page_title: String) -> Self {
Self {
page_id,
page_title,
focus: EditorFocus::Content,
saving: false,
status: None,
}
}
}
// ─── Search view ─────────────────────────────────────────────────────────────
#[derive(Debug, Clone, Deserialize)]
pub struct SearchResultSpace {
pub name: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct SearchResult {
pub id: String,
pub title: Option<String>,
pub icon: Option<String>,
pub highlight: Option<String>,
pub space: Option<SearchResultSpace>,
}
pub struct SearchView {
pub query: String,
pub results: Vec<SearchResult>,
pub selected: usize,
pub loading: bool,
pub error: Option<String>,
pub opening_page: bool,
}
impl SearchView {
pub fn new() -> Self {
Self {
query: String::new(),
results: vec![],
selected: 0,
loading: false,
error: None,
opening_page: false,
}
}
}
// ─── Root app ─────────────────────────────────────────────────────────────────
pub struct App {
pub state: AppState,
pub login: LoginForm,
pub main: MainView,
pub editor: EditorView,
pub search: SearchView,
pub base_url: String,
pub token: String,
}
impl App {
pub fn new() -> Self {
Self {
state: AppState::Login,
login: LoginForm::new(),
main: MainView::new(),
editor: EditorView::new(String::new(), String::new()),
search: SearchView::new(),
base_url: String::new(),
token: String::new(),
}
}
}
// ─── Messages ─────────────────────────────────────────────────────────────────
pub enum AppMsg {
LoginSuccess { token: String, base_url: String },
LoginError(String),
SpacesLoaded(Vec<Space>),
PagesLoaded(Vec<Page>),
PageContentLoaded { page_id: String, title: String, content: String },
PageSaved,
SaveError(String),
PageCreated { page_id: String, title: String },
CreateError(String),
SpaceCreated(Space),
CreateSpaceError(String),
SpaceDeleted(String),
PageDeleted(String),
DeleteError(String),
SearchResults(Vec<SearchResult>),
SearchError(String),
ApiError(String),
}
// ─── Request DTOs ─────────────────────────────────────────────────────────────
#[derive(Serialize)]
pub struct LoginRequest<'a> {
pub email: &'a str,
pub password: &'a str,
}
#[derive(Serialize)]
pub struct SidebarPagesRequest<'a> {
#[serde(rename = "spaceId")]
pub space_id: &'a str,
}