This commit is contained in:
2026-08-26 20:51:00 +07:00
parent a00ee68258
commit df8b8a9598
5 changed files with 180 additions and 61 deletions
+6
View File
@@ -35,6 +35,12 @@ pub fn home_dir() -> PathBuf {
dirs::home_dir().expect("failed to determine home directory")
}
/// Returns the current user's Desktop folder, falling back to the home
/// directory (or an empty path) when it can't be determined.
pub fn desktop_dir() -> PathBuf {
dirs::desktop_dir().unwrap_or_else(|| dirs::home_dir().unwrap_or_default())
}
/// Sets a custom directory for all user data, overriding the default data
/// directory. Must be called before any other path operation. The directory
/// is created if it doesn't exist and canonicalized to an absolute path.
+1
View File
@@ -7,6 +7,7 @@ publish.workspace = true
[dependencies]
assets = { path = "../assets" }
dock = { workspace = true }
paths = { path = "../paths" }
signed_core = { path = "../signed_core" }
signed_git = { path = "../signed_git" }
signed_state = { path = "../signed_state" }
@@ -1,12 +1,11 @@
use dock::{DockArea, DockPlacement, panel_handle};
use gpui::prelude::*;
use gpui::{App, Entity, SharedString, WeakEntity, Window, div, px};
use gpui::{App, Entity, PathPromptOptions, SharedString, WeakEntity, Window, div, px};
use gpui_base::input::TextareaState;
use gpui_component::button::{Button, ButtonVariants};
use gpui_component::button::{Button, ButtonVariants, Toggle, ToggleVariants};
use gpui_component::dialog::{DialogDescription, DialogFooter, DialogHeader, DialogTitle};
use gpui_component::form::{field, v_form};
use gpui_component::input::{Input, InputState, Textarea};
use gpui_component::tag::Tag;
use gpui_component::{ActiveTheme, Disableable, IconName, Sizable, WindowExt, h_flex, v_flex};
use nostr::prelude::*;
use signed_core::{Announcement, filters};
@@ -29,6 +28,8 @@ pub struct CreateRepoState {
pub loading_servers: bool,
pub error: Option<SharedString>,
pub grasp_servers: Vec<RelayUrl>,
/// Whether the grasp server section is shown; defaults to shown.
pub servers_enabled: bool,
}
impl CreateRepoState {
@@ -36,6 +37,7 @@ impl CreateRepoState {
fn new_default() -> Self {
Self {
loading_servers: true,
servers_enabled: false,
grasp_servers: DEFAULT_GRASP_SERVERS
.iter()
.filter_map(|url| RelayUrl::parse(url).ok())
@@ -57,6 +59,10 @@ pub fn open(dock_area: WeakEntity<DockArea>, window: &mut Window, cx: &mut App)
.auto_grow(3, 5)
.placeholder("Short description")
});
let folder_input = cx.new(|cx| {
InputState::new(window, cx)
.default_value(paths::desktop_dir().to_string_lossy().to_string())
});
let relay_input = cx.new(|cx| {
InputState::new(window, cx).placeholder("wss://relay.example.com or relay.example.com")
});
@@ -65,8 +71,14 @@ pub fn open(dock_area: WeakEntity<DockArea>, window: &mut Window, cx: &mut App)
load_user_grasp_servers(state.clone(), window, cx);
window.open_dialog(cx, move |dialog, _window, _cx| {
const DESC: &str = "Publish a new repository to your grasp servers.";
const FOLDER_NOTE: &str = "Where the repository is stored, defaults to your Desktop";
const SERVER_NOTE: &str =
"Where the repository is hosted, the initial push goes to each server";
let name_input = name_input.clone();
let desc_input = desc_input.clone();
let folder_input = folder_input.clone();
let relay_input = relay_input.clone();
let state = state.clone();
let dock_area = dock_area.clone();
@@ -79,15 +91,13 @@ pub fn open(dock_area: WeakEntity<DockArea>, window: &mut Window, cx: &mut App)
let error = state.read(cx).error.clone();
let servers = state.read(cx).grasp_servers.clone();
let loading_servers = state.read(cx).loading_servers;
let servers_enabled = state.read(cx).servers_enabled;
content
.child(
DialogHeader::new()
.child(DialogTitle::new().child("Create repository"))
.child(
DialogDescription::new()
.child("Publish a new repository to your grasp servers."),
),
.child(DialogDescription::new().child(DESC)),
)
.child(
v_form()
@@ -101,34 +111,86 @@ pub fn open(dock_area: WeakEntity<DockArea>, window: &mut Window, cx: &mut App)
.child(
field()
.label("Description")
.description("Optional")
.child(Textarea::new(&desc_input)),
)
.child(
field().label("Folder").description(FOLDER_NOTE).child(
h_flex()
.gap_1()
.items_center()
.child(
div()
.flex_1()
.child(Input::new(&folder_input).disabled(true)),
)
.child(
Button::new("choose-folder")
.icon(IconName::FolderOpen)
.ghost()
.tooltip("Choose folder")
.on_click({
let folder_input = folder_input.clone();
move |_ev, window, cx| {
choose_folder(&folder_input, window, cx);
}
}),
),
),
)
.child(
field()
.label("Grasp servers")
.description(
"Where the repository is hosted; the initial push goes to each server",
)
.child(
v_flex()
.gap_1()
.children(
servers
.iter()
.enumerate()
.map(|(ix, relay)| {
render_server_row(ix, relay, state.clone())
}),
)
.child(
h_flex().gap_1().items_center().child(
.label_fn({
let state = state.clone();
move |_window, cx| {
let enabled = state.read(cx).servers_enabled;
h_flex()
.w_full()
.justify_between()
.items_center()
.gap_1()
.child(
Toggle::new("grasp-servers-toggle")
.xsmall()
.ghost()
.icon({
if enabled {
IconName::ChevronDown
} else {
IconName::ChevronUp
}
})
.checked(enabled)
.on_click({
let state = state.clone();
move |checked, _window, cx| {
state.update(cx, |state, cx| {
state.servers_enabled =
*checked;
cx.notify();
});
}
}),
)
.child(div().child("Grasp servers"))
}
})
.when(servers_enabled, |this| this.description(SERVER_NOTE))
.child(v_flex().gap_1().when(servers_enabled, |this| {
this.children(servers.iter().enumerate().map(
|(ix, relay)| {
render_server_row(ix, relay, state.clone(), cx)
},
))
.child(
h_flex()
.gap_1()
.items_center()
.child(
div().flex_1().child(Input::new(&relay_input)),
)
.child(
Button::new("add-relay")
.icon(IconName::Plus)
.small()
.ghost()
.tooltip("Add grasp server")
.on_click({
@@ -144,16 +206,19 @@ pub fn open(dock_area: WeakEntity<DockArea>, window: &mut Window, cx: &mut App)
}
}),
),
)
.when(loading_servers, |this| {
)
.when(
loading_servers,
|this| {
this.child(
div()
.text_xs()
.text_color(cx.theme().muted_foreground)
.child("Loading your grasp servers…"),
)
}),
),
},
)
})),
),
)
.children(error.map(|message| {
@@ -164,6 +229,7 @@ pub fn open(dock_area: WeakEntity<DockArea>, window: &mut Window, cx: &mut App)
Button::new("create")
.primary()
.label("Create repository")
.icon(IconName::ArrowRight)
.tooltip("Create repository")
.loading(busy)
.disabled(busy)
@@ -195,16 +261,26 @@ fn render_server_row(
ix: usize,
relay: &RelayUrl,
state: Entity<CreateRepoState>,
cx: &App,
) -> impl IntoElement {
h_flex()
.w_full()
.gap_1()
.items_center()
.child(Tag::new().flex_1().child(display_server(relay)))
.child(
h_flex()
.h_8()
.w_full()
.px_2()
.bg(cx.theme().muted)
.text_color(cx.theme().muted_foreground)
.text_sm()
.rounded(cx.theme().radius)
.child(display_server(relay)),
)
.child(
Button::new(format!("remove-relay:{ix}"))
.icon(IconName::Close)
.small()
.ghost()
.flex_shrink_0()
.tooltip("Remove")
@@ -227,6 +303,34 @@ fn display_server(relay: &RelayUrl) -> SharedString {
.unwrap_or_else(|| SharedString::from(relay.to_string()))
}
/// Prompt the user to pick the folder the repository will be stored in, using
/// the platform's native folder picker, and show the result in the disabled
/// folder input.
fn choose_folder(folder_input: &Entity<InputState>, window: &mut Window, cx: &mut App) {
let handle = window.window_handle();
let folder_input = folder_input.clone();
let prompt = cx.prompt_for_paths(PathPromptOptions {
files: false,
directories: true,
multiple: false,
prompt: Some("Choose folder".into()),
});
cx.spawn(async move |cx| {
if let Ok(Ok(Some(mut paths))) = prompt.await
&& let Some(path) = paths.pop()
{
let path = path.to_string_lossy().to_string();
cx.update_window(handle, |_, window, cx| {
folder_input.update(cx, |input, cx| input.set_value(path, window, cx));
})
.ok();
}
})
.detach();
}
/// Parse the relay input (accepting a bare host) and append it to the list.
fn add_relay(
state: &Entity<CreateRepoState>,