1109 lines
36 KiB
Rust
1109 lines
36 KiB
Rust
use crate::shared::state::AppState;
|
|
use crate::slides::collaboration::broadcast_slide_change;
|
|
use crate::slides::storage::{
|
|
create_new_presentation, create_slide_with_layout, delete_presentation_from_drive,
|
|
get_current_user_id, list_presentations_from_drive, load_presentation_by_id,
|
|
load_presentation_from_drive, save_presentation_to_drive,
|
|
};
|
|
use crate::slides::types::{
|
|
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::{
|
|
extract::{Path, Query, State},
|
|
http::StatusCode,
|
|
response::IntoResponse,
|
|
Json,
|
|
};
|
|
use chrono::Utc;
|
|
use log::error;
|
|
use std::collections::HashMap;
|
|
use std::sync::{Arc, LazyLock, RwLock};
|
|
use uuid::Uuid;
|
|
|
|
pub async fn handle_slides_ai(
|
|
State(_state): State<Arc<AppState>>,
|
|
Json(req): Json<SlidesAiRequest>,
|
|
) -> impl IntoResponse {
|
|
let command = req.command.to_lowercase();
|
|
|
|
let response = if command.contains("add") && command.contains("slide") {
|
|
"I've added a new slide to your presentation."
|
|
} else if command.contains("duplicate") {
|
|
"I've duplicated the current slide."
|
|
} else if command.contains("delete") || command.contains("remove") {
|
|
"I've removed the slide from your presentation."
|
|
} else if command.contains("text") || command.contains("title") {
|
|
"I've added a text box to your slide. Click to edit."
|
|
} else if command.contains("image") || command.contains("picture") {
|
|
"I've added an image placeholder. Click to upload an image."
|
|
} else if command.contains("shape") {
|
|
"I've added a shape to your slide. You can resize and move it."
|
|
} else if command.contains("chart") {
|
|
"I've added a chart. Click to edit the data."
|
|
} else if command.contains("table") {
|
|
"I've added a table. Click cells to edit."
|
|
} else if command.contains("theme") || command.contains("design") {
|
|
"I can help you change the theme. Choose from the Design menu."
|
|
} else if command.contains("animate") || command.contains("animation") {
|
|
"I've added an animation to the selected element."
|
|
} else if command.contains("transition") {
|
|
"I've applied a transition effect to this slide."
|
|
} else if command.contains("help") {
|
|
"I can help you with:\n• Add/duplicate/delete slides\n• Insert text, images, shapes\n• Add charts and tables\n• Apply themes and animations\n• Set slide transitions"
|
|
} else {
|
|
"I understand you want help with your presentation. Try commands like 'add slide', 'insert image', 'add chart', or 'apply animation'."
|
|
};
|
|
|
|
Json(SlidesAiResponse {
|
|
response: response.to_string(),
|
|
action: None,
|
|
data: None,
|
|
})
|
|
}
|
|
|
|
pub async fn handle_new_presentation(
|
|
State(_state): State<Arc<AppState>>,
|
|
) -> Result<Json<Presentation>, (StatusCode, Json<serde_json::Value>)> {
|
|
Ok(Json(create_new_presentation()))
|
|
}
|
|
|
|
pub async fn handle_list_presentations(
|
|
State(state): State<Arc<AppState>>,
|
|
) -> Result<Json<Vec<PresentationMetadata>>, (StatusCode, Json<serde_json::Value>)> {
|
|
let user_id = get_current_user_id();
|
|
|
|
match list_presentations_from_drive(&state, &user_id).await {
|
|
Ok(presentations) => Ok(Json(presentations)),
|
|
Err(e) => {
|
|
error!("Failed to list presentations: {}", e);
|
|
Ok(Json(Vec::new()))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn handle_search_presentations(
|
|
State(state): State<Arc<AppState>>,
|
|
Query(query): Query<SearchQuery>,
|
|
) -> Result<Json<Vec<PresentationMetadata>>, (StatusCode, Json<serde_json::Value>)> {
|
|
let user_id = get_current_user_id();
|
|
|
|
let presentations = match list_presentations_from_drive(&state, &user_id).await {
|
|
Ok(p) => p,
|
|
Err(_) => Vec::new(),
|
|
};
|
|
|
|
let filtered = if let Some(q) = query.q {
|
|
let q_lower = q.to_lowercase();
|
|
presentations
|
|
.into_iter()
|
|
.filter(|p| p.name.to_lowercase().contains(&q_lower))
|
|
.collect()
|
|
} else {
|
|
presentations
|
|
};
|
|
|
|
Ok(Json(filtered))
|
|
}
|
|
|
|
pub async fn handle_load_presentation(
|
|
State(state): State<Arc<AppState>>,
|
|
Query(query): Query<LoadQuery>,
|
|
) -> Result<Json<Presentation>, (StatusCode, Json<serde_json::Value>)> {
|
|
let user_id = get_current_user_id();
|
|
|
|
match load_presentation_from_drive(&state, &user_id, &query.id).await {
|
|
Ok(presentation) => Ok(Json(presentation)),
|
|
Err(e) => Err((
|
|
StatusCode::NOT_FOUND,
|
|
Json(serde_json::json!({ "error": e })),
|
|
)),
|
|
}
|
|
}
|
|
|
|
pub async fn handle_save_presentation(
|
|
State(state): State<Arc<AppState>>,
|
|
Json(req): Json<SavePresentationRequest>,
|
|
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
|
|
let user_id = get_current_user_id();
|
|
let presentation_id = req.id.unwrap_or_else(|| Uuid::new_v4().to_string());
|
|
|
|
let presentation = Presentation {
|
|
id: presentation_id.clone(),
|
|
name: req.name,
|
|
owner_id: user_id.clone(),
|
|
slides: req.slides,
|
|
theme: req.theme,
|
|
created_at: Utc::now(),
|
|
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: presentation_id,
|
|
success: true,
|
|
message: Some("Presentation saved successfully".to_string()),
|
|
}))
|
|
}
|
|
|
|
pub async fn handle_delete_presentation(
|
|
State(state): State<Arc<AppState>>,
|
|
Json(req): Json<LoadQuery>,
|
|
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
|
|
let user_id = get_current_user_id();
|
|
|
|
if let Err(e) = delete_presentation_from_drive(&state, &user_id, &req.id).await {
|
|
return Err((
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(serde_json::json!({ "error": e })),
|
|
));
|
|
}
|
|
|
|
Ok(Json(SaveResponse {
|
|
id: req.id.unwrap_or_default(),
|
|
success: true,
|
|
message: Some("Presentation deleted".to_string()),
|
|
}))
|
|
}
|
|
|
|
pub async fn handle_get_presentation_by_id(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(presentation_id): Path<String>,
|
|
) -> Result<Json<Presentation>, (StatusCode, Json<serde_json::Value>)> {
|
|
let user_id = get_current_user_id();
|
|
match load_presentation_by_id(&state, &user_id, &presentation_id).await {
|
|
Ok(presentation) => Ok(Json(presentation)),
|
|
Err(e) => Err((
|
|
StatusCode::NOT_FOUND,
|
|
Json(serde_json::json!({ "error": e })),
|
|
)),
|
|
}
|
|
}
|
|
|
|
pub async fn handle_add_slide(
|
|
State(state): State<Arc<AppState>>,
|
|
Json(req): Json<AddSlideRequest>,
|
|
) -> 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 })),
|
|
))
|
|
}
|
|
};
|
|
|
|
let new_slide = create_slide_with_layout(&req.layout, &presentation.theme);
|
|
|
|
if let Some(position) = req.position {
|
|
if position <= presentation.slides.len() {
|
|
presentation.slides.insert(position, new_slide);
|
|
} else {
|
|
presentation.slides.push(new_slide);
|
|
}
|
|
} else {
|
|
presentation.slides.push(new_slide);
|
|
}
|
|
|
|
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("Slide added".to_string()),
|
|
}))
|
|
}
|
|
|
|
pub async fn handle_delete_slide(
|
|
State(state): State<Arc<AppState>>,
|
|
Json(req): Json<DeleteSlideRequest>,
|
|
) -> 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.remove(req.slide_index);
|
|
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("Slide deleted".to_string()),
|
|
}))
|
|
}
|
|
|
|
pub async fn handle_duplicate_slide(
|
|
State(state): State<Arc<AppState>>,
|
|
Json(req): Json<DuplicateSlideRequest>,
|
|
) -> 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" })),
|
|
));
|
|
}
|
|
|
|
let mut duplicated = presentation.slides[req.slide_index].clone();
|
|
duplicated.id = Uuid::new_v4().to_string();
|
|
presentation.slides.insert(req.slide_index + 1, duplicated);
|
|
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("Slide duplicated".to_string()),
|
|
}))
|
|
}
|
|
|
|
pub async fn handle_reorder_slides(
|
|
State(state): State<Arc<AppState>>,
|
|
Json(req): Json<ReorderSlidesRequest>,
|
|
) -> 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_order.len() != presentation.slides.len() {
|
|
return Err((
|
|
StatusCode::BAD_REQUEST,
|
|
Json(serde_json::json!({ "error": "Invalid slide order" })),
|
|
));
|
|
}
|
|
|
|
let old_slides = presentation.slides.clone();
|
|
presentation.slides = req
|
|
.slide_order
|
|
.iter()
|
|
.filter_map(|&idx| old_slides.get(idx).cloned())
|
|
.collect();
|
|
|
|
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("Slides reordered".to_string()),
|
|
}))
|
|
}
|
|
|
|
pub async fn handle_update_slide_notes(
|
|
State(state): State<Arc<AppState>>,
|
|
Json(req): Json<UpdateSlideNotesRequest>,
|
|
) -> 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].notes = Some(req.notes);
|
|
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("Slide notes updated".to_string()),
|
|
}))
|
|
}
|
|
|
|
pub async fn handle_add_element(
|
|
State(state): State<Arc<AppState>>,
|
|
Json(req): Json<AddElementRequest>,
|
|
) -> 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].elements.push(req.element);
|
|
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 })),
|
|
));
|
|
}
|
|
|
|
broadcast_slide_change(
|
|
&req.presentation_id,
|
|
&user_id,
|
|
"User",
|
|
"element_added",
|
|
Some(req.slide_index),
|
|
None,
|
|
None,
|
|
)
|
|
.await;
|
|
|
|
Ok(Json(SaveResponse {
|
|
id: req.presentation_id,
|
|
success: true,
|
|
message: Some("Element added".to_string()),
|
|
}))
|
|
}
|
|
|
|
pub async fn handle_update_element(
|
|
State(state): State<Arc<AppState>>,
|
|
Json(req): Json<UpdateElementRequest>,
|
|
) -> 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" })),
|
|
));
|
|
}
|
|
|
|
let slide = &mut presentation.slides[req.slide_index];
|
|
if let Some(pos) = slide.elements.iter().position(|e| e.id == req.element.id) {
|
|
slide.elements[pos] = req.element.clone();
|
|
} else {
|
|
slide.elements.push(req.element.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 })),
|
|
));
|
|
}
|
|
|
|
broadcast_slide_change(
|
|
&req.presentation_id,
|
|
&user_id,
|
|
"User",
|
|
"element_updated",
|
|
Some(req.slide_index),
|
|
Some(&req.element.id),
|
|
None,
|
|
)
|
|
.await;
|
|
|
|
Ok(Json(SaveResponse {
|
|
id: req.presentation_id,
|
|
success: true,
|
|
message: Some("Element updated".to_string()),
|
|
}))
|
|
}
|
|
|
|
pub async fn handle_delete_element(
|
|
State(state): State<Arc<AppState>>,
|
|
Json(req): Json<DeleteElementRequest>,
|
|
) -> 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]
|
|
.elements
|
|
.retain(|e| e.id != req.element_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(SaveResponse {
|
|
id: req.presentation_id,
|
|
success: true,
|
|
message: Some("Element deleted".to_string()),
|
|
}))
|
|
}
|
|
|
|
pub async fn handle_apply_theme(
|
|
State(state): State<Arc<AppState>>,
|
|
Json(req): Json<ApplyThemeRequest>,
|
|
) -> 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 })),
|
|
))
|
|
}
|
|
};
|
|
|
|
presentation.theme = req.theme;
|
|
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("Theme applied".to_string()),
|
|
}))
|
|
}
|
|
|
|
pub async fn handle_export_presentation(
|
|
State(state): State<Arc<AppState>>,
|
|
Json(req): Json<ExportRequest>,
|
|
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
|
let user_id = get_current_user_id();
|
|
|
|
let presentation = match load_presentation_by_id(&state, &user_id, &req.id).await {
|
|
Ok(p) => p,
|
|
Err(e) => {
|
|
return Err((
|
|
StatusCode::NOT_FOUND,
|
|
Json(serde_json::json!({ "error": e })),
|
|
))
|
|
}
|
|
};
|
|
|
|
match req.format.as_str() {
|
|
"html" => {
|
|
let html = export_to_html(&presentation);
|
|
Ok(([(axum::http::header::CONTENT_TYPE, "text/html")], html))
|
|
}
|
|
"json" => {
|
|
let json = serde_json::to_string_pretty(&presentation).unwrap_or_default();
|
|
Ok(([(axum::http::header::CONTENT_TYPE, "application/json")], json))
|
|
}
|
|
"pptx" => {
|
|
Ok((
|
|
[(
|
|
axum::http::header::CONTENT_TYPE,
|
|
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
)],
|
|
"PPTX export not yet implemented".to_string(),
|
|
))
|
|
}
|
|
_ => Err((
|
|
StatusCode::BAD_REQUEST,
|
|
Json(serde_json::json!({ "error": "Unsupported format" })),
|
|
)),
|
|
}
|
|
}
|
|
|
|
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,
|
|
}))
|
|
}
|