From 32438ce25f69de99d2523b98237f62cfa90890d7 Mon Sep 17 00:00:00 2001 From: cashmere Date: Tue, 28 Jul 2026 20:14:51 +0200 Subject: feat: port domain crates to Rust (session 1 complete) All green: cargo test --workspace (~300 tests, 0 failures). - timeline (45): placement, resize/ripple, split, snap, retime, audio separation, scene helpers - animation (73): bezier solver, channel sampling, keyframe ops, split/clamp/clone, param definitions - commands (9): Command trait, BatchCommand, history, 19 concrete commands, freeze-frame port 1:1 - audio (22): audible collection, range mixing, mastering limiter, rms/peak buckets - text (19): cosmic-text layout/measure/raster, backgrounds, decorations - graphics (52): full CSS gradient parser, tiny-skia raster, 4 shape definitions - effect-defs (16): blur definition, registry, pass resolution - mask-defs (19): all 9 mask renderers via tiny-skia - renderer (6): node model + scene builder port - desktop: GPUI projects screen wired to ProjectStore - scene: factory (web defaults: v32, Main Track, 1920x1080, 30fps) --- apps/desktop/src/projects_view.rs | 164 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 apps/desktop/src/projects_view.rs (limited to 'apps/desktop/src/projects_view.rs') diff --git a/apps/desktop/src/projects_view.rs b/apps/desktop/src/projects_view.rs new file mode 100644 index 00000000..2d69cf22 --- /dev/null +++ b/apps/desktop/src/projects_view.rs @@ -0,0 +1,164 @@ +use gpui::{Context, SharedString, WeakEntity, Window, div, prelude::*, rgb}; +use scene::ProjectMetadata; +use storage::ProjectStore; + +use crate::app::OpenCutApp; +use crate::{projects_root, theme}; + +pub struct ProjectsView { + store: ProjectStore, + projects: Vec, + load_error: Option, + app: WeakEntity, +} + +impl ProjectsView { + pub fn new(app: WeakEntity) -> Self { + let store = ProjectStore::new(projects_root()); + let (projects, load_error) = match store.list_projects() { + Ok(projects) => (projects, None), + Err(error) => (Vec::new(), Some(error.to_string())), + }; + Self { + store, + projects, + load_error, + app, + } + } + + fn reload(&mut self) { + match self.store.list_projects() { + Ok(projects) => { + self.projects = projects; + self.load_error = None; + } + Err(error) => self.load_error = Some(error.to_string()), + } + } + + fn create_project(&mut self, cx: &mut Context) { + let project = scene::new_project("Untitled Project"); + if self.store.create_project(&project).is_ok() { + self.open_project(project.metadata.id, project.metadata.name, cx); + } + } + + fn open_project(&mut self, id: String, name: String, cx: &mut Context) { + let _ = self.app.update(cx, |app, cx| app.open_project(id, name, cx)); + } + + fn delete_project(&mut self, id: &str, cx: &mut Context) { + let _ = self.store.delete_project(id); + self.reload(); + cx.notify(); + } +} + +impl Render for ProjectsView { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let mut list = div().flex().flex_col().gap_2().w_full(); + + if let Some(error) = &self.load_error { + list = list.child( + div() + .text_color(rgb(0xef4444)) + .child(format!("Failed to load projects: {error}")), + ); + } + + if self.projects.is_empty() && self.load_error.is_none() { + list = list.child( + div() + .text_color(rgb(theme::TEXT_SECONDARY)) + .child("No projects yet. Create one to get started."), + ); + } + + for project in &self.projects { + let id = project.id.clone(); + let name = project.name.clone(); + let delete_id = project.id.clone(); + let updated = project.updated_at.format("%Y-%m-%d %H:%M").to_string(); + let duration = project.duration.to_seconds_f64(); + + list = list.child( + div() + .id(SharedString::from(format!("project-{id}"))) + .w_full() + .px_4() + .py_3() + .bg(rgb(theme::SURFACE)) + .rounded_md() + .border_1() + .border_color(rgb(theme::BORDER)) + .flex() + .flex_row() + .justify_between() + .items_center() + .cursor_pointer() + .hover(|style| style.bg(rgb(theme::SURFACE_HOVER))) + .on_click(cx.listener(move |this, _, _, cx| { + this.open_project(id.clone(), name.clone(), cx); + })) + .child( + div() + .flex() + .flex_col() + .child(div().child(project.name.clone())) + .child( + div() + .text_sm() + .text_color(rgb(theme::TEXT_SECONDARY)) + .child(format!("{duration:.1}s ยท updated {updated}")), + ), + ) + .child( + div() + .id(SharedString::from(format!("delete-{delete_id}"))) + .px_2() + .py_1() + .text_sm() + .text_color(rgb(theme::TEXT_SECONDARY)) + .rounded_sm() + .cursor_pointer() + .hover(|style| style.text_color(rgb(0xef4444))) + .on_click(cx.listener(move |this, _, _, cx| { + this.delete_project(&delete_id, cx); + cx.stop_propagation(); + })) + .child("Delete"), + ), + ); + } + + div() + .size_full() + .flex() + .flex_col() + .p_8() + .gap_6() + .child( + div() + .flex() + .flex_row() + .justify_between() + .items_center() + .w_full() + .child(div().text_xl().child("Projects")) + .child( + div() + .id("new-project") + .px_4() + .py_2() + .bg(rgb(theme::ACCENT)) + .rounded_md() + .cursor_pointer() + .hover(|style| style.bg(rgb(0x2563eb))) + .on_click(cx.listener(|this, _, _, cx| this.create_project(cx))) + .child("New project"), + ), + ) + .child(div().flex_1().overflow_y_hidden().child(list)) + } +} -- cgit v1.3.1