feat(office): Add Phase 3 advanced features - Comments, Track Changes, TOC, Footnotes, Styles, Presenter View, Transitions, Media
This commit is contained in:
parent
1850564e62
commit
840c7789f3
9 changed files with 3428 additions and 36 deletions
1133
src/docs/handlers.rs
1133
src/docs/handlers.rs
File diff suppressed because it is too large
Load diff
|
|
@ -14,16 +14,25 @@ use std::sync::Arc;
|
|||
|
||||
pub use collaboration::handle_docs_websocket;
|
||||
pub use handlers::{
|
||||
handle_ai_custom, handle_ai_expand, handle_ai_improve, handle_ai_simplify, handle_ai_summarize,
|
||||
handle_ai_translate, handle_autosave, handle_delete_document, handle_docs_ai, handle_docs_get_by_id,
|
||||
handle_docs_save, handle_export_docx, handle_export_html, handle_export_md, handle_export_pdf,
|
||||
handle_export_txt, handle_get_document, handle_list_documents, handle_new_document,
|
||||
handle_accept_reject_all, handle_accept_reject_change, handle_add_comment, handle_add_endnote,
|
||||
handle_add_footnote, handle_ai_custom, handle_ai_expand, handle_ai_improve, handle_ai_simplify,
|
||||
handle_ai_summarize, handle_ai_translate, handle_apply_style, handle_autosave,
|
||||
handle_compare_documents, handle_create_style, handle_delete_comment, handle_delete_document,
|
||||
handle_delete_endnote, handle_delete_footnote, handle_delete_style, handle_docs_ai,
|
||||
handle_docs_get_by_id, handle_docs_save, handle_enable_track_changes, handle_export_docx,
|
||||
handle_export_html, handle_export_md, handle_export_pdf, handle_export_txt,
|
||||
handle_generate_toc, handle_get_document, handle_get_outline, handle_list_comments,
|
||||
handle_list_documents, handle_list_endnotes, handle_list_footnotes, handle_list_styles,
|
||||
handle_list_track_changes, handle_new_document, handle_reply_comment, handle_resolve_comment,
|
||||
handle_save_document, handle_search_documents, handle_template_blank, handle_template_letter,
|
||||
handle_template_meeting, handle_template_report,
|
||||
handle_template_meeting, handle_template_report, handle_update_endnote, handle_update_footnote,
|
||||
handle_update_style, handle_update_toc,
|
||||
};
|
||||
pub use types::{
|
||||
AiRequest, AiResponse, Collaborator, CollabMessage, Document, DocumentMetadata, SaveRequest,
|
||||
SaveResponse, SearchQuery,
|
||||
AiRequest, AiResponse, Collaborator, CollabMessage, CommentReply, ComparisonSummary, Document,
|
||||
DocumentComment, DocumentComparison, DocumentDiff, DocumentMetadata, DocumentStyle, Endnote,
|
||||
Footnote, OutlineItem, SaveRequest, SaveResponse, SearchQuery, TableOfContents, TocEntry,
|
||||
TrackChange,
|
||||
};
|
||||
|
||||
pub fn configure_docs_routes() -> Router<Arc<AppState>> {
|
||||
|
|
@ -52,5 +61,31 @@ pub fn configure_docs_routes() -> Router<Arc<AppState>> {
|
|||
.route("/api/docs/export/md", get(handle_export_md))
|
||||
.route("/api/docs/export/html", get(handle_export_html))
|
||||
.route("/api/docs/export/txt", get(handle_export_txt))
|
||||
.route("/api/docs/comment", post(handle_add_comment))
|
||||
.route("/api/docs/comment/reply", post(handle_reply_comment))
|
||||
.route("/api/docs/comment/resolve", post(handle_resolve_comment))
|
||||
.route("/api/docs/comment/delete", post(handle_delete_comment))
|
||||
.route("/api/docs/comments", get(handle_list_comments))
|
||||
.route("/api/docs/track-changes/enable", post(handle_enable_track_changes))
|
||||
.route("/api/docs/track-changes/accept-reject", post(handle_accept_reject_change))
|
||||
.route("/api/docs/track-changes/accept-reject-all", post(handle_accept_reject_all))
|
||||
.route("/api/docs/track-changes", get(handle_list_track_changes))
|
||||
.route("/api/docs/toc/generate", post(handle_generate_toc))
|
||||
.route("/api/docs/toc/update", post(handle_update_toc))
|
||||
.route("/api/docs/footnote", post(handle_add_footnote))
|
||||
.route("/api/docs/footnote/update", post(handle_update_footnote))
|
||||
.route("/api/docs/footnote/delete", post(handle_delete_footnote))
|
||||
.route("/api/docs/footnotes", get(handle_list_footnotes))
|
||||
.route("/api/docs/endnote", post(handle_add_endnote))
|
||||
.route("/api/docs/endnote/update", post(handle_update_endnote))
|
||||
.route("/api/docs/endnote/delete", post(handle_delete_endnote))
|
||||
.route("/api/docs/endnotes", get(handle_list_endnotes))
|
||||
.route("/api/docs/style", post(handle_create_style))
|
||||
.route("/api/docs/style/update", post(handle_update_style))
|
||||
.route("/api/docs/style/delete", post(handle_delete_style))
|
||||
.route("/api/docs/style/apply", post(handle_apply_style))
|
||||
.route("/api/docs/styles", get(handle_list_styles))
|
||||
.route("/api/docs/outline", post(handle_get_outline))
|
||||
.route("/api/docs/compare", post(handle_compare_documents))
|
||||
.route("/ws/docs/:doc_id", get(handle_docs_websocket))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,152 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TrackChange {
|
||||
pub id: String,
|
||||
pub change_type: String,
|
||||
pub author_id: String,
|
||||
pub author_name: String,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
pub original_text: Option<String>,
|
||||
pub new_text: Option<String>,
|
||||
pub position: usize,
|
||||
pub length: usize,
|
||||
pub accepted: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DocumentComment {
|
||||
pub id: String,
|
||||
pub author_id: String,
|
||||
pub author_name: String,
|
||||
pub content: String,
|
||||
pub position: usize,
|
||||
pub length: usize,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
#[serde(default)]
|
||||
pub replies: Vec<CommentReply>,
|
||||
#[serde(default)]
|
||||
pub resolved: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CommentReply {
|
||||
pub id: String,
|
||||
pub author_id: String,
|
||||
pub author_name: String,
|
||||
pub content: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TableOfContents {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub entries: Vec<TocEntry>,
|
||||
pub max_level: u32,
|
||||
pub show_page_numbers: bool,
|
||||
pub use_hyperlinks: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TocEntry {
|
||||
pub id: String,
|
||||
pub text: String,
|
||||
pub level: u32,
|
||||
pub page_number: Option<u32>,
|
||||
pub position: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Footnote {
|
||||
pub id: String,
|
||||
pub reference_mark: String,
|
||||
pub content: String,
|
||||
pub position: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Endnote {
|
||||
pub id: String,
|
||||
pub reference_mark: String,
|
||||
pub content: String,
|
||||
pub position: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DocumentStyle {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub style_type: String,
|
||||
pub based_on: Option<String>,
|
||||
pub next_style: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub font_family: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub font_size: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub font_weight: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub font_style: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub color: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub background: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub line_spacing: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub space_before: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub space_after: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub text_align: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub indent_left: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub indent_right: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub indent_first_line: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OutlineItem {
|
||||
pub id: String,
|
||||
pub text: String,
|
||||
pub level: u32,
|
||||
pub position: usize,
|
||||
pub length: usize,
|
||||
pub style_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DocumentComparison {
|
||||
pub id: String,
|
||||
pub original_doc_id: String,
|
||||
pub modified_doc_id: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub differences: Vec<DocumentDiff>,
|
||||
pub summary: ComparisonSummary,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DocumentDiff {
|
||||
pub diff_type: String,
|
||||
pub position: usize,
|
||||
pub original_text: Option<String>,
|
||||
pub modified_text: Option<String>,
|
||||
pub length: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ComparisonSummary {
|
||||
pub insertions: u32,
|
||||
pub deletions: u32,
|
||||
pub modifications: u32,
|
||||
pub total_changes: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CollabMessage {
|
||||
pub msg_type: String,
|
||||
|
|
@ -42,6 +188,20 @@ pub struct Document {
|
|||
pub collaborators: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub version: u64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub track_changes: Option<Vec<TrackChange>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub comments: Option<Vec<DocumentComment>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub footnotes: Option<Vec<Footnote>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub endnotes: Option<Vec<Endnote>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub styles: Option<Vec<DocumentStyle>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub toc: Option<TableOfContents>,
|
||||
#[serde(default)]
|
||||
pub track_changes_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
@ -159,3 +319,181 @@ pub struct TemplateResponse {
|
|||
pub title: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AddCommentRequest {
|
||||
pub doc_id: String,
|
||||
pub content: String,
|
||||
pub position: usize,
|
||||
pub length: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ReplyCommentRequest {
|
||||
pub doc_id: String,
|
||||
pub comment_id: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ResolveCommentRequest {
|
||||
pub doc_id: String,
|
||||
pub comment_id: String,
|
||||
pub resolved: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DeleteCommentRequest {
|
||||
pub doc_id: String,
|
||||
pub comment_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ListCommentsResponse {
|
||||
pub comments: Vec<DocumentComment>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EnableTrackChangesRequest {
|
||||
pub doc_id: String,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AcceptRejectChangeRequest {
|
||||
pub doc_id: String,
|
||||
pub change_id: String,
|
||||
pub accept: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AcceptRejectAllRequest {
|
||||
pub doc_id: String,
|
||||
pub accept: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ListTrackChangesResponse {
|
||||
pub changes: Vec<TrackChange>,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GenerateTocRequest {
|
||||
pub doc_id: String,
|
||||
pub max_level: u32,
|
||||
pub show_page_numbers: bool,
|
||||
pub use_hyperlinks: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateTocRequest {
|
||||
pub doc_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TocResponse {
|
||||
pub toc: TableOfContents,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AddFootnoteRequest {
|
||||
pub doc_id: String,
|
||||
pub content: String,
|
||||
pub position: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateFootnoteRequest {
|
||||
pub doc_id: String,
|
||||
pub footnote_id: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DeleteFootnoteRequest {
|
||||
pub doc_id: String,
|
||||
pub footnote_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ListFootnotesResponse {
|
||||
pub footnotes: Vec<Footnote>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AddEndnoteRequest {
|
||||
pub doc_id: String,
|
||||
pub content: String,
|
||||
pub position: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateEndnoteRequest {
|
||||
pub doc_id: String,
|
||||
pub endnote_id: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DeleteEndnoteRequest {
|
||||
pub doc_id: String,
|
||||
pub endnote_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ListEndnotesResponse {
|
||||
pub endnotes: Vec<Endnote>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreateStyleRequest {
|
||||
pub doc_id: String,
|
||||
pub style: DocumentStyle,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateStyleRequest {
|
||||
pub doc_id: String,
|
||||
pub style: DocumentStyle,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DeleteStyleRequest {
|
||||
pub doc_id: String,
|
||||
pub style_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ListStylesResponse {
|
||||
pub styles: Vec<DocumentStyle>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ApplyStyleRequest {
|
||||
pub doc_id: String,
|
||||
pub style_id: String,
|
||||
pub position: usize,
|
||||
pub length: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GetOutlineRequest {
|
||||
pub doc_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OutlineResponse {
|
||||
pub items: Vec<OutlineItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CompareDocumentsRequest {
|
||||
pub original_doc_id: String,
|
||||
pub modified_doc_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CompareDocumentsResponse {
|
||||
pub comparison: DocumentComparison,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,13 +8,19 @@ use crate::sheet::storage::{
|
|||
save_sheet_to_drive,
|
||||
};
|
||||
use crate::sheet::types::{
|
||||
AddNoteRequest, CellData, CellUpdateRequest, ChartConfig, ChartOptions,
|
||||
ChartPosition, ChartRequest, ClearFilterRequest, ConditionalFormatRequest,
|
||||
ConditionalFormatRule, DataValidationRequest, DeleteChartRequest, ExportRequest, FilterConfig,
|
||||
FilterRequest, FormatRequest, FormulaRequest, FormulaResult, FreezePanesRequest,
|
||||
LoadFromDriveRequest, LoadQuery, MergeCellsRequest, MergedCell, SaveRequest, SaveResponse,
|
||||
SearchQuery, ShareRequest, SheetAiRequest, SheetAiResponse, SortRequest, Spreadsheet,
|
||||
SpreadsheetMetadata, ValidateCellRequest, ValidationResult, ValidationRule, Worksheet,
|
||||
AddCommentRequest, AddExternalLinkRequest, AddNoteRequest, ArrayFormula, ArrayFormulaRequest,
|
||||
CellComment, CellData, CellUpdateRequest, ChartConfig, ChartOptions, ChartPosition,
|
||||
ChartRequest, ClearFilterRequest, CommentReply, CommentWithLocation, ConditionalFormatRequest,
|
||||
ConditionalFormatRule, CreateNamedRangeRequest, DataValidationRequest, DeleteArrayFormulaRequest,
|
||||
DeleteChartRequest, DeleteCommentRequest, DeleteNamedRangeRequest, ExportRequest, ExternalLink,
|
||||
FilterConfig, FilterRequest, FormatRequest, FormulaRequest, FormulaResult, FreezePanesRequest,
|
||||
ListCommentsRequest, ListCommentsResponse, ListExternalLinksResponse, ListNamedRangesRequest,
|
||||
ListNamedRangesResponse, LoadFromDriveRequest, LoadQuery, LockCellsRequest, MergeCellsRequest,
|
||||
MergedCell, NamedRange, ProtectSheetRequest, RefreshExternalLinkRequest, RemoveExternalLinkRequest,
|
||||
ReplyCommentRequest, ResolveCommentRequest, SaveRequest, SaveResponse, SearchQuery, ShareRequest,
|
||||
SheetAiRequest, SheetAiResponse, SheetProtection, SortRequest, Spreadsheet, SpreadsheetMetadata,
|
||||
UnprotectSheetRequest, UpdateNamedRangeRequest, ValidateCellRequest, ValidationResult,
|
||||
ValidationRule, Worksheet,
|
||||
};
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
|
|
@ -1157,3 +1163,836 @@ pub async fn handle_import_sheet(
|
|||
) -> Result<Json<Spreadsheet>, (StatusCode, Json<serde_json::Value>)> {
|
||||
Ok(Json(create_new_spreadsheet()))
|
||||
}
|
||||
|
||||
pub async fn handle_add_comment(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<AddCommentRequest>,
|
||||
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let user_id = get_current_user_id();
|
||||
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
if req.worksheet_index >= sheet.worksheets.len() {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({ "error": "Invalid worksheet index" })),
|
||||
));
|
||||
}
|
||||
|
||||
let worksheet = &mut sheet.worksheets[req.worksheet_index];
|
||||
let key = format!("{},{}", req.row, req.col);
|
||||
|
||||
let comment = CellComment {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
author_id: user_id.clone(),
|
||||
author_name: "User".to_string(),
|
||||
content: req.content,
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
replies: vec![],
|
||||
resolved: false,
|
||||
};
|
||||
|
||||
let comments = worksheet.comments.get_or_insert_with(HashMap::new);
|
||||
comments.insert(key.clone(), comment);
|
||||
|
||||
let cell = worksheet.data.entry(key).or_insert_with(|| CellData {
|
||||
value: None,
|
||||
formula: None,
|
||||
style: None,
|
||||
format: None,
|
||||
note: None,
|
||||
locked: None,
|
||||
has_comment: None,
|
||||
array_formula_id: None,
|
||||
});
|
||||
cell.has_comment = Some(true);
|
||||
|
||||
sheet.updated_at = Utc::now();
|
||||
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Json(SaveResponse {
|
||||
id: req.sheet_id,
|
||||
success: true,
|
||||
message: Some("Comment added".to_string()),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn handle_reply_comment(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<ReplyCommentRequest>,
|
||||
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let user_id = get_current_user_id();
|
||||
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
if req.worksheet_index >= sheet.worksheets.len() {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({ "error": "Invalid worksheet index" })),
|
||||
));
|
||||
}
|
||||
|
||||
let worksheet = &mut sheet.worksheets[req.worksheet_index];
|
||||
let key = format!("{},{}", req.row, req.col);
|
||||
|
||||
if let Some(comments) = &mut worksheet.comments {
|
||||
if let Some(comment) = comments.get_mut(&key) {
|
||||
if comment.id == req.comment_id {
|
||||
let reply = CommentReply {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
author_id: user_id.clone(),
|
||||
author_name: "User".to_string(),
|
||||
content: req.content,
|
||||
created_at: Utc::now(),
|
||||
};
|
||||
comment.replies.push(reply);
|
||||
comment.updated_at = Utc::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sheet.updated_at = Utc::now();
|
||||
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Json(SaveResponse {
|
||||
id: req.sheet_id,
|
||||
success: true,
|
||||
message: Some("Reply added".to_string()),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn handle_resolve_comment(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<ResolveCommentRequest>,
|
||||
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let user_id = get_current_user_id();
|
||||
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
if req.worksheet_index >= sheet.worksheets.len() {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({ "error": "Invalid worksheet index" })),
|
||||
));
|
||||
}
|
||||
|
||||
let worksheet = &mut sheet.worksheets[req.worksheet_index];
|
||||
let key = format!("{},{}", req.row, req.col);
|
||||
|
||||
if let Some(comments) = &mut worksheet.comments {
|
||||
if let Some(comment) = comments.get_mut(&key) {
|
||||
if comment.id == req.comment_id {
|
||||
comment.resolved = req.resolved;
|
||||
comment.updated_at = Utc::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sheet.updated_at = Utc::now();
|
||||
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Json(SaveResponse {
|
||||
id: req.sheet_id,
|
||||
success: true,
|
||||
message: Some("Comment resolved".to_string()),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn handle_delete_comment(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<DeleteCommentRequest>,
|
||||
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let user_id = get_current_user_id();
|
||||
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
if req.worksheet_index >= sheet.worksheets.len() {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({ "error": "Invalid worksheet index" })),
|
||||
));
|
||||
}
|
||||
|
||||
let worksheet = &mut sheet.worksheets[req.worksheet_index];
|
||||
let key = format!("{},{}", req.row, req.col);
|
||||
|
||||
if let Some(comments) = &mut worksheet.comments {
|
||||
comments.remove(&key);
|
||||
}
|
||||
|
||||
if let Some(cell) = worksheet.data.get_mut(&key) {
|
||||
cell.has_comment = Some(false);
|
||||
}
|
||||
|
||||
sheet.updated_at = Utc::now();
|
||||
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Json(SaveResponse {
|
||||
id: req.sheet_id,
|
||||
success: true,
|
||||
message: Some("Comment deleted".to_string()),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn handle_list_comments(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<ListCommentsRequest>,
|
||||
) -> Result<Json<ListCommentsResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let user_id = get_current_user_id();
|
||||
let sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
if req.worksheet_index >= sheet.worksheets.len() {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({ "error": "Invalid worksheet index" })),
|
||||
));
|
||||
}
|
||||
|
||||
let worksheet = &sheet.worksheets[req.worksheet_index];
|
||||
let mut comments_list = vec![];
|
||||
|
||||
if let Some(comments) = &worksheet.comments {
|
||||
for (key, comment) in comments {
|
||||
let parts: Vec<&str> = key.split(',').collect();
|
||||
if parts.len() == 2 {
|
||||
if let (Ok(row), Ok(col)) = (parts[0].parse::<u32>(), parts[1].parse::<u32>()) {
|
||||
comments_list.push(CommentWithLocation {
|
||||
row,
|
||||
col,
|
||||
comment: comment.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Json(ListCommentsResponse { comments: comments_list }))
|
||||
}
|
||||
|
||||
pub async fn handle_protect_sheet(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<ProtectSheetRequest>,
|
||||
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let user_id = get_current_user_id();
|
||||
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
if req.worksheet_index >= sheet.worksheets.len() {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({ "error": "Invalid worksheet index" })),
|
||||
));
|
||||
}
|
||||
|
||||
let mut protection = req.protection;
|
||||
if let Some(password) = req.password {
|
||||
protection.password_hash = Some(format!("{:x}", md5::compute(password.as_bytes())));
|
||||
}
|
||||
|
||||
sheet.worksheets[req.worksheet_index].protection = Some(protection);
|
||||
sheet.updated_at = Utc::now();
|
||||
|
||||
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Json(SaveResponse {
|
||||
id: req.sheet_id,
|
||||
success: true,
|
||||
message: Some("Sheet protected".to_string()),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn handle_unprotect_sheet(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<UnprotectSheetRequest>,
|
||||
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let user_id = get_current_user_id();
|
||||
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
if req.worksheet_index >= sheet.worksheets.len() {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({ "error": "Invalid worksheet index" })),
|
||||
));
|
||||
}
|
||||
|
||||
let worksheet = &mut sheet.worksheets[req.worksheet_index];
|
||||
if let Some(protection) = &worksheet.protection {
|
||||
if let Some(hash) = &protection.password_hash {
|
||||
if let Some(password) = &req.password {
|
||||
let provided_hash = format!("{:x}", md5::compute(password.as_bytes()));
|
||||
if &provided_hash != hash {
|
||||
return Err((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(serde_json::json!({ "error": "Invalid password" })),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return Err((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(serde_json::json!({ "error": "Password required" })),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
worksheet.protection = None;
|
||||
sheet.updated_at = Utc::now();
|
||||
|
||||
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Json(SaveResponse {
|
||||
id: req.sheet_id,
|
||||
success: true,
|
||||
message: Some("Sheet unprotected".to_string()),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn handle_lock_cells(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<LockCellsRequest>,
|
||||
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let user_id = get_current_user_id();
|
||||
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
if req.worksheet_index >= sheet.worksheets.len() {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({ "error": "Invalid worksheet index" })),
|
||||
));
|
||||
}
|
||||
|
||||
let worksheet = &mut sheet.worksheets[req.worksheet_index];
|
||||
|
||||
for row in req.start_row..=req.end_row {
|
||||
for col in req.start_col..=req.end_col {
|
||||
let key = format!("{row},{col}");
|
||||
let cell = worksheet.data.entry(key).or_insert_with(|| CellData {
|
||||
value: None,
|
||||
formula: None,
|
||||
style: None,
|
||||
format: None,
|
||||
note: None,
|
||||
locked: None,
|
||||
has_comment: None,
|
||||
array_formula_id: None,
|
||||
});
|
||||
cell.locked = Some(req.locked);
|
||||
}
|
||||
}
|
||||
|
||||
sheet.updated_at = Utc::now();
|
||||
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Json(SaveResponse {
|
||||
id: req.sheet_id,
|
||||
success: true,
|
||||
message: Some(if req.locked { "Cells locked" } else { "Cells unlocked" }.to_string()),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn handle_add_external_link(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<AddExternalLinkRequest>,
|
||||
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let user_id = get_current_user_id();
|
||||
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let link = ExternalLink {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
source_path: req.source_path,
|
||||
link_type: req.link_type,
|
||||
target_sheet: req.target_sheet,
|
||||
target_range: req.target_range,
|
||||
status: "active".to_string(),
|
||||
last_updated: Utc::now(),
|
||||
};
|
||||
|
||||
let links = sheet.external_links.get_or_insert_with(Vec::new);
|
||||
links.push(link);
|
||||
|
||||
sheet.updated_at = Utc::now();
|
||||
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Json(SaveResponse {
|
||||
id: req.sheet_id,
|
||||
success: true,
|
||||
message: Some("External link added".to_string()),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn handle_refresh_external_link(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<RefreshExternalLinkRequest>,
|
||||
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let user_id = get_current_user_id();
|
||||
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(links) = &mut sheet.external_links {
|
||||
for link in links.iter_mut() {
|
||||
if link.id == req.link_id {
|
||||
link.last_updated = Utc::now();
|
||||
link.status = "refreshed".to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sheet.updated_at = Utc::now();
|
||||
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Json(SaveResponse {
|
||||
id: req.sheet_id,
|
||||
success: true,
|
||||
message: Some("External link refreshed".to_string()),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn handle_remove_external_link(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<RemoveExternalLinkRequest>,
|
||||
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let user_id = get_current_user_id();
|
||||
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(links) = &mut sheet.external_links {
|
||||
links.retain(|link| link.id != req.link_id);
|
||||
}
|
||||
|
||||
sheet.updated_at = Utc::now();
|
||||
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Json(SaveResponse {
|
||||
id: req.sheet_id,
|
||||
success: true,
|
||||
message: Some("External link removed".to_string()),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn handle_list_external_links(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(params): Query<HashMap<String, String>>,
|
||||
) -> Result<Json<ListExternalLinksResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let sheet_id = params.get("sheet_id").cloned().unwrap_or_default();
|
||||
let user_id = get_current_user_id();
|
||||
let sheet = match load_sheet_by_id(&state, &user_id, &sheet_id).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let links = sheet.external_links.unwrap_or_default();
|
||||
Ok(Json(ListExternalLinksResponse { links }))
|
||||
}
|
||||
|
||||
pub async fn handle_array_formula(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<ArrayFormulaRequest>,
|
||||
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let user_id = get_current_user_id();
|
||||
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
if req.worksheet_index >= sheet.worksheets.len() {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({ "error": "Invalid worksheet index" })),
|
||||
));
|
||||
}
|
||||
|
||||
let array_formula_id = Uuid::new_v4().to_string();
|
||||
let array_formula = ArrayFormula {
|
||||
id: array_formula_id.clone(),
|
||||
formula: req.formula.clone(),
|
||||
start_row: req.start_row,
|
||||
start_col: req.start_col,
|
||||
end_row: req.end_row,
|
||||
end_col: req.end_col,
|
||||
is_dynamic: req.formula.starts_with('=') && req.formula.contains('#'),
|
||||
};
|
||||
|
||||
let worksheet = &mut sheet.worksheets[req.worksheet_index];
|
||||
let array_formulas = worksheet.array_formulas.get_or_insert_with(Vec::new);
|
||||
array_formulas.push(array_formula);
|
||||
|
||||
for row in req.start_row..=req.end_row {
|
||||
for col in req.start_col..=req.end_col {
|
||||
let key = format!("{row},{col}");
|
||||
let cell = worksheet.data.entry(key).or_insert_with(|| CellData {
|
||||
value: None,
|
||||
formula: None,
|
||||
style: None,
|
||||
format: None,
|
||||
note: None,
|
||||
locked: None,
|
||||
has_comment: None,
|
||||
array_formula_id: None,
|
||||
});
|
||||
cell.array_formula_id = Some(array_formula_id.clone());
|
||||
if row == req.start_row && col == req.start_col {
|
||||
cell.formula = Some(format!("{{{}}}", req.formula));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sheet.updated_at = Utc::now();
|
||||
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Json(SaveResponse {
|
||||
id: req.sheet_id,
|
||||
success: true,
|
||||
message: Some("Array formula created".to_string()),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn handle_delete_array_formula(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<DeleteArrayFormulaRequest>,
|
||||
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let user_id = get_current_user_id();
|
||||
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
if req.worksheet_index >= sheet.worksheets.len() {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({ "error": "Invalid worksheet index" })),
|
||||
));
|
||||
}
|
||||
|
||||
let worksheet = &mut sheet.worksheets[req.worksheet_index];
|
||||
|
||||
if let Some(array_formulas) = &mut worksheet.array_formulas {
|
||||
array_formulas.retain(|af| af.id != req.array_formula_id);
|
||||
}
|
||||
|
||||
for cell in worksheet.data.values_mut() {
|
||||
if cell.array_formula_id.as_ref() == Some(&req.array_formula_id) {
|
||||
cell.array_formula_id = None;
|
||||
cell.formula = None;
|
||||
}
|
||||
}
|
||||
|
||||
sheet.updated_at = Utc::now();
|
||||
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Json(SaveResponse {
|
||||
id: req.sheet_id,
|
||||
success: true,
|
||||
message: Some("Array formula deleted".to_string()),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn handle_create_named_range(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<CreateNamedRangeRequest>,
|
||||
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let user_id = get_current_user_id();
|
||||
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let named_range = NamedRange {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
name: req.name,
|
||||
scope: req.scope,
|
||||
worksheet_index: req.worksheet_index,
|
||||
start_row: req.start_row,
|
||||
start_col: req.start_col,
|
||||
end_row: req.end_row,
|
||||
end_col: req.end_col,
|
||||
comment: req.comment,
|
||||
};
|
||||
|
||||
let named_ranges = sheet.named_ranges.get_or_insert_with(Vec::new);
|
||||
named_ranges.push(named_range);
|
||||
|
||||
sheet.updated_at = Utc::now();
|
||||
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Json(SaveResponse {
|
||||
id: req.sheet_id,
|
||||
success: true,
|
||||
message: Some("Named range created".to_string()),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn handle_update_named_range(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<UpdateNamedRangeRequest>,
|
||||
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let user_id = get_current_user_id();
|
||||
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(named_ranges) = &mut sheet.named_ranges {
|
||||
for range in named_ranges.iter_mut() {
|
||||
if range.id == req.range_id {
|
||||
if let Some(name) = req.name {
|
||||
range.name = name;
|
||||
}
|
||||
if let Some(start_row) = req.start_row {
|
||||
range.start_row = start_row;
|
||||
}
|
||||
if let Some(start_col) = req.start_col {
|
||||
range.start_col = start_col;
|
||||
}
|
||||
if let Some(end_row) = req.end_row {
|
||||
range.end_row = end_row;
|
||||
}
|
||||
if let Some(end_col) = req.end_col {
|
||||
range.end_col = end_col;
|
||||
}
|
||||
if let Some(comment) = req.comment {
|
||||
range.comment = Some(comment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sheet.updated_at = Utc::now();
|
||||
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Json(SaveResponse {
|
||||
id: req.sheet_id,
|
||||
success: true,
|
||||
message: Some("Named range updated".to_string()),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn handle_delete_named_range(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<DeleteNamedRangeRequest>,
|
||||
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let user_id = get_current_user_id();
|
||||
let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(named_ranges) = &mut sheet.named_ranges {
|
||||
named_ranges.retain(|r| r.id != req.range_id);
|
||||
}
|
||||
|
||||
sheet.updated_at = Utc::now();
|
||||
if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Json(SaveResponse {
|
||||
id: req.sheet_id,
|
||||
success: true,
|
||||
message: Some("Named range deleted".to_string()),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn handle_list_named_ranges(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(params): Query<HashMap<String, String>>,
|
||||
) -> Result<Json<ListNamedRangesResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let sheet_id = params.get("sheet_id").cloned().unwrap_or_default();
|
||||
let user_id = get_current_user_id();
|
||||
let sheet = match load_sheet_by_id(&state, &user_id, &sheet_id).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let ranges = sheet.named_ranges.unwrap_or_default();
|
||||
Ok(Json(ListNamedRangesResponse { ranges }))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,17 +14,23 @@ use std::sync::Arc;
|
|||
|
||||
pub use collaboration::{handle_get_collaborators, handle_sheet_websocket};
|
||||
pub use handlers::{
|
||||
handle_add_note, handle_clear_filter, handle_conditional_format, handle_create_chart,
|
||||
handle_data_validation, handle_delete_chart, handle_delete_sheet, handle_evaluate_formula,
|
||||
handle_export_sheet, handle_filter_data, handle_format_cells, handle_freeze_panes,
|
||||
handle_get_sheet_by_id, handle_import_sheet, handle_list_sheets, handle_load_from_drive,
|
||||
handle_load_sheet, handle_merge_cells, handle_new_sheet, handle_save_sheet, handle_search_sheets,
|
||||
handle_share_sheet, handle_sheet_ai, handle_sort_range, handle_unmerge_cells, handle_update_cell,
|
||||
handle_add_comment, handle_add_external_link, handle_add_note, handle_array_formula,
|
||||
handle_clear_filter, handle_conditional_format, handle_create_chart, handle_create_named_range,
|
||||
handle_data_validation, handle_delete_array_formula, handle_delete_chart, handle_delete_comment,
|
||||
handle_delete_named_range, handle_delete_sheet, handle_evaluate_formula, handle_export_sheet,
|
||||
handle_filter_data, handle_format_cells, handle_freeze_panes, handle_get_sheet_by_id,
|
||||
handle_import_sheet, handle_list_comments, handle_list_external_links, handle_list_named_ranges,
|
||||
handle_list_sheets, handle_load_from_drive, handle_load_sheet, handle_lock_cells,
|
||||
handle_merge_cells, handle_new_sheet, handle_protect_sheet, handle_refresh_external_link,
|
||||
handle_remove_external_link, handle_reply_comment, handle_resolve_comment, handle_save_sheet,
|
||||
handle_search_sheets, handle_share_sheet, handle_sheet_ai, handle_sort_range,
|
||||
handle_unmerge_cells, handle_unprotect_sheet, handle_update_cell, handle_update_named_range,
|
||||
handle_validate_cell,
|
||||
};
|
||||
pub use types::{
|
||||
CellData, CellStyle, ChartConfig, ChartDataset, ChartOptions, ChartPosition, Collaborator,
|
||||
CollabMessage, ConditionalFormatRule, FilterConfig, MergedCell, SaveResponse, Spreadsheet,
|
||||
ArrayFormula, CellComment, CellData, CellStyle, ChartConfig, ChartDataset, ChartOptions,
|
||||
ChartPosition, Collaborator, CollabMessage, CommentReply, ConditionalFormatRule, ExternalLink,
|
||||
FilterConfig, MergedCell, NamedRange, SaveResponse, SheetProtection, Spreadsheet,
|
||||
SpreadsheetMetadata, ValidationRule, Worksheet,
|
||||
};
|
||||
|
||||
|
|
@ -58,5 +64,23 @@ pub fn configure_sheet_routes() -> Router<Arc<AppState>> {
|
|||
.route("/api/sheet/ai", post(handle_sheet_ai))
|
||||
.route("/api/sheet/:id", get(handle_get_sheet_by_id))
|
||||
.route("/api/sheet/:id/collaborators", get(handle_get_collaborators))
|
||||
.route("/api/sheet/comment", post(handle_add_comment))
|
||||
.route("/api/sheet/comment/reply", post(handle_reply_comment))
|
||||
.route("/api/sheet/comment/resolve", post(handle_resolve_comment))
|
||||
.route("/api/sheet/comment/delete", post(handle_delete_comment))
|
||||
.route("/api/sheet/comments", post(handle_list_comments))
|
||||
.route("/api/sheet/protect", post(handle_protect_sheet))
|
||||
.route("/api/sheet/unprotect", post(handle_unprotect_sheet))
|
||||
.route("/api/sheet/lock-cells", post(handle_lock_cells))
|
||||
.route("/api/sheet/external-link", post(handle_add_external_link))
|
||||
.route("/api/sheet/external-link/refresh", post(handle_refresh_external_link))
|
||||
.route("/api/sheet/external-link/remove", post(handle_remove_external_link))
|
||||
.route("/api/sheet/external-links", get(handle_list_external_links))
|
||||
.route("/api/sheet/array-formula", post(handle_array_formula))
|
||||
.route("/api/sheet/array-formula/delete", post(handle_delete_array_formula))
|
||||
.route("/api/sheet/named-range", post(handle_create_named_range))
|
||||
.route("/api/sheet/named-range/update", post(handle_update_named_range))
|
||||
.route("/api/sheet/named-range/delete", post(handle_delete_named_range))
|
||||
.route("/api/sheet/named-ranges", get(handle_list_named_ranges))
|
||||
.route("/ws/sheet/:sheet_id", get(handle_sheet_websocket))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,104 @@ use chrono::{DateTime, Utc};
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CellComment {
|
||||
pub id: String,
|
||||
pub author_id: String,
|
||||
pub author_name: String,
|
||||
pub content: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
#[serde(default)]
|
||||
pub replies: Vec<CommentReply>,
|
||||
#[serde(default)]
|
||||
pub resolved: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CommentReply {
|
||||
pub id: String,
|
||||
pub author_id: String,
|
||||
pub author_name: String,
|
||||
pub content: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SheetProtection {
|
||||
pub protected: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub password_hash: Option<String>,
|
||||
#[serde(default)]
|
||||
pub locked_cells: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub allow_select_locked: bool,
|
||||
#[serde(default)]
|
||||
pub allow_select_unlocked: bool,
|
||||
#[serde(default)]
|
||||
pub allow_format_cells: bool,
|
||||
#[serde(default)]
|
||||
pub allow_format_columns: bool,
|
||||
#[serde(default)]
|
||||
pub allow_format_rows: bool,
|
||||
#[serde(default)]
|
||||
pub allow_insert_columns: bool,
|
||||
#[serde(default)]
|
||||
pub allow_insert_rows: bool,
|
||||
#[serde(default)]
|
||||
pub allow_insert_hyperlinks: bool,
|
||||
#[serde(default)]
|
||||
pub allow_delete_columns: bool,
|
||||
#[serde(default)]
|
||||
pub allow_delete_rows: bool,
|
||||
#[serde(default)]
|
||||
pub allow_sort: bool,
|
||||
#[serde(default)]
|
||||
pub allow_filter: bool,
|
||||
#[serde(default)]
|
||||
pub allow_pivot_tables: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ExternalLink {
|
||||
pub id: String,
|
||||
pub source_path: String,
|
||||
pub link_type: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub target_sheet: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub target_range: Option<String>,
|
||||
pub status: String,
|
||||
pub last_updated: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ArrayFormula {
|
||||
pub id: String,
|
||||
pub formula: String,
|
||||
pub start_row: u32,
|
||||
pub start_col: u32,
|
||||
pub end_row: u32,
|
||||
pub end_col: u32,
|
||||
#[serde(default)]
|
||||
pub is_dynamic: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NamedRange {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub scope: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub worksheet_index: Option<usize>,
|
||||
pub start_row: u32,
|
||||
pub start_col: u32,
|
||||
pub end_row: u32,
|
||||
pub end_col: u32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub comment: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CollabMessage {
|
||||
pub msg_type: String,
|
||||
|
|
@ -38,6 +136,10 @@ pub struct Spreadsheet {
|
|||
pub worksheets: Vec<Worksheet>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub named_ranges: Option<Vec<NamedRange>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub external_links: Option<Vec<ExternalLink>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
@ -64,6 +166,12 @@ pub struct Worksheet {
|
|||
pub conditional_formats: Option<Vec<ConditionalFormatRule>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub charts: Option<Vec<ChartConfig>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub comments: Option<HashMap<String, CellComment>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub protection: Option<SheetProtection>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub array_formulas: Option<Vec<ArrayFormula>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
@ -78,6 +186,12 @@ pub struct CellData {
|
|||
pub format: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub note: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub locked: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub has_comment: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub array_formula_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
|
|
@ -423,6 +537,185 @@ pub struct AddNoteRequest {
|
|||
pub note: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AddCommentRequest {
|
||||
pub sheet_id: String,
|
||||
pub worksheet_index: usize,
|
||||
pub row: u32,
|
||||
pub col: u32,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ReplyCommentRequest {
|
||||
pub sheet_id: String,
|
||||
pub worksheet_index: usize,
|
||||
pub row: u32,
|
||||
pub col: u32,
|
||||
pub comment_id: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ResolveCommentRequest {
|
||||
pub sheet_id: String,
|
||||
pub worksheet_index: usize,
|
||||
pub row: u32,
|
||||
pub col: u32,
|
||||
pub comment_id: String,
|
||||
pub resolved: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DeleteCommentRequest {
|
||||
pub sheet_id: String,
|
||||
pub worksheet_index: usize,
|
||||
pub row: u32,
|
||||
pub col: u32,
|
||||
pub comment_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProtectSheetRequest {
|
||||
pub sheet_id: String,
|
||||
pub worksheet_index: usize,
|
||||
pub protection: SheetProtection,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UnprotectSheetRequest {
|
||||
pub sheet_id: String,
|
||||
pub worksheet_index: usize,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LockCellsRequest {
|
||||
pub sheet_id: String,
|
||||
pub worksheet_index: usize,
|
||||
pub start_row: u32,
|
||||
pub start_col: u32,
|
||||
pub end_row: u32,
|
||||
pub end_col: u32,
|
||||
pub locked: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AddExternalLinkRequest {
|
||||
pub sheet_id: String,
|
||||
pub source_path: String,
|
||||
pub link_type: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub target_sheet: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub target_range: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RefreshExternalLinkRequest {
|
||||
pub sheet_id: String,
|
||||
pub link_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RemoveExternalLinkRequest {
|
||||
pub sheet_id: String,
|
||||
pub link_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ArrayFormulaRequest {
|
||||
pub sheet_id: String,
|
||||
pub worksheet_index: usize,
|
||||
pub formula: String,
|
||||
pub start_row: u32,
|
||||
pub start_col: u32,
|
||||
pub end_row: u32,
|
||||
pub end_col: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DeleteArrayFormulaRequest {
|
||||
pub sheet_id: String,
|
||||
pub worksheet_index: usize,
|
||||
pub array_formula_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreateNamedRangeRequest {
|
||||
pub sheet_id: String,
|
||||
pub name: String,
|
||||
pub scope: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub worksheet_index: Option<usize>,
|
||||
pub start_row: u32,
|
||||
pub start_col: u32,
|
||||
pub end_row: u32,
|
||||
pub end_col: u32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub comment: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateNamedRangeRequest {
|
||||
pub sheet_id: String,
|
||||
pub range_id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub start_row: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub start_col: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub end_row: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub end_col: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub comment: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DeleteNamedRangeRequest {
|
||||
pub sheet_id: String,
|
||||
pub range_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ListNamedRangesRequest {
|
||||
pub sheet_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ListNamedRangesResponse {
|
||||
pub ranges: Vec<NamedRange>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ListExternalLinksResponse {
|
||||
pub links: Vec<ExternalLink>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ListCommentsRequest {
|
||||
pub sheet_id: String,
|
||||
pub worksheet_index: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ListCommentsResponse {
|
||||
pub comments: Vec<CommentWithLocation>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CommentWithLocation {
|
||||
pub row: u32,
|
||||
pub col: u32,
|
||||
pub comment: CellComment,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SheetAiRequest {
|
||||
pub command: String,
|
||||
|
|
|
|||
|
|
@ -6,10 +6,15 @@ use crate::slides::storage::{
|
|||
load_presentation_from_drive, save_presentation_to_drive,
|
||||
};
|
||||
use crate::slides::types::{
|
||||
AddElementRequest, AddSlideRequest, ApplyThemeRequest, DeleteElementRequest,
|
||||
DeleteSlideRequest, DuplicateSlideRequest, ExportRequest, LoadQuery, Presentation,
|
||||
PresentationMetadata, ReorderSlidesRequest, SavePresentationRequest, SaveResponse, SearchQuery,
|
||||
SlidesAiRequest, SlidesAiResponse, UpdateElementRequest, UpdateSlideNotesRequest,
|
||||
AddElementRequest, AddMediaRequest, AddSlideRequest, ApplyThemeRequest,
|
||||
ApplyTransitionToAllRequest, CollaborationCursor, CollaborationSelection, DeleteElementRequest,
|
||||
DeleteMediaRequest, DeleteSlideRequest, DuplicateSlideRequest, EndPresenterRequest,
|
||||
ExportRequest, ListCursorsResponse, ListMediaResponse, ListSelectionsResponse, LoadQuery,
|
||||
Presentation, PresentationMetadata, PresenterNotesResponse, PresenterSession,
|
||||
PresenterSessionResponse, RemoveTransitionRequest, ReorderSlidesRequest,
|
||||
SavePresentationRequest, SaveResponse, SearchQuery, SetTransitionRequest, SlidesAiRequest,
|
||||
SlidesAiResponse, StartPresenterRequest, UpdateCursorRequest, UpdateElementRequest,
|
||||
UpdateMediaRequest, UpdatePresenterRequest, UpdateSelectionRequest, UpdateSlideNotesRequest,
|
||||
};
|
||||
use crate::slides::utils::export_to_html;
|
||||
use axum::{
|
||||
|
|
@ -20,7 +25,8 @@ use axum::{
|
|||
};
|
||||
use chrono::Utc;
|
||||
use log::error;
|
||||
use std::sync::Arc;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, LazyLock, RwLock};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub async fn handle_slides_ai(
|
||||
|
|
@ -623,3 +629,481 @@ pub async fn handle_export_presentation(
|
|||
)),
|
||||
}
|
||||
}
|
||||
|
||||
static CURSORS: LazyLock<RwLock<HashMap<String, Vec<CollaborationCursor>>>> =
|
||||
LazyLock::new(|| RwLock::new(HashMap::new()));
|
||||
|
||||
static SELECTIONS: LazyLock<RwLock<HashMap<String, Vec<CollaborationSelection>>>> =
|
||||
LazyLock::new(|| RwLock::new(HashMap::new()));
|
||||
|
||||
static PRESENTER_SESSIONS: LazyLock<RwLock<HashMap<String, PresenterSession>>> =
|
||||
LazyLock::new(|| RwLock::new(HashMap::new()));
|
||||
|
||||
pub async fn handle_update_cursor(
|
||||
State(_state): State<Arc<AppState>>,
|
||||
Json(req): Json<UpdateCursorRequest>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let user_id = get_current_user_id();
|
||||
|
||||
let cursor = CollaborationCursor {
|
||||
user_id: user_id.clone(),
|
||||
user_name: "User".to_string(),
|
||||
user_color: "#4285f4".to_string(),
|
||||
slide_index: req.slide_index,
|
||||
element_id: req.element_id,
|
||||
x: req.x,
|
||||
y: req.y,
|
||||
last_activity: Utc::now(),
|
||||
};
|
||||
|
||||
if let Ok(mut cursors) = CURSORS.write() {
|
||||
let presentation_cursors = cursors.entry(req.presentation_id.clone()).or_default();
|
||||
presentation_cursors.retain(|c| c.user_id != user_id);
|
||||
presentation_cursors.push(cursor);
|
||||
}
|
||||
|
||||
Ok(Json(serde_json::json!({ "success": true })))
|
||||
}
|
||||
|
||||
pub async fn handle_update_selection(
|
||||
State(_state): State<Arc<AppState>>,
|
||||
Json(req): Json<UpdateSelectionRequest>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let user_id = get_current_user_id();
|
||||
|
||||
let selection = CollaborationSelection {
|
||||
user_id: user_id.clone(),
|
||||
user_name: "User".to_string(),
|
||||
user_color: "#4285f4".to_string(),
|
||||
slide_index: req.slide_index,
|
||||
element_ids: req.element_ids,
|
||||
};
|
||||
|
||||
if let Ok(mut selections) = SELECTIONS.write() {
|
||||
let presentation_selections = selections.entry(req.presentation_id.clone()).or_default();
|
||||
presentation_selections.retain(|s| s.user_id != user_id);
|
||||
presentation_selections.push(selection);
|
||||
}
|
||||
|
||||
Ok(Json(serde_json::json!({ "success": true })))
|
||||
}
|
||||
|
||||
pub async fn handle_list_cursors(
|
||||
State(_state): State<Arc<AppState>>,
|
||||
Query(params): Query<HashMap<String, String>>,
|
||||
) -> Result<Json<ListCursorsResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let presentation_id = params.get("presentation_id").cloned().unwrap_or_default();
|
||||
|
||||
let cursors = if let Ok(cursors_map) = CURSORS.read() {
|
||||
cursors_map.get(&presentation_id).cloned().unwrap_or_default()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
Ok(Json(ListCursorsResponse { cursors }))
|
||||
}
|
||||
|
||||
pub async fn handle_list_selections(
|
||||
State(_state): State<Arc<AppState>>,
|
||||
Query(params): Query<HashMap<String, String>>,
|
||||
) -> Result<Json<ListSelectionsResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let presentation_id = params.get("presentation_id").cloned().unwrap_or_default();
|
||||
|
||||
let selections = if let Ok(selections_map) = SELECTIONS.read() {
|
||||
selections_map.get(&presentation_id).cloned().unwrap_or_default()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
Ok(Json(ListSelectionsResponse { selections }))
|
||||
}
|
||||
|
||||
pub async fn handle_set_transition(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<SetTransitionRequest>,
|
||||
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let user_id = get_current_user_id();
|
||||
let mut presentation = match load_presentation_by_id(&state, &user_id, &req.presentation_id).await {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
if req.slide_index >= presentation.slides.len() {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({ "error": "Invalid slide index" })),
|
||||
));
|
||||
}
|
||||
|
||||
presentation.slides[req.slide_index].transition_config = Some(req.transition);
|
||||
presentation.updated_at = Utc::now();
|
||||
|
||||
if let Err(e) = save_presentation_to_drive(&state, &user_id, &presentation).await {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Json(SaveResponse {
|
||||
id: req.presentation_id,
|
||||
success: true,
|
||||
message: Some("Transition set".to_string()),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn handle_apply_transition_to_all(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<ApplyTransitionToAllRequest>,
|
||||
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let user_id = get_current_user_id();
|
||||
let mut presentation = match load_presentation_by_id(&state, &user_id, &req.presentation_id).await {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
for slide in presentation.slides.iter_mut() {
|
||||
slide.transition_config = Some(req.transition.clone());
|
||||
}
|
||||
presentation.updated_at = Utc::now();
|
||||
|
||||
if let Err(e) = save_presentation_to_drive(&state, &user_id, &presentation).await {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Json(SaveResponse {
|
||||
id: req.presentation_id,
|
||||
success: true,
|
||||
message: Some("Transition applied to all slides".to_string()),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn handle_remove_transition(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<RemoveTransitionRequest>,
|
||||
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let user_id = get_current_user_id();
|
||||
let mut presentation = match load_presentation_by_id(&state, &user_id, &req.presentation_id).await {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
if req.slide_index >= presentation.slides.len() {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({ "error": "Invalid slide index" })),
|
||||
));
|
||||
}
|
||||
|
||||
presentation.slides[req.slide_index].transition_config = None;
|
||||
presentation.slides[req.slide_index].transition = None;
|
||||
presentation.updated_at = Utc::now();
|
||||
|
||||
if let Err(e) = save_presentation_to_drive(&state, &user_id, &presentation).await {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Json(SaveResponse {
|
||||
id: req.presentation_id,
|
||||
success: true,
|
||||
message: Some("Transition removed".to_string()),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn handle_add_media(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<AddMediaRequest>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let user_id = get_current_user_id();
|
||||
let mut presentation = match load_presentation_by_id(&state, &user_id, &req.presentation_id).await {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
if req.slide_index >= presentation.slides.len() {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({ "error": "Invalid slide index" })),
|
||||
));
|
||||
}
|
||||
|
||||
let media_list = presentation.slides[req.slide_index].media.get_or_insert_with(Vec::new);
|
||||
media_list.push(req.media.clone());
|
||||
presentation.updated_at = Utc::now();
|
||||
|
||||
if let Err(e) = save_presentation_to_drive(&state, &user_id, &presentation).await {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Json(serde_json::json!({ "success": true, "media": req.media })))
|
||||
}
|
||||
|
||||
pub async fn handle_update_media(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<UpdateMediaRequest>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let user_id = get_current_user_id();
|
||||
let mut presentation = match load_presentation_by_id(&state, &user_id, &req.presentation_id).await {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
if req.slide_index >= presentation.slides.len() {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({ "error": "Invalid slide index" })),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(media_list) = &mut presentation.slides[req.slide_index].media {
|
||||
for media in media_list.iter_mut() {
|
||||
if media.id == req.media_id {
|
||||
if let Some(autoplay) = req.autoplay {
|
||||
media.autoplay = autoplay;
|
||||
}
|
||||
if let Some(loop_playback) = req.loop_playback {
|
||||
media.loop_playback = loop_playback;
|
||||
}
|
||||
if let Some(muted) = req.muted {
|
||||
media.muted = muted;
|
||||
}
|
||||
if let Some(volume) = req.volume {
|
||||
media.volume = Some(volume);
|
||||
}
|
||||
if let Some(start_time) = req.start_time {
|
||||
media.start_time = Some(start_time);
|
||||
}
|
||||
if let Some(end_time) = req.end_time {
|
||||
media.end_time = Some(end_time);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
presentation.updated_at = Utc::now();
|
||||
if let Err(e) = save_presentation_to_drive(&state, &user_id, &presentation).await {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Json(serde_json::json!({ "success": true })))
|
||||
}
|
||||
|
||||
pub async fn handle_delete_media(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<DeleteMediaRequest>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let user_id = get_current_user_id();
|
||||
let mut presentation = match load_presentation_by_id(&state, &user_id, &req.presentation_id).await {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
if req.slide_index >= presentation.slides.len() {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({ "error": "Invalid slide index" })),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(media_list) = &mut presentation.slides[req.slide_index].media {
|
||||
media_list.retain(|m| m.id != req.media_id);
|
||||
}
|
||||
|
||||
presentation.updated_at = Utc::now();
|
||||
if let Err(e) = save_presentation_to_drive(&state, &user_id, &presentation).await {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Json(serde_json::json!({ "success": true })))
|
||||
}
|
||||
|
||||
pub async fn handle_list_media(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(params): Query<HashMap<String, String>>,
|
||||
) -> Result<Json<ListMediaResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let presentation_id = params.get("presentation_id").cloned().unwrap_or_default();
|
||||
let slide_index: usize = params.get("slide_index").and_then(|s| s.parse().ok()).unwrap_or(0);
|
||||
let user_id = get_current_user_id();
|
||||
|
||||
let presentation = match load_presentation_by_id(&state, &user_id, &presentation_id).await {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
if slide_index >= presentation.slides.len() {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({ "error": "Invalid slide index" })),
|
||||
));
|
||||
}
|
||||
|
||||
let media = presentation.slides[slide_index].media.clone().unwrap_or_default();
|
||||
Ok(Json(ListMediaResponse { media }))
|
||||
}
|
||||
|
||||
pub async fn handle_start_presenter(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<StartPresenterRequest>,
|
||||
) -> Result<Json<PresenterSessionResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let user_id = get_current_user_id();
|
||||
|
||||
let _presentation = match load_presentation_by_id(&state, &user_id, &req.presentation_id).await {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let session = PresenterSession {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
presentation_id: req.presentation_id,
|
||||
current_slide: 0,
|
||||
started_at: Utc::now(),
|
||||
elapsed_time: Some(0),
|
||||
is_paused: false,
|
||||
settings: req.settings,
|
||||
};
|
||||
|
||||
if let Ok(mut sessions) = PRESENTER_SESSIONS.write() {
|
||||
sessions.insert(session.id.clone(), session.clone());
|
||||
}
|
||||
|
||||
Ok(Json(PresenterSessionResponse { session }))
|
||||
}
|
||||
|
||||
pub async fn handle_update_presenter(
|
||||
State(_state): State<Arc<AppState>>,
|
||||
Json(req): Json<UpdatePresenterRequest>,
|
||||
) -> Result<Json<PresenterSessionResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let session = if let Ok(mut sessions) = PRESENTER_SESSIONS.write() {
|
||||
if let Some(session) = sessions.get_mut(&req.session_id) {
|
||||
if let Some(current_slide) = req.current_slide {
|
||||
session.current_slide = current_slide;
|
||||
}
|
||||
if let Some(is_paused) = req.is_paused {
|
||||
session.is_paused = is_paused;
|
||||
}
|
||||
if let Some(settings) = req.settings {
|
||||
session.settings = settings;
|
||||
}
|
||||
session.clone()
|
||||
} else {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({ "error": "Session not found" })),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": "Failed to access sessions" })),
|
||||
));
|
||||
};
|
||||
|
||||
Ok(Json(PresenterSessionResponse { session }))
|
||||
}
|
||||
|
||||
pub async fn handle_end_presenter(
|
||||
State(_state): State<Arc<AppState>>,
|
||||
Json(req): Json<EndPresenterRequest>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
|
||||
if let Ok(mut sessions) = PRESENTER_SESSIONS.write() {
|
||||
sessions.remove(&req.session_id);
|
||||
}
|
||||
|
||||
Ok(Json(serde_json::json!({ "success": true })))
|
||||
}
|
||||
|
||||
pub async fn handle_get_presenter_notes(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(params): Query<HashMap<String, String>>,
|
||||
) -> Result<Json<PresenterNotesResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let presentation_id = params.get("presentation_id").cloned().unwrap_or_default();
|
||||
let slide_index: usize = params.get("slide_index").and_then(|s| s.parse().ok()).unwrap_or(0);
|
||||
let user_id = get_current_user_id();
|
||||
|
||||
let presentation = match load_presentation_by_id(&state, &user_id, &presentation_id).await {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({ "error": e })),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
if slide_index >= presentation.slides.len() {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({ "error": "Invalid slide index" })),
|
||||
));
|
||||
}
|
||||
|
||||
let notes = presentation.slides[slide_index].notes.clone();
|
||||
let next_slide_notes = if slide_index + 1 < presentation.slides.len() {
|
||||
presentation.slides[slide_index + 1].notes.clone()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Json(PresenterNotesResponse {
|
||||
slide_index,
|
||||
notes,
|
||||
next_slide_notes,
|
||||
next_slide_thumbnail: None,
|
||||
}))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,18 +14,25 @@ use std::sync::Arc;
|
|||
|
||||
pub use collaboration::{handle_get_collaborators, handle_slides_websocket};
|
||||
pub use handlers::{
|
||||
handle_add_element, handle_add_slide, handle_apply_theme, handle_delete_element,
|
||||
handle_add_element, handle_add_media, handle_add_slide, handle_apply_theme,
|
||||
handle_apply_transition_to_all, handle_delete_element, handle_delete_media,
|
||||
handle_delete_presentation, handle_delete_slide, handle_duplicate_slide,
|
||||
handle_export_presentation, handle_get_presentation_by_id, handle_list_presentations,
|
||||
handle_load_presentation, handle_new_presentation, handle_reorder_slides,
|
||||
handle_save_presentation, handle_search_presentations, handle_slides_ai,
|
||||
handle_update_element, handle_update_slide_notes,
|
||||
handle_end_presenter, handle_export_presentation, handle_get_presentation_by_id,
|
||||
handle_get_presenter_notes, handle_list_cursors, handle_list_media,
|
||||
handle_list_presentations, handle_list_selections, handle_load_presentation,
|
||||
handle_new_presentation, handle_remove_transition, handle_reorder_slides,
|
||||
handle_save_presentation, handle_search_presentations, handle_set_transition,
|
||||
handle_slides_ai, handle_start_presenter, handle_update_cursor, handle_update_element,
|
||||
handle_update_media, handle_update_presenter, handle_update_selection,
|
||||
handle_update_slide_notes,
|
||||
};
|
||||
pub use types::{
|
||||
Animation, ChartData, ChartDataset, Collaborator, ElementContent, ElementStyle,
|
||||
GradientStop, GradientStyle, Presentation, PresentationMetadata, PresentationTheme,
|
||||
SaveResponse, ShadowStyle, Slide, SlideBackground, SlideElement, SlideMessage,
|
||||
SlideTransition, TableCell, TableData, ThemeColors, ThemeFonts,
|
||||
Animation, ChartData, ChartDataset, Collaborator, CollaborationCursor,
|
||||
CollaborationSelection, ElementContent, ElementStyle, GradientStop, GradientStyle,
|
||||
MediaElement, Presentation, PresentationMetadata, PresentationTheme, PresenterSession,
|
||||
PresenterViewSettings, SaveResponse, ShadowStyle, Slide, SlideBackground, SlideElement,
|
||||
SlideMessage, SlideTransition, TableCell, TableData, ThemeColors, ThemeFonts,
|
||||
TransitionConfig, TransitionSound,
|
||||
};
|
||||
|
||||
pub fn configure_slides_routes() -> Router<Arc<AppState>> {
|
||||
|
|
@ -49,5 +56,20 @@ pub fn configure_slides_routes() -> Router<Arc<AppState>> {
|
|||
.route("/api/slides/element/delete", post(handle_delete_element))
|
||||
.route("/api/slides/theme", post(handle_apply_theme))
|
||||
.route("/api/slides/export", post(handle_export_presentation))
|
||||
.route("/api/slides/cursor", post(handle_update_cursor))
|
||||
.route("/api/slides/selection", post(handle_update_selection))
|
||||
.route("/api/slides/cursors", get(handle_list_cursors))
|
||||
.route("/api/slides/selections", get(handle_list_selections))
|
||||
.route("/api/slides/transition", post(handle_set_transition))
|
||||
.route("/api/slides/transition/all", post(handle_apply_transition_to_all))
|
||||
.route("/api/slides/transition/remove", post(handle_remove_transition))
|
||||
.route("/api/slides/media", post(handle_add_media))
|
||||
.route("/api/slides/media/update", post(handle_update_media))
|
||||
.route("/api/slides/media/delete", post(handle_delete_media))
|
||||
.route("/api/slides/media/list", get(handle_list_media))
|
||||
.route("/api/slides/presenter/start", post(handle_start_presenter))
|
||||
.route("/api/slides/presenter/update", post(handle_update_presenter))
|
||||
.route("/api/slides/presenter/end", post(handle_end_presenter))
|
||||
.route("/api/slides/presenter/notes", get(handle_get_presenter_notes))
|
||||
.route("/ws/slides/:presentation_id", get(handle_slides_websocket))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,105 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CollaborationCursor {
|
||||
pub user_id: String,
|
||||
pub user_name: String,
|
||||
pub user_color: String,
|
||||
pub slide_index: usize,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub element_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub x: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub y: Option<f64>,
|
||||
pub last_activity: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CollaborationSelection {
|
||||
pub user_id: String,
|
||||
pub user_name: String,
|
||||
pub user_color: String,
|
||||
pub slide_index: usize,
|
||||
pub element_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TransitionConfig {
|
||||
pub transition_type: String,
|
||||
pub duration: f64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub direction: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub easing: Option<String>,
|
||||
#[serde(default)]
|
||||
pub advance_on_click: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub advance_after_time: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sound: Option<TransitionSound>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TransitionSound {
|
||||
pub src: String,
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub loop_until_next: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MediaElement {
|
||||
pub id: String,
|
||||
pub media_type: String,
|
||||
pub src: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub poster: Option<String>,
|
||||
#[serde(default)]
|
||||
pub autoplay: bool,
|
||||
#[serde(default)]
|
||||
pub loop_playback: bool,
|
||||
#[serde(default)]
|
||||
pub muted: bool,
|
||||
#[serde(default)]
|
||||
pub controls: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub start_time: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub end_time: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub volume: Option<f64>,
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
pub width: f64,
|
||||
pub height: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PresenterViewSettings {
|
||||
pub show_notes: bool,
|
||||
pub show_next_slide: bool,
|
||||
pub show_timer: bool,
|
||||
pub show_clock: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub notes_font_size: Option<f64>,
|
||||
#[serde(default)]
|
||||
pub zoom_level: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PresenterSession {
|
||||
pub id: String,
|
||||
pub presentation_id: String,
|
||||
pub current_slide: usize,
|
||||
pub started_at: DateTime<Utc>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub elapsed_time: Option<u64>,
|
||||
pub is_paused: bool,
|
||||
pub settings: PresenterViewSettings,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SlideMessage {
|
||||
pub msg_type: String,
|
||||
|
|
@ -47,6 +146,10 @@ pub struct Slide {
|
|||
pub notes: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub transition: Option<SlideTransition>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub transition_config: Option<TransitionConfig>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub media: Option<Vec<MediaElement>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
@ -357,3 +460,124 @@ pub struct LoadFromDriveRequest {
|
|||
pub bucket: String,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateCursorRequest {
|
||||
pub presentation_id: String,
|
||||
pub slide_index: usize,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub element_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub x: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub y: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateSelectionRequest {
|
||||
pub presentation_id: String,
|
||||
pub slide_index: usize,
|
||||
pub element_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ListCursorsResponse {
|
||||
pub cursors: Vec<CollaborationCursor>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ListSelectionsResponse {
|
||||
pub selections: Vec<CollaborationSelection>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SetTransitionRequest {
|
||||
pub presentation_id: String,
|
||||
pub slide_index: usize,
|
||||
pub transition: TransitionConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ApplyTransitionToAllRequest {
|
||||
pub presentation_id: String,
|
||||
pub transition: TransitionConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RemoveTransitionRequest {
|
||||
pub presentation_id: String,
|
||||
pub slide_index: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AddMediaRequest {
|
||||
pub presentation_id: String,
|
||||
pub slide_index: usize,
|
||||
pub media: MediaElement,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateMediaRequest {
|
||||
pub presentation_id: String,
|
||||
pub slide_index: usize,
|
||||
pub media_id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub autoplay: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub loop_playback: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub muted: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub volume: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub start_time: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub end_time: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DeleteMediaRequest {
|
||||
pub presentation_id: String,
|
||||
pub slide_index: usize,
|
||||
pub media_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ListMediaResponse {
|
||||
pub media: Vec<MediaElement>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StartPresenterRequest {
|
||||
pub presentation_id: String,
|
||||
pub settings: PresenterViewSettings,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdatePresenterRequest {
|
||||
pub session_id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub current_slide: Option<usize>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub is_paused: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub settings: Option<PresenterViewSettings>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EndPresenterRequest {
|
||||
pub session_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PresenterSessionResponse {
|
||||
pub session: PresenterSession,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PresenterNotesResponse {
|
||||
pub slide_index: usize,
|
||||
pub notes: Option<String>,
|
||||
pub next_slide_notes: Option<String>,
|
||||
pub next_slide_thumbnail: Option<String>,
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue