summaryrefslogtreecommitdiff
path: root/apps/desktop/src/projects_view.rs
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-07-28 20:14:51 +0200
committercashmere <cashmere@autistici.org>2026-07-28 20:14:51 +0200
commit32438ce25f69de99d2523b98237f62cfa90890d7 (patch)
tree3a712c347dec0889bdea3328b4c817c2d9392b27 /apps/desktop/src/projects_view.rs
parentf5a82152394fde4d8f740f664ab7737b82861618 (diff)
downloadopencut-32438ce25f69de99d2523b98237f62cfa90890d7.tar.gz
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)
Diffstat (limited to 'apps/desktop/src/projects_view.rs')
-rw-r--r--apps/desktop/src/projects_view.rs164
1 files changed, 164 insertions, 0 deletions
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<ProjectMetadata>,
+ load_error: Option<String>,
+ app: WeakEntity<OpenCutApp>,
+}
+
+impl ProjectsView {
+ pub fn new(app: WeakEntity<OpenCutApp>) -> 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<Self>) {
+ 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<Self>) {
+ let _ = self.app.update(cx, |app, cx| app.open_project(id, name, cx));
+ }
+
+ fn delete_project(&mut self, id: &str, cx: &mut Context<Self>) {
+ let _ = self.store.delete_project(id);
+ self.reload();
+ cx.notify();
+ }
+}
+
+impl Render for ProjectsView {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> 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))
+ }
+}