summaryrefslogtreecommitdiff
path: root/apps/desktop/src
diff options
context:
space:
mode:
Diffstat (limited to 'apps/desktop/src')
-rw-r--r--apps/desktop/src/app.rs62
-rw-r--r--apps/desktop/src/main.rs37
-rw-r--r--apps/desktop/src/projects_view.rs164
-rw-r--r--apps/desktop/src/theme.rs7
4 files changed, 246 insertions, 24 deletions
diff --git a/apps/desktop/src/app.rs b/apps/desktop/src/app.rs
new file mode 100644
index 00000000..8eab4f87
--- /dev/null
+++ b/apps/desktop/src/app.rs
@@ -0,0 +1,62 @@
+use gpui::{Context, Window, div, prelude::*, rgb};
+
+use crate::projects_view::ProjectsView;
+use crate::theme;
+
+pub enum Screen {
+ Projects,
+ Editor { project_id: String, project_name: String },
+}
+
+pub struct OpenCutApp {
+ pub screen: Screen,
+ pub projects_view: gpui::Entity<ProjectsView>,
+}
+
+impl OpenCutApp {
+ pub fn new(cx: &mut Context<Self>) -> Self {
+ let app = cx.weak_entity();
+ let projects_view = cx.new(|_| ProjectsView::new(app));
+ Self {
+ screen: Screen::Projects,
+ projects_view,
+ }
+ }
+
+ pub fn open_project(&mut self, project_id: String, project_name: String, cx: &mut Context<Self>) {
+ self.screen = Screen::Editor {
+ project_id,
+ project_name,
+ };
+ cx.notify();
+ }
+}
+
+impl Render for OpenCutApp {
+ fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
+ let content: gpui::AnyElement = match &self.screen {
+ Screen::Projects => self.projects_view.clone().into_any_element(),
+ Screen::Editor { project_name, project_id } => div()
+ .size_full()
+ .flex()
+ .flex_col()
+ .items_center()
+ .justify_center()
+ .gap_2()
+ .child(format!("Editing: {project_name}"))
+ .child(
+ div()
+ .text_sm()
+ .text_color(rgb(theme::TEXT_SECONDARY))
+ .child(format!("Editor UI lands here — preview, timeline, panels ({project_id})")),
+ )
+ .into_any_element(),
+ };
+
+ div()
+ .size_full()
+ .bg(rgb(theme::BACKGROUND))
+ .text_color(rgb(theme::TEXT_PRIMARY))
+ .child(content)
+ }
+}
diff --git a/apps/desktop/src/main.rs b/apps/desktop/src/main.rs
index 0ba70959..0508aa33 100644
--- a/apps/desktop/src/main.rs
+++ b/apps/desktop/src/main.rs
@@ -1,24 +1,17 @@
-use gpui::{
- div, prelude::*, px, rgb, size, App, Application, Bounds, Context, SharedString, Window,
- WindowBounds, WindowOptions,
-};
+use std::path::PathBuf;
-struct AppWindow {
- title: SharedString,
-}
+use gpui::{App, AppContext, Application, Bounds, WindowBounds, WindowOptions, px, size};
+
+mod app;
+mod projects_view;
+mod theme;
-impl Render for AppWindow {
- fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
- div()
- .size_full()
- .bg(rgb(0x0f0f0f))
- .flex()
- .justify_center()
- .items_center()
- .text_xl()
- .text_color(rgb(0xffffff))
- .child(self.title.clone())
- }
+use app::OpenCutApp;
+
+pub fn projects_root() -> PathBuf {
+ std::env::home_dir()
+ .unwrap_or_else(|| PathBuf::from("."))
+ .join(".local/share/opencut/projects")
}
fn main() {
@@ -29,11 +22,7 @@ fn main() {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
- |_, cx| {
- cx.new(|_| AppWindow {
- title: "OpenCut".into(),
- })
- },
+ |_, cx| cx.new(|cx| OpenCutApp::new(cx)),
)
.unwrap();
cx.activate(true);
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))
+ }
+}
diff --git a/apps/desktop/src/theme.rs b/apps/desktop/src/theme.rs
new file mode 100644
index 00000000..9bbe6579
--- /dev/null
+++ b/apps/desktop/src/theme.rs
@@ -0,0 +1,7 @@
+pub const BACKGROUND: u32 = 0x0f0f0f;
+pub const SURFACE: u32 = 0x1a1a1a;
+pub const SURFACE_HOVER: u32 = 0x262626;
+pub const BORDER: u32 = 0x2e2e2e;
+pub const TEXT_PRIMARY: u32 = 0xf5f5f5;
+pub const TEXT_SECONDARY: u32 = 0x9c9c9c;
+pub const ACCENT: u32 = 0x3b82f6;