restructure signed_app into flat modules and bump gpui-native-kit
Rust / build (macos-latest, stable) (push) Waiting to run
Rust / build (ubuntu-latest, stable) (push) Waiting to run
Rust / build (windows-latest, stable) (push) Waiting to run
Rust / build (macos-latest, stable) (pull_request) Waiting to run
Rust / build (ubuntu-latest, stable) (pull_request) Waiting to run
Rust / build (windows-latest, stable) (pull_request) Waiting to run
Rust / build (macos-latest, stable) (push) Waiting to run
Rust / build (ubuntu-latest, stable) (push) Waiting to run
Rust / build (windows-latest, stable) (push) Waiting to run
Rust / build (macos-latest, stable) (pull_request) Waiting to run
Rust / build (ubuntu-latest, stable) (pull_request) Waiting to run
Rust / build (windows-latest, stable) (pull_request) Waiting to run
This commit is contained in:
Generated
+8
-8
@@ -3059,7 +3059,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui-native-core"
|
name = "gpui-native-core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=97106b7a3a54ec929c47feb70cdab4bed044d20e#97106b7a3a54ec929c47feb70cdab4bed044d20e"
|
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=72118bdd6c6c6a90f17f92f767c6ad342c1516b8#72118bdd6c6c6a90f17f92f767c6ad342c1516b8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cpal",
|
"cpal",
|
||||||
"flume 0.12.0",
|
"flume 0.12.0",
|
||||||
@@ -3074,7 +3074,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui-native-data"
|
name = "gpui-native-data"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=97106b7a3a54ec929c47feb70cdab4bed044d20e#97106b7a3a54ec929c47feb70cdab4bed044d20e"
|
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=72118bdd6c6c6a90f17f92f767c6ad342c1516b8#72118bdd6c6c6a90f17f92f767c6ad342c1516b8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gpui",
|
"gpui",
|
||||||
"gpui-native-core",
|
"gpui-native-core",
|
||||||
@@ -3086,7 +3086,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui-native-editor"
|
name = "gpui-native-editor"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=97106b7a3a54ec929c47feb70cdab4bed044d20e#97106b7a3a54ec929c47feb70cdab4bed044d20e"
|
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=72118bdd6c6c6a90f17f92f767c6ad342c1516b8#72118bdd6c6c6a90f17f92f767c6ad342c1516b8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gpui",
|
"gpui",
|
||||||
"gpui-native-core",
|
"gpui-native-core",
|
||||||
@@ -3105,7 +3105,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui-native-icons"
|
name = "gpui-native-icons"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=97106b7a3a54ec929c47feb70cdab4bed044d20e#97106b7a3a54ec929c47feb70cdab4bed044d20e"
|
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=72118bdd6c6c6a90f17f92f767c6ad342c1516b8#72118bdd6c6c6a90f17f92f767c6ad342c1516b8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"gpui",
|
"gpui",
|
||||||
@@ -3114,7 +3114,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui-native-kit"
|
name = "gpui-native-kit"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=97106b7a3a54ec929c47feb70cdab4bed044d20e#97106b7a3a54ec929c47feb70cdab4bed044d20e"
|
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=72118bdd6c6c6a90f17f92f767c6ad342c1516b8#72118bdd6c6c6a90f17f92f767c6ad342c1516b8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gpui",
|
"gpui",
|
||||||
"gpui-native-core",
|
"gpui-native-core",
|
||||||
@@ -3128,7 +3128,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui-native-layout"
|
name = "gpui-native-layout"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=97106b7a3a54ec929c47feb70cdab4bed044d20e#97106b7a3a54ec929c47feb70cdab4bed044d20e"
|
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=72118bdd6c6c6a90f17f92f767c6ad342c1516b8#72118bdd6c6c6a90f17f92f767c6ad342c1516b8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gpui",
|
"gpui",
|
||||||
"gpui-native-core",
|
"gpui-native-core",
|
||||||
@@ -3141,7 +3141,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui-native-theme"
|
name = "gpui-native-theme"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=97106b7a3a54ec929c47feb70cdab4bed044d20e#97106b7a3a54ec929c47feb70cdab4bed044d20e"
|
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=72118bdd6c6c6a90f17f92f767c6ad342c1516b8#72118bdd6c6c6a90f17f92f767c6ad342c1516b8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-channel",
|
"async-channel",
|
||||||
@@ -3162,7 +3162,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui-native-utils"
|
name = "gpui-native-utils"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=97106b7a3a54ec929c47feb70cdab4bed044d20e#97106b7a3a54ec929c47feb70cdab4bed044d20e"
|
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=72118bdd6c6c6a90f17f92f767c6ad342c1516b8#72118bdd6c6c6a90f17f92f767c6ad342c1516b8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gpui",
|
"gpui",
|
||||||
"gpui-native-theme",
|
"gpui-native-theme",
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@ gpui = { git = "https://github.com/zed-industries/zed", rev = "f8cfc7ed9e57503de
|
|||||||
gpui_platform = { git = "https://github.com/zed-industries/zed", rev = "f8cfc7ed9e57503def9479138894f3139f5400cc", features = ["font-kit"] }
|
gpui_platform = { git = "https://github.com/zed-industries/zed", rev = "f8cfc7ed9e57503def9479138894f3139f5400cc", features = ["font-kit"] }
|
||||||
reqwest_client = { git = "https://github.com/zed-industries/zed", rev = "f8cfc7ed9e57503def9479138894f3139f5400cc" }
|
reqwest_client = { git = "https://github.com/zed-industries/zed", rev = "f8cfc7ed9e57503def9479138894f3139f5400cc" }
|
||||||
|
|
||||||
gpui-native-kit = { git = "https://git.reya.info/reya/gpui-native-kit", rev = "97106b7a3a54ec929c47feb70cdab4bed044d20e", features = ["layout", "editor"] }
|
gpui-native-kit = { git = "https://git.reya.info/reya/gpui-native-kit", rev = "72118bdd6c6c6a90f17f92f767c6ad342c1516b8", features = ["layout", "editor"] }
|
||||||
|
|
||||||
settings = { path = "crates/settings" }
|
settings = { path = "crates/settings" }
|
||||||
signed_app = { path = "crates/signed_app" }
|
signed_app = { path = "crates/signed_app" }
|
||||||
|
|||||||
@@ -96,3 +96,4 @@ impl IconNamed for CustomIconName {
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
use gpui::App;
|
||||||
|
use gpui_native_kit::theme::{ThemeVariant, follow_system, set_mode_override};
|
||||||
|
use settings::{AppearanceMode, Settings};
|
||||||
|
|
||||||
|
/// Applies the persisted light/dark/system preference to the kit theme.
|
||||||
|
pub fn apply_appearance(settings: &Settings, cx: &mut App) {
|
||||||
|
match settings.appearance {
|
||||||
|
AppearanceMode::System => follow_system(cx),
|
||||||
|
AppearanceMode::Light => set_mode_override(Some(ThemeVariant::Light), cx),
|
||||||
|
AppearanceMode::Dark => set_mode_override(Some(ThemeVariant::Dark), cx),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,23 @@
|
|||||||
|
pub mod helpers;
|
||||||
|
mod placeholder;
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, AppContext as _, Context, Entity, IntoElement, ParentElement as _, Render, Styled as _,
|
AppContext as _, Context, Entity, IntoElement, ParentElement as _, Render, Styled as _,
|
||||||
Subscription, Window, div,
|
Subscription, Window, div,
|
||||||
};
|
};
|
||||||
use gpui_native_kit::icons::IconRole;
|
use gpui_native_kit::icons::IconRole;
|
||||||
use gpui_native_kit::layout::dock::{Dock, DockState};
|
use gpui_native_kit::layout::dock::{Dock, DockState};
|
||||||
use gpui_native_kit::layout::shell::{LayoutState, Shell, SidebarToggles};
|
use gpui_native_kit::layout::shell::{LayoutState, Shell, SidebarToggles};
|
||||||
use gpui_native_kit::theme::{ActiveTheme as _, ThemeVariant, follow_system, set_mode_override};
|
use gpui_native_kit::theme::ActiveTheme as _;
|
||||||
use gpui_native_kit::toast::Toast;
|
use gpui_native_kit::toast::Toast;
|
||||||
use settings::{AppearanceMode, Settings, SettingsStore};
|
use helpers::apply_appearance;
|
||||||
|
use settings::SettingsStore;
|
||||||
use signed_state::{Backend, BackendEvent};
|
use signed_state::{Backend, BackendEvent};
|
||||||
|
|
||||||
use crate::dialogs::{self, DialogHost};
|
use crate::app::placeholder::PlaceholderView;
|
||||||
use crate::views::placeholder::PlaceholderView;
|
use crate::identity;
|
||||||
use crate::views::sidebar::SidebarView;
|
use crate::shared::dialogs::DialogHost;
|
||||||
|
use crate::sidebar::SidebarView;
|
||||||
|
|
||||||
pub struct AppRoot {
|
pub struct AppRoot {
|
||||||
layout: Entity<LayoutState>,
|
layout: Entity<LayoutState>,
|
||||||
@@ -53,7 +58,7 @@ impl AppRoot {
|
|||||||
|
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
cx.subscribe(&backend, |this, _backend, event, cx| match event {
|
cx.subscribe(&backend, |this, _backend, event, cx| match event {
|
||||||
BackendEvent::PassphraseRequired => dialogs::passphrase::open(cx),
|
BackendEvent::PassphraseRequired => identity::helpers::open_passphrase(cx),
|
||||||
BackendEvent::Error(message) => {
|
BackendEvent::Error(message) => {
|
||||||
this.layout.update(cx, |layout, cx| {
|
this.layout.update(cx, |layout, cx| {
|
||||||
layout.push_toast(Toast::new(message.clone()).icon(IconRole::Warning), cx);
|
layout.push_toast(Toast::new(message.clone()).icon(IconRole::Warning), cx);
|
||||||
@@ -70,7 +75,7 @@ impl AppRoot {
|
|||||||
|
|
||||||
window.defer(cx, move |_window, cx| {
|
window.defer(cx, move |_window, cx| {
|
||||||
if backend.read(cx).passphrase_required() {
|
if backend.read(cx).passphrase_required() {
|
||||||
dialogs::passphrase::open(cx);
|
identity::helpers::open_passphrase(cx);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -104,11 +109,3 @@ impl Render for AppRoot {
|
|||||||
.child(self.dialogs.clone())
|
.child(self.dialogs.clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn apply_appearance(settings: &Settings, cx: &mut App) {
|
|
||||||
match settings.appearance {
|
|
||||||
AppearanceMode::System => follow_system(cx),
|
|
||||||
AppearanceMode::Light => set_mode_override(Some(ThemeVariant::Light), cx),
|
|
||||||
AppearanceMode::Dark => set_mode_override(Some(ThemeVariant::Dark), cx),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
use assets::CustomIconName;
|
|
||||||
use gpui_native_kit::badge::{Badge, BadgeVariant};
|
|
||||||
use gpui_native_kit::icons::Icon;
|
|
||||||
use gpui_native_kit::tooltip::Tooltip;
|
|
||||||
use signed_core::RepoStatus;
|
|
||||||
|
|
||||||
/// The status pill shown on issue and pull-request rows.
|
|
||||||
pub fn status_badge(status: RepoStatus) -> Badge {
|
|
||||||
let (label, variant, icon) = match status {
|
|
||||||
RepoStatus::Open => ("Open", BadgeVariant::Neutral, CustomIconName::GitIssueOpen),
|
|
||||||
RepoStatus::Closed => (
|
|
||||||
"Closed",
|
|
||||||
BadgeVariant::Danger,
|
|
||||||
CustomIconName::GitIssueClosed,
|
|
||||||
),
|
|
||||||
RepoStatus::Draft => (
|
|
||||||
"Draft",
|
|
||||||
BadgeVariant::Warning,
|
|
||||||
CustomIconName::GitIssueOngoing,
|
|
||||||
),
|
|
||||||
RepoStatus::Applied => (
|
|
||||||
"Applied",
|
|
||||||
BadgeVariant::Success,
|
|
||||||
CustomIconName::GitIssueDone,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
Badge::new(format!("status-{label}"), label)
|
|
||||||
.variant(variant)
|
|
||||||
.icon(Icon::named(icon))
|
|
||||||
.tooltip(Tooltip::text(label))
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
use assets::CustomIconName;
|
||||||
|
use gpui::prelude::FluentBuilder as _;
|
||||||
|
use gpui::{
|
||||||
|
AnyElement, Entity, InteractiveElement as _, IntoElement, ParentElement as _, SharedString,
|
||||||
|
StatefulInteractiveElement as _, Styled as _, WeakEntity, div, px,
|
||||||
|
};
|
||||||
|
use gpui_native_kit::avatar::Avatar;
|
||||||
|
use gpui_native_kit::common::{Sizable as _, h_flex, v_flex};
|
||||||
|
use gpui_native_kit::icons::Icon;
|
||||||
|
use gpui_native_kit::layout::dock::DockState;
|
||||||
|
use gpui_native_kit::theme::ActiveTheme as _;
|
||||||
|
use metadata::ProfileDirectory;
|
||||||
|
use signed_core::Announcement;
|
||||||
|
use signed_state::{RepoListStore, Timestamp};
|
||||||
|
use utils::relative_time;
|
||||||
|
|
||||||
|
use crate::repo::actions::open_repo_panel;
|
||||||
|
|
||||||
|
const RECENT_COUNT: usize = 10;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub(super) enum RepoFilter {
|
||||||
|
All,
|
||||||
|
#[default]
|
||||||
|
Popular,
|
||||||
|
Recent,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RepoFilter {
|
||||||
|
/// Indices into the store's `announcements` this filter includes, in display order.
|
||||||
|
pub(super) fn visible(self, store: &RepoListStore, query: &str) -> Vec<usize> {
|
||||||
|
let announcements = &store.announcements;
|
||||||
|
let mut indices: Vec<usize> = (0..announcements.len()).collect();
|
||||||
|
let query = query.trim().to_lowercase();
|
||||||
|
|
||||||
|
if !query.is_empty() {
|
||||||
|
indices.retain(|&ix| {
|
||||||
|
let announcement = &announcements[ix];
|
||||||
|
let name = announcement.name.as_deref().unwrap_or(&announcement.id);
|
||||||
|
name.to_lowercase().contains(&query)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
match self {
|
||||||
|
Self::All => {}
|
||||||
|
Self::Recent => indices.truncate(RECENT_COUNT),
|
||||||
|
Self::Popular => {
|
||||||
|
let counts = &store.counts;
|
||||||
|
let scores: Vec<u32> = announcements
|
||||||
|
.iter()
|
||||||
|
.map(|a| counts.get(&a.addr()).map_or(0, |c| c.score()))
|
||||||
|
.collect();
|
||||||
|
indices.sort_by(|&a, &b| scores[b].cmp(&scores[a]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
indices
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn render_card(
|
||||||
|
ix: usize,
|
||||||
|
announcement: &Announcement,
|
||||||
|
last_activity: Option<Timestamp>,
|
||||||
|
announcements: &[Announcement],
|
||||||
|
directory: &Entity<ProfileDirectory>,
|
||||||
|
dock: &WeakEntity<DockState>,
|
||||||
|
cx: &mut gpui::App,
|
||||||
|
) -> AnyElement {
|
||||||
|
let owner = directory.read(cx).profile(&announcement.owner, cx);
|
||||||
|
let owner_name = owner.name();
|
||||||
|
let dock = dock.clone();
|
||||||
|
let announcement = announcement.clone();
|
||||||
|
|
||||||
|
let name = announcement
|
||||||
|
.name
|
||||||
|
.as_deref()
|
||||||
|
.map(SharedString::from)
|
||||||
|
.unwrap_or_else(|| SharedString::from(announcement.id.clone()));
|
||||||
|
|
||||||
|
let description = announcement
|
||||||
|
.description
|
||||||
|
.as_deref()
|
||||||
|
.map(SharedString::from)
|
||||||
|
.unwrap_or_else(|| SharedString::from("No description"));
|
||||||
|
|
||||||
|
let activity = last_activity
|
||||||
|
.map(relative_time)
|
||||||
|
.map(|label| SharedString::from(format!("Updated {label}")))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let fork_label: Option<SharedString> = announcement.upstream.as_ref().and_then(|upstream| {
|
||||||
|
let addr = upstream.addr.as_ref()?;
|
||||||
|
let name = announcements
|
||||||
|
.iter()
|
||||||
|
.find(|a| a.addr() == *addr)
|
||||||
|
.map(|a| {
|
||||||
|
a.name
|
||||||
|
.as_deref()
|
||||||
|
.map(SharedString::from)
|
||||||
|
.unwrap_or_else(|| SharedString::from(a.id.clone()))
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| SharedString::from(addr.identifier().to_owned()));
|
||||||
|
Some(SharedString::from(format!("forked from {name}")))
|
||||||
|
});
|
||||||
|
|
||||||
|
v_flex()
|
||||||
|
.id(ix)
|
||||||
|
.flex_1()
|
||||||
|
.h_full()
|
||||||
|
.min_w_0()
|
||||||
|
.px_3()
|
||||||
|
.rounded(cx.theme().radius.control)
|
||||||
|
.border_1()
|
||||||
|
.border_color(cx.theme().colors.border)
|
||||||
|
.hover(|this| this.bg(cx.theme().colors.muted_hover))
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.h_10()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.min_w_0()
|
||||||
|
.text_sm()
|
||||||
|
.font_weight(gpui::FontWeight::SEMIBOLD)
|
||||||
|
.whitespace_nowrap()
|
||||||
|
.text_ellipsis()
|
||||||
|
.child(name),
|
||||||
|
)
|
||||||
|
.when_some(fork_label, |this, label| {
|
||||||
|
this.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().colors.muted_foreground)
|
||||||
|
.whitespace_nowrap()
|
||||||
|
.child(
|
||||||
|
Icon::named(CustomIconName::GitBranch)
|
||||||
|
.size(px(12.))
|
||||||
|
.color(cx.theme().colors.muted_foreground),
|
||||||
|
)
|
||||||
|
.child(label),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.h_16()
|
||||||
|
.min_w_0()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().colors.muted_foreground)
|
||||||
|
.line_clamp(2)
|
||||||
|
.child(description),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.h_12()
|
||||||
|
.gap_2()
|
||||||
|
.overflow_hidden()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
Avatar::new(announcement.owner)
|
||||||
|
.name(owner_name.clone())
|
||||||
|
.picture(owner.picture())
|
||||||
|
.small(),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().colors.muted_foreground)
|
||||||
|
.whitespace_nowrap()
|
||||||
|
.child(owner_name),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.flex_1()
|
||||||
|
.justify_end()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().colors.muted_foreground)
|
||||||
|
.whitespace_nowrap()
|
||||||
|
.child(activity),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.on_click(move |_, window, cx| {
|
||||||
|
open_repo_panel(&dock, &announcement.addr(), Some(&announcement), window, cx);
|
||||||
|
})
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
|
mod helpers;
|
||||||
|
|
||||||
use assets::CustomIconName;
|
use assets::CustomIconName;
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, Context, Entity, InteractiveElement as _, IntoElement, ParentElement as _,
|
AnyElement, App, Context, Entity, InteractiveElement as _, IntoElement, ParentElement as _,
|
||||||
Render, SharedString, StatefulInteractiveElement as _, Styled as _, Subscription,
|
Render, SharedString, Styled as _, Subscription, UniformListScrollHandle, WeakEntity, Window,
|
||||||
UniformListScrollHandle, WeakEntity, Window, div, px, uniform_list,
|
div, px, uniform_list,
|
||||||
};
|
};
|
||||||
use gpui_native_kit::avatar::Avatar;
|
use gpui_native_kit::common::{h_flex, v_flex};
|
||||||
use gpui_native_kit::common::{Sizable as _, h_flex, v_flex};
|
|
||||||
use gpui_native_kit::empty_state::EmptyState;
|
use gpui_native_kit::empty_state::EmptyState;
|
||||||
use gpui_native_kit::icons::{Icon, IconRole};
|
use gpui_native_kit::icons::{Icon, IconRole};
|
||||||
use gpui_native_kit::input::{Input, InputState, InputStateEvent};
|
use gpui_native_kit::input::{Input, InputState, InputStateEvent};
|
||||||
@@ -14,57 +15,13 @@ use gpui_native_kit::layout::dock::{DockState, Panel};
|
|||||||
use gpui_native_kit::scrollbar::Scrollbar;
|
use gpui_native_kit::scrollbar::Scrollbar;
|
||||||
use gpui_native_kit::segmented::{Segmented, SegmentedOption};
|
use gpui_native_kit::segmented::{Segmented, SegmentedOption};
|
||||||
use gpui_native_kit::theme::ActiveTheme as _;
|
use gpui_native_kit::theme::ActiveTheme as _;
|
||||||
|
use helpers::{RepoFilter, render_card};
|
||||||
use metadata::ProfileDirectory;
|
use metadata::ProfileDirectory;
|
||||||
use nostr::prelude::PublicKey;
|
use nostr::prelude::PublicKey;
|
||||||
use signed_core::Announcement;
|
use signed_state::RepoListStore;
|
||||||
use signed_state::{RepoListStore, Timestamp};
|
|
||||||
use utils::relative_time;
|
|
||||||
|
|
||||||
use crate::views::repo::actions::open_repo_panel;
|
|
||||||
|
|
||||||
const COLUMNS: usize = 2;
|
const COLUMNS: usize = 2;
|
||||||
const CARD_HEIGHT: f32 = 40. + 64. + 48. + 2. + 6.;
|
const CARD_HEIGHT: f32 = 40. + 64. + 48. + 2. + 6.;
|
||||||
const RECENT_COUNT: usize = 10;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
||||||
enum RepoFilter {
|
|
||||||
All,
|
|
||||||
#[default]
|
|
||||||
Popular,
|
|
||||||
Recent,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RepoFilter {
|
|
||||||
/// Indices into the store's `announcements` this filter includes, in display order.
|
|
||||||
fn visible(self, store: &RepoListStore, query: &str) -> Vec<usize> {
|
|
||||||
let announcements = &store.announcements;
|
|
||||||
let mut indices: Vec<usize> = (0..announcements.len()).collect();
|
|
||||||
let query = query.trim().to_lowercase();
|
|
||||||
|
|
||||||
if !query.is_empty() {
|
|
||||||
indices.retain(|&ix| {
|
|
||||||
let announcement = &announcements[ix];
|
|
||||||
let name = announcement.name.as_deref().unwrap_or(&announcement.id);
|
|
||||||
name.to_lowercase().contains(&query)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
match self {
|
|
||||||
Self::All => {}
|
|
||||||
Self::Recent => indices.truncate(RECENT_COUNT),
|
|
||||||
Self::Popular => {
|
|
||||||
let counts = &store.counts;
|
|
||||||
let scores: Vec<u32> = announcements
|
|
||||||
.iter()
|
|
||||||
.map(|a| counts.get(&a.addr()).map_or(0, |c| c.score()))
|
|
||||||
.collect();
|
|
||||||
indices.sort_by(|&a, &b| scores[b].cmp(&scores[a]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
indices
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ExploreView {
|
pub struct ExploreView {
|
||||||
store: Entity<RepoListStore>,
|
store: Entity<RepoListStore>,
|
||||||
@@ -85,7 +42,9 @@ impl ExploreView {
|
|||||||
let store = RepoListStore::global(cx);
|
let store = RepoListStore::global(cx);
|
||||||
let directory = ProfileDirectory::new(cx);
|
let directory = ProfileDirectory::new(cx);
|
||||||
|
|
||||||
|
let entity = cx.entity();
|
||||||
let search = InputState::new(cx);
|
let search = InputState::new(cx);
|
||||||
|
|
||||||
let search_subscription = cx.subscribe(&search, |this, _search, event, cx| {
|
let search_subscription = cx.subscribe(&search, |this, _search, event, cx| {
|
||||||
if matches!(event, InputStateEvent::TextChanged) {
|
if matches!(event, InputStateEvent::TextChanged) {
|
||||||
this.rebuild_rows(cx);
|
this.rebuild_rows(cx);
|
||||||
@@ -96,7 +55,13 @@ impl ExploreView {
|
|||||||
this.rebuild_rows(cx);
|
this.rebuild_rows(cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut view = Self {
|
cx.defer(move |cx| {
|
||||||
|
entity.update(cx, |this, cx| {
|
||||||
|
this.rebuild_rows(cx);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
store,
|
store,
|
||||||
dock,
|
dock,
|
||||||
scroll_handle: UniformListScrollHandle::new(),
|
scroll_handle: UniformListScrollHandle::new(),
|
||||||
@@ -106,9 +71,7 @@ impl ExploreView {
|
|||||||
directory,
|
directory,
|
||||||
_search_subscription: search_subscription,
|
_search_subscription: search_subscription,
|
||||||
_store_subscription: store_subscription,
|
_store_subscription: store_subscription,
|
||||||
};
|
}
|
||||||
view.rebuild_rows(cx);
|
|
||||||
view
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rebuild_rows(&mut self, cx: &mut Context<Self>) {
|
fn rebuild_rows(&mut self, cx: &mut Context<Self>) {
|
||||||
@@ -290,133 +253,3 @@ impl Render for ExploreView {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_card(
|
|
||||||
ix: usize,
|
|
||||||
announcement: &Announcement,
|
|
||||||
last_activity: Option<Timestamp>,
|
|
||||||
announcements: &[Announcement],
|
|
||||||
directory: &Entity<ProfileDirectory>,
|
|
||||||
dock: &WeakEntity<DockState>,
|
|
||||||
cx: &mut gpui::App,
|
|
||||||
) -> AnyElement {
|
|
||||||
let owner = directory.read(cx).profile(&announcement.owner, cx);
|
|
||||||
let name = announcement
|
|
||||||
.name
|
|
||||||
.as_deref()
|
|
||||||
.map(SharedString::from)
|
|
||||||
.unwrap_or_else(|| SharedString::from(announcement.id.clone()));
|
|
||||||
let description = announcement
|
|
||||||
.description
|
|
||||||
.as_deref()
|
|
||||||
.map(SharedString::from)
|
|
||||||
.unwrap_or_else(|| SharedString::from("No description"));
|
|
||||||
let activity = last_activity
|
|
||||||
.map(relative_time)
|
|
||||||
.map(|label| SharedString::from(format!("Updated {label}")))
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let fork_label: Option<SharedString> = announcement.upstream.as_ref().and_then(|upstream| {
|
|
||||||
let addr = upstream.addr.as_ref()?;
|
|
||||||
let name = announcements
|
|
||||||
.iter()
|
|
||||||
.find(|a| a.addr() == *addr)
|
|
||||||
.map(|a| {
|
|
||||||
a.name
|
|
||||||
.as_deref()
|
|
||||||
.map(SharedString::from)
|
|
||||||
.unwrap_or_else(|| SharedString::from(a.id.clone()))
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|| SharedString::from(addr.identifier().to_owned()));
|
|
||||||
Some(SharedString::from(format!("forked from {name}")))
|
|
||||||
});
|
|
||||||
|
|
||||||
let owner_name = owner.name();
|
|
||||||
let dock = dock.clone();
|
|
||||||
let announcement = announcement.clone();
|
|
||||||
|
|
||||||
v_flex()
|
|
||||||
.id(ix)
|
|
||||||
.flex_1()
|
|
||||||
.h_full()
|
|
||||||
.min_w_0()
|
|
||||||
.px_3()
|
|
||||||
.rounded(cx.theme().radius.control)
|
|
||||||
.border_1()
|
|
||||||
.border_color(cx.theme().colors.border)
|
|
||||||
.hover(|this| this.bg(cx.theme().colors.muted_hover))
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.h_10()
|
|
||||||
.gap_2()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.min_w_0()
|
|
||||||
.text_sm()
|
|
||||||
.font_weight(gpui::FontWeight::SEMIBOLD)
|
|
||||||
.whitespace_nowrap()
|
|
||||||
.text_ellipsis()
|
|
||||||
.child(name),
|
|
||||||
)
|
|
||||||
.when_some(fork_label, |this, label| {
|
|
||||||
this.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_1()
|
|
||||||
.text_xs()
|
|
||||||
.text_color(cx.theme().colors.muted_foreground)
|
|
||||||
.whitespace_nowrap()
|
|
||||||
.child(
|
|
||||||
Icon::named(CustomIconName::GitBranch)
|
|
||||||
.size(px(12.))
|
|
||||||
.color(cx.theme().colors.muted_foreground),
|
|
||||||
)
|
|
||||||
.child(label),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.h_16()
|
|
||||||
.min_w_0()
|
|
||||||
.text_xs()
|
|
||||||
.text_color(cx.theme().colors.muted_foreground)
|
|
||||||
.line_clamp(2)
|
|
||||||
.child(description),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.h_12()
|
|
||||||
.gap_2()
|
|
||||||
.overflow_hidden()
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_2()
|
|
||||||
.child(
|
|
||||||
Avatar::new(announcement.owner)
|
|
||||||
.name(owner_name.clone())
|
|
||||||
.picture(owner.picture())
|
|
||||||
.small(),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_xs()
|
|
||||||
.text_color(cx.theme().colors.muted_foreground)
|
|
||||||
.whitespace_nowrap()
|
|
||||||
.child(owner_name),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.flex_1()
|
|
||||||
.justify_end()
|
|
||||||
.text_xs()
|
|
||||||
.text_color(cx.theme().colors.muted_foreground)
|
|
||||||
.whitespace_nowrap()
|
|
||||||
.child(activity),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.on_click(move |_, window, cx| {
|
|
||||||
open_repo_panel(&dock, &announcement.addr(), Some(&announcement), window, cx);
|
|
||||||
})
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, AppContext as _, Context, IntoElement, ParentElement as _, Render,
|
AnyElement, Context, IntoElement, ParentElement as _, Render, Styled as _, Window, div,
|
||||||
Styled as _, Window, div,
|
|
||||||
};
|
};
|
||||||
use gpui_native_kit::button::{Button, ButtonVariant};
|
use gpui_native_kit::button::{Button, ButtonVariant};
|
||||||
use gpui_native_kit::common::v_flex;
|
use gpui_native_kit::common::v_flex;
|
||||||
@@ -8,7 +7,7 @@ use gpui_native_kit::dialog::Dialog;
|
|||||||
use gpui_native_kit::theme::ActiveTheme as _;
|
use gpui_native_kit::theme::ActiveTheme as _;
|
||||||
use signed_state::Backend;
|
use signed_state::Backend;
|
||||||
|
|
||||||
use super::{close_global, open_global};
|
use crate::shared::dialogs::close_global;
|
||||||
|
|
||||||
pub struct ExitDialog;
|
pub struct ExitDialog;
|
||||||
|
|
||||||
@@ -55,8 +54,3 @@ impl Render for ExitDialog {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opens the sign-out confirmation.
|
|
||||||
pub fn open(cx: &mut App) {
|
|
||||||
open_global(cx.new(|_| ExitDialog), cx);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
use gpui::{App, AppContext as _};
|
||||||
|
|
||||||
|
use super::exit::ExitDialog;
|
||||||
|
use super::import::ImportDialog;
|
||||||
|
use super::onboarding::OnboardingDialog;
|
||||||
|
use super::passphrase::PassphraseDialog;
|
||||||
|
use crate::shared::dialogs::open_global;
|
||||||
|
|
||||||
|
pub fn open_passphrase(cx: &mut App) {
|
||||||
|
open_global(PassphraseDialog::new(cx), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_onboarding(cx: &mut App) {
|
||||||
|
open_global(OnboardingDialog::new(cx), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_import(cx: &mut App) {
|
||||||
|
open_global(ImportDialog::new(cx), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_exit(cx: &mut App) {
|
||||||
|
open_global(cx.new(|_| ExitDialog), cx);
|
||||||
|
}
|
||||||
@@ -11,8 +11,8 @@ use gpui_native_kit::input::{Input, InputState, InputStateEvent};
|
|||||||
use gpui_native_kit::theme::ActiveTheme as _;
|
use gpui_native_kit::theme::ActiveTheme as _;
|
||||||
use signed_state::Backend;
|
use signed_state::Backend;
|
||||||
|
|
||||||
use super::close_global;
|
use crate::shared::dialogs::close_global;
|
||||||
use super::progress::{DialogProgress, error_row};
|
use crate::shared::progress::{DialogProgress, error_row};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Default, PartialEq, Eq)]
|
#[derive(Clone, Copy, Default, PartialEq, Eq)]
|
||||||
enum Stage {
|
enum Stage {
|
||||||
@@ -65,12 +65,14 @@ pub struct ImportDialog {
|
|||||||
|
|
||||||
impl ImportDialog {
|
impl ImportDialog {
|
||||||
pub fn new(cx: &mut App) -> Entity<Self> {
|
pub fn new(cx: &mut App) -> Entity<Self> {
|
||||||
|
let state = cx.new(|_| ImportState::default());
|
||||||
let secret_input = InputState::new(cx);
|
let secret_input = InputState::new(cx);
|
||||||
|
|
||||||
let pass_input = InputState::new(cx);
|
let pass_input = InputState::new(cx);
|
||||||
pass_input.update(cx, |state, cx| state.set_password(true, cx));
|
pass_input.update(cx, |state, cx| state.set_password(true, cx));
|
||||||
|
|
||||||
let repass_input = InputState::new(cx);
|
let repass_input = InputState::new(cx);
|
||||||
repass_input.update(cx, |state, cx| state.set_password(true, cx));
|
repass_input.update(cx, |state, cx| state.set_password(true, cx));
|
||||||
let state = cx.new(|_| ImportState::default());
|
|
||||||
|
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
let mut subscriptions = Vec::new();
|
let mut subscriptions = Vec::new();
|
||||||
@@ -101,12 +103,15 @@ impl ImportDialog {
|
|||||||
|
|
||||||
impl Render for ImportDialog {
|
impl Render for ImportDialog {
|
||||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let state = self.state.clone();
|
||||||
|
|
||||||
let secret_input = self.secret_input.clone();
|
let secret_input = self.secret_input.clone();
|
||||||
let pass_input = self.pass_input.clone();
|
let pass_input = self.pass_input.clone();
|
||||||
let repass_input = self.repass_input.clone();
|
let repass_input = self.repass_input.clone();
|
||||||
let state = self.state.clone();
|
|
||||||
let action_state = state.clone();
|
let action_state = state.clone();
|
||||||
let action_secret = secret_input.clone();
|
let action_secret = secret_input.clone();
|
||||||
|
|
||||||
let action_pass = pass_input.clone();
|
let action_pass = pass_input.clone();
|
||||||
let action_repass = repass_input.clone();
|
let action_repass = repass_input.clone();
|
||||||
|
|
||||||
@@ -122,6 +127,7 @@ impl Render for ImportDialog {
|
|||||||
.content(move |_window, cx| {
|
.content(move |_window, cx| {
|
||||||
let state = state.read(cx);
|
let state = state.read(cx);
|
||||||
let stage = state.stage;
|
let stage = state.stage;
|
||||||
|
|
||||||
let description = match stage {
|
let description = match stage {
|
||||||
Stage::Credential => {
|
Stage::Credential => {
|
||||||
"Paste the secret key or bunker URI of an existing identity."
|
"Paste the secret key or bunker URI of an existing identity."
|
||||||
@@ -168,11 +174,13 @@ impl Render for ImportDialog {
|
|||||||
.actions(move |_, cx| {
|
.actions(move |_, cx| {
|
||||||
let state = action_state.read(cx);
|
let state = action_state.read(cx);
|
||||||
let busy = state.progress.busy;
|
let busy = state.progress.busy;
|
||||||
|
|
||||||
let label = match state.stage {
|
let label = match state.stage {
|
||||||
Stage::Credential => "Continue",
|
Stage::Credential => "Continue",
|
||||||
Stage::Encrypt => "Import identity",
|
Stage::Encrypt => "Import identity",
|
||||||
Stage::Decrypt => "Unlock",
|
Stage::Decrypt => "Unlock",
|
||||||
};
|
};
|
||||||
|
|
||||||
let secret = action_secret.clone();
|
let secret = action_secret.clone();
|
||||||
let pass = action_pass.clone();
|
let pass = action_pass.clone();
|
||||||
let repass = action_repass.clone();
|
let repass = action_repass.clone();
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod exit;
|
||||||
|
pub mod helpers;
|
||||||
|
pub mod import;
|
||||||
|
pub mod onboarding;
|
||||||
|
pub mod passphrase;
|
||||||
+5
-3
@@ -11,8 +11,8 @@ use gpui_native_kit::input::{Input, InputState};
|
|||||||
use gpui_native_kit::theme::ActiveTheme as _;
|
use gpui_native_kit::theme::ActiveTheme as _;
|
||||||
use signed_state::Backend;
|
use signed_state::Backend;
|
||||||
|
|
||||||
use super::close_global;
|
use crate::shared::dialogs::close_global;
|
||||||
use super::progress::{DialogProgress, error_row};
|
use crate::shared::progress::{DialogProgress, error_row};
|
||||||
|
|
||||||
pub struct OnboardingDialog {
|
pub struct OnboardingDialog {
|
||||||
name_input: Entity<InputState>,
|
name_input: Entity<InputState>,
|
||||||
@@ -23,12 +23,14 @@ pub struct OnboardingDialog {
|
|||||||
|
|
||||||
impl OnboardingDialog {
|
impl OnboardingDialog {
|
||||||
pub fn new(cx: &mut App) -> Entity<Self> {
|
pub fn new(cx: &mut App) -> Entity<Self> {
|
||||||
|
let state = cx.new(|_| DialogProgress::default());
|
||||||
let name_input = InputState::new(cx);
|
let name_input = InputState::new(cx);
|
||||||
|
|
||||||
let pass_input = InputState::new(cx);
|
let pass_input = InputState::new(cx);
|
||||||
pass_input.update(cx, |state, cx| state.set_password(true, cx));
|
pass_input.update(cx, |state, cx| state.set_password(true, cx));
|
||||||
|
|
||||||
let repass_input = InputState::new(cx);
|
let repass_input = InputState::new(cx);
|
||||||
repass_input.update(cx, |state, cx| state.set_password(true, cx));
|
repass_input.update(cx, |state, cx| state.set_password(true, cx));
|
||||||
let state = cx.new(|_| DialogProgress::default());
|
|
||||||
|
|
||||||
cx.new(|_| Self {
|
cx.new(|_| Self {
|
||||||
name_input,
|
name_input,
|
||||||
+4
-8
@@ -13,8 +13,8 @@ use gpui_native_kit::input::{Input, InputState, InputStateEvent};
|
|||||||
use gpui_native_kit::theme::ActiveTheme as _;
|
use gpui_native_kit::theme::ActiveTheme as _;
|
||||||
use signed_state::Backend;
|
use signed_state::Backend;
|
||||||
|
|
||||||
use super::progress::{DialogProgress, error_row, push_steps_row};
|
use crate::shared::dialogs::close_global;
|
||||||
use super::{close_global, open_global};
|
use crate::shared::progress::{DialogProgress, error_row, push_steps_row};
|
||||||
|
|
||||||
pub struct PassphraseDialog {
|
pub struct PassphraseDialog {
|
||||||
pass_input: Entity<InputState>,
|
pass_input: Entity<InputState>,
|
||||||
@@ -24,9 +24,10 @@ pub struct PassphraseDialog {
|
|||||||
|
|
||||||
impl PassphraseDialog {
|
impl PassphraseDialog {
|
||||||
pub fn new(cx: &mut App) -> Entity<Self> {
|
pub fn new(cx: &mut App) -> Entity<Self> {
|
||||||
|
let state = cx.new(|_| DialogProgress::default());
|
||||||
|
|
||||||
let pass_input = InputState::new(cx);
|
let pass_input = InputState::new(cx);
|
||||||
pass_input.update(cx, |state, cx| state.set_password(true, cx));
|
pass_input.update(cx, |state, cx| state.set_password(true, cx));
|
||||||
let state = cx.new(|_| DialogProgress::default());
|
|
||||||
|
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
let enter_subscription =
|
let enter_subscription =
|
||||||
@@ -132,8 +133,3 @@ impl Render for PassphraseDialog {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opens the dialog; used by the backend passphrase event and the startup fallback.
|
|
||||||
pub fn open(cx: &mut App) {
|
|
||||||
open_global(PassphraseDialog::new(cx), cx);
|
|
||||||
}
|
|
||||||
+3
-7
@@ -15,8 +15,8 @@ use signed_core::GitEvent;
|
|||||||
use signed_state::RepoStore;
|
use signed_state::RepoStore;
|
||||||
use utils::relative_time;
|
use utils::relative_time;
|
||||||
|
|
||||||
use crate::components::status_badge;
|
use crate::issues::helpers::{comment_form, comments_section, issue_roots, sidebar_section};
|
||||||
use crate::views::discussion::{comment_form, comments_section, issue_roots, sidebar_section};
|
use crate::shared::widgets::{repo_tab_avatar, status_badge};
|
||||||
|
|
||||||
pub struct IssueDetailView {
|
pub struct IssueDetailView {
|
||||||
store: Entity<RepoStore>,
|
store: Entity<RepoStore>,
|
||||||
@@ -97,11 +97,7 @@ impl Panel for IssueDetailView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn lead(&self, cx: &gpui::App) -> Option<AnyElement> {
|
fn lead(&self, cx: &gpui::App) -> Option<AnyElement> {
|
||||||
Some(crate::views::repo::repo_tab_avatar(
|
Some(repo_tab_avatar(self.store.read(cx), &self.directory, cx))
|
||||||
self.store.read(cx),
|
|
||||||
&self.directory,
|
|
||||||
cx,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
+67
-16
@@ -13,23 +13,75 @@ use gpui_native_kit::theme::ActiveTheme as _;
|
|||||||
use gpui_native_kit::tooltip::Tooltip;
|
use gpui_native_kit::tooltip::Tooltip;
|
||||||
use metadata::ProfileDirectory;
|
use metadata::ProfileDirectory;
|
||||||
use nostr::prelude::{Event, EventId, PublicKey};
|
use nostr::prelude::{Event, EventId, PublicKey};
|
||||||
|
use signed_core::RepoStatus;
|
||||||
use signed_state::RepoStore;
|
use signed_state::RepoStore;
|
||||||
use utils::relative_time;
|
use utils::relative_time;
|
||||||
|
|
||||||
pub fn issue_roots(store: &RepoStore) -> &[Event] {
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub(super) enum IssueFilter {
|
||||||
|
All,
|
||||||
|
Open,
|
||||||
|
Closed,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IssueFilter {
|
||||||
|
pub(super) fn matches(self, status: RepoStatus) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::All => true,
|
||||||
|
Self::Open => status == RepoStatus::Open,
|
||||||
|
Self::Closed => matches!(status, RepoStatus::Closed | RepoStatus::Applied),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Root events counted by their resolved status.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub(super) struct StatusCounts {
|
||||||
|
pub(super) total: usize,
|
||||||
|
pub(super) open: usize,
|
||||||
|
pub(super) closed: usize,
|
||||||
|
pub(super) draft: usize,
|
||||||
|
pub(super) applied: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StatusCounts {
|
||||||
|
fn record(&mut self, status: RepoStatus) {
|
||||||
|
self.total += 1;
|
||||||
|
match status {
|
||||||
|
RepoStatus::Open => self.open += 1,
|
||||||
|
RepoStatus::Closed => self.closed += 1,
|
||||||
|
RepoStatus::Draft => self.draft += 1,
|
||||||
|
RepoStatus::Applied => self.applied += 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Indices of `roots` whose status `keep` accepts, counting every root's status.
|
||||||
|
pub(super) fn filter_by_status<'a>(
|
||||||
|
roots: impl IntoIterator<Item = &'a Event>,
|
||||||
|
status_of: impl Fn(&Event) -> RepoStatus,
|
||||||
|
keep: impl Fn(RepoStatus) -> bool,
|
||||||
|
) -> (Vec<usize>, StatusCounts) {
|
||||||
|
let mut counts = StatusCounts::default();
|
||||||
|
|
||||||
|
let visible = roots
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(index, root)| {
|
||||||
|
let status = status_of(root);
|
||||||
|
counts.record(status);
|
||||||
|
keep(status).then_some(index)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
(visible, counts)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn issue_roots(store: &RepoStore) -> &[Event] {
|
||||||
&store.issues
|
&store.issues
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sidebar_title(text: &str, cx: &App) -> AnyElement {
|
pub(super) fn sidebar_section(
|
||||||
div()
|
|
||||||
.text_xs()
|
|
||||||
.font_weight(gpui::FontWeight::SEMIBOLD)
|
|
||||||
.text_color(cx.theme().colors.muted_foreground)
|
|
||||||
.child(text.to_string())
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sidebar_section(
|
|
||||||
store: &Entity<RepoStore>,
|
store: &Entity<RepoStore>,
|
||||||
id: EventId,
|
id: EventId,
|
||||||
roots: fn(&RepoStore) -> &[Event],
|
roots: fn(&RepoStore) -> &[Event],
|
||||||
@@ -61,7 +113,7 @@ pub fn sidebar_section(
|
|||||||
v_flex()
|
v_flex()
|
||||||
.when(top_gap, |this| this.mt_4())
|
.when(top_gap, |this| this.mt_4())
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(sidebar_title("Participants", cx))
|
.child(crate::shared::widgets::section_label("Participants", cx))
|
||||||
.children(participants.iter().map(|pubkey| {
|
.children(participants.iter().map(|pubkey| {
|
||||||
let profile = directory.read(cx).profile(pubkey, cx);
|
let profile = directory.read(cx).profile(pubkey, cx);
|
||||||
let name = profile.name();
|
let name = profile.name();
|
||||||
@@ -81,7 +133,7 @@ pub fn sidebar_section(
|
|||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(sidebar_title("Labels", cx))
|
.child(crate::shared::widgets::section_label("Labels", cx))
|
||||||
.map(|this| {
|
.map(|this| {
|
||||||
if labels.is_empty() {
|
if labels.is_empty() {
|
||||||
this.child(
|
this.child(
|
||||||
@@ -101,7 +153,7 @@ pub fn sidebar_section(
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn comments_section(
|
pub(super) fn comments_section(
|
||||||
store: &Entity<RepoStore>,
|
store: &Entity<RepoStore>,
|
||||||
root: EventId,
|
root: EventId,
|
||||||
directory: &Entity<ProfileDirectory>,
|
directory: &Entity<ProfileDirectory>,
|
||||||
@@ -162,8 +214,7 @@ pub fn comments_section(
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `roots` selects the root's list within the store, issues or pull requests.
|
pub(super) fn comment_form(
|
||||||
pub fn comment_form(
|
|
||||||
store: &Entity<RepoStore>,
|
store: &Entity<RepoStore>,
|
||||||
root: EventId,
|
root: EventId,
|
||||||
roots: fn(&RepoStore) -> &[Event],
|
roots: fn(&RepoStore) -> &[Event],
|
||||||
@@ -16,37 +16,22 @@ use gpui_native_kit::segmented::{Segmented, SegmentedOption};
|
|||||||
use gpui_native_kit::theme::ActiveTheme as _;
|
use gpui_native_kit::theme::ActiveTheme as _;
|
||||||
use metadata::ProfileDirectory;
|
use metadata::ProfileDirectory;
|
||||||
use nostr::prelude::PublicKey;
|
use nostr::prelude::PublicKey;
|
||||||
use signed_core::{GitEvent, RepoStatus};
|
use signed_core::GitEvent;
|
||||||
use signed_state::RepoStore;
|
use signed_state::RepoStore;
|
||||||
use utils::relative_time;
|
use utils::relative_time;
|
||||||
|
|
||||||
use self::detail::IssueDetailView;
|
use self::detail::IssueDetailView;
|
||||||
use super::status_list::{StatusCounts, filter_by_status};
|
use helpers::{IssueFilter, StatusCounts, filter_by_status};
|
||||||
use crate::components::status_badge;
|
|
||||||
use crate::dialogs::new_issue;
|
use crate::shared::panels;
|
||||||
use crate::panels;
|
use crate::shared::widgets::{repo_tab_avatar, status_badge};
|
||||||
|
|
||||||
pub mod detail;
|
pub mod detail;
|
||||||
|
mod helpers;
|
||||||
|
pub mod new_issue;
|
||||||
|
|
||||||
const ISSUE_ROW_HEIGHT: f32 = 73.;
|
const ISSUE_ROW_HEIGHT: f32 = 73.;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
enum IssueFilter {
|
|
||||||
All,
|
|
||||||
Open,
|
|
||||||
Closed,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IssueFilter {
|
|
||||||
fn matches(self, status: RepoStatus) -> bool {
|
|
||||||
match self {
|
|
||||||
Self::All => true,
|
|
||||||
Self::Open => status == RepoStatus::Open,
|
|
||||||
Self::Closed => matches!(status, RepoStatus::Closed | RepoStatus::Applied),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct IssuesView {
|
pub struct IssuesView {
|
||||||
dock: WeakEntity<DockState>,
|
dock: WeakEntity<DockState>,
|
||||||
store: Entity<RepoStore>,
|
store: Entity<RepoStore>,
|
||||||
@@ -185,11 +170,7 @@ impl Panel for IssuesView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn lead(&self, cx: &App) -> Option<AnyElement> {
|
fn lead(&self, cx: &App) -> Option<AnyElement> {
|
||||||
Some(super::repo::repo_tab_avatar(
|
Some(repo_tab_avatar(self.store.read(cx), &self.directory, cx))
|
||||||
self.store.read(cx),
|
|
||||||
&self.directory,
|
|
||||||
cx,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
+1
-1
@@ -10,7 +10,7 @@ use gpui_native_kit::input::{Input, InputState};
|
|||||||
use gpui_native_kit::theme::ActiveTheme as _;
|
use gpui_native_kit::theme::ActiveTheme as _;
|
||||||
use signed_state::RepoStore;
|
use signed_state::RepoStore;
|
||||||
|
|
||||||
use super::{close_global, open_global};
|
use crate::shared::dialogs::{close_global, open_global};
|
||||||
|
|
||||||
pub struct NewIssueDialog {
|
pub struct NewIssueDialog {
|
||||||
store: Entity<RepoStore>,
|
store: Entity<RepoStore>,
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
mod app;
|
mod app;
|
||||||
pub mod components;
|
mod explore;
|
||||||
mod dialogs;
|
mod identity;
|
||||||
mod panels;
|
mod issues;
|
||||||
mod views;
|
mod repo;
|
||||||
|
mod shared;
|
||||||
|
mod sidebar;
|
||||||
|
|
||||||
pub use app::{AppRoot, apply_appearance};
|
pub use app::AppRoot;
|
||||||
|
pub use app::helpers::apply_appearance;
|
||||||
|
|
||||||
use gpui::{App, AppContext as _, Entity, Window};
|
use gpui::{App, AppContext as _, Entity, Window};
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ use gpui::{App, AppContext as _, SharedString, WeakEntity, Window};
|
|||||||
use gpui_native_kit::layout::dock::DockState;
|
use gpui_native_kit::layout::dock::DockState;
|
||||||
use signed_core::{Announcement, RepoAddr};
|
use signed_core::{Announcement, RepoAddr};
|
||||||
|
|
||||||
use crate::panels;
|
use crate::repo::RepoDetailView;
|
||||||
use crate::views::repo::RepoDetailView;
|
use crate::shared::panels;
|
||||||
|
|
||||||
/// Opens a repository as a dock panel, keyed by its address.
|
/// Opens a repository as a dock panel, keyed by its address.
|
||||||
pub fn open_repo_panel(
|
pub fn open_repo_panel(
|
||||||
+13
-132
@@ -11,7 +11,6 @@ use gpui_native_kit::badge::{Badge, BadgeVariant};
|
|||||||
use gpui_native_kit::clipboard::ClipboardButton;
|
use gpui_native_kit::clipboard::ClipboardButton;
|
||||||
use gpui_native_kit::common::{h_flex, v_flex};
|
use gpui_native_kit::common::{h_flex, v_flex};
|
||||||
use gpui_native_kit::data::tree::{Tree, TreeItem};
|
use gpui_native_kit::data::tree::{Tree, TreeItem};
|
||||||
use gpui_native_kit::empty_state::EmptyState;
|
|
||||||
use gpui_native_kit::icons::{Icon, IconRole};
|
use gpui_native_kit::icons::{Icon, IconRole};
|
||||||
use gpui_native_kit::layout::dock::Panel;
|
use gpui_native_kit::layout::dock::Panel;
|
||||||
use gpui_native_kit::layout::resizable::{Resizable, ResizableState};
|
use gpui_native_kit::layout::resizable::{Resizable, ResizableState};
|
||||||
@@ -19,17 +18,17 @@ use gpui_native_kit::scrollbar::Scrollbar;
|
|||||||
use gpui_native_kit::spinner::Spinner;
|
use gpui_native_kit::spinner::Spinner;
|
||||||
use gpui_native_kit::theme::ActiveTheme as _;
|
use gpui_native_kit::theme::ActiveTheme as _;
|
||||||
use metadata::ProfileDirectory;
|
use metadata::ProfileDirectory;
|
||||||
use signed_git::{
|
use signed_git::{CommitDiff, DiffStatus, FileCommit, FileDiff, Repo};
|
||||||
CommitDiff, DiffHunk, DiffLine, DiffLineKind, DiffStatus, FileCommit, FileDiff, Repo,
|
|
||||||
};
|
|
||||||
use signed_state::RepoStore;
|
use signed_state::RepoStore;
|
||||||
use utils::relative_time_secs;
|
use utils::relative_time_secs;
|
||||||
|
|
||||||
use crate::views::tree::{TreeRow, build_tree_items, collect_expanded, flatten};
|
use super::helpers::{DiffRow, diff_rows, render_diff_row};
|
||||||
|
use crate::shared::tree::{TreeItemSeed, TreeRow, build_tree_items, collect_expanded, flatten};
|
||||||
|
use crate::shared::widgets::{empty_state, repo_tab_avatar};
|
||||||
|
|
||||||
pub struct DiffPane {
|
pub struct DiffPane {
|
||||||
diff: Option<CommitDiff>,
|
diff: Option<CommitDiff>,
|
||||||
seeds: Vec<crate::views::tree::TreeItemSeed>,
|
seeds: Vec<TreeItemSeed>,
|
||||||
expanded: HashSet<String>,
|
expanded: HashSet<String>,
|
||||||
rows: Vec<TreeRow>,
|
rows: Vec<TreeRow>,
|
||||||
selected_file: Option<SharedString>,
|
selected_file: Option<SharedString>,
|
||||||
@@ -195,17 +194,17 @@ impl DiffPane {
|
|||||||
|
|
||||||
fn render_detail_column(&self, cx: &mut Context<Self>) -> AnyElement {
|
fn render_detail_column(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
let Some(diff) = self.diff.as_ref() else {
|
let Some(diff) = self.diff.as_ref() else {
|
||||||
return placeholder("No changes");
|
return empty_state("No changes");
|
||||||
};
|
};
|
||||||
let Some(path) = self.selected_file.clone() else {
|
let Some(path) = self.selected_file.clone() else {
|
||||||
return if diff.files.is_empty() {
|
return if diff.files.is_empty() {
|
||||||
placeholder("No files changed")
|
empty_state("No files changed")
|
||||||
} else {
|
} else {
|
||||||
placeholder("Select a file")
|
empty_state("Select a file")
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
let Some(file) = diff.files.iter().find(|file| file.path == path.as_ref()) else {
|
let Some(file) = diff.files.iter().find(|file| file.path == path.as_ref()) else {
|
||||||
return placeholder("File not found");
|
return empty_state("File not found");
|
||||||
};
|
};
|
||||||
self.render_file_diff(file, cx)
|
self.render_file_diff(file, cx)
|
||||||
}
|
}
|
||||||
@@ -230,9 +229,9 @@ impl DiffPane {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let body: AnyElement = if file.binary {
|
let body: AnyElement = if file.binary {
|
||||||
placeholder("Diff not available")
|
empty_state("Diff not available")
|
||||||
} else if file.hunks.is_empty() {
|
} else if file.hunks.is_empty() {
|
||||||
placeholder("No content changes")
|
empty_state("No content changes")
|
||||||
} else {
|
} else {
|
||||||
let hunks = file.hunks.clone();
|
let hunks = file.hunks.clone();
|
||||||
let rows = self.diff_rows.clone();
|
let rows = self.diff_rows.clone();
|
||||||
@@ -515,11 +514,7 @@ impl Panel for CommitDiffView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn lead(&self, cx: &App) -> Option<AnyElement> {
|
fn lead(&self, cx: &App) -> Option<AnyElement> {
|
||||||
Some(super::repo_tab_avatar(
|
Some(repo_tab_avatar(self.store.read(cx), &self.directory, cx))
|
||||||
self.store.read(cx),
|
|
||||||
&self.directory,
|
|
||||||
cx,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -533,7 +528,7 @@ impl Render for CommitDiffView {
|
|||||||
.child(Spinner::new("commit-diff-loading"))
|
.child(Spinner::new("commit-diff-loading"))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
} else if let Some(error) = self.error.clone() {
|
} else if let Some(error) = self.error.clone() {
|
||||||
placeholder(&error)
|
empty_state(error)
|
||||||
} else {
|
} else {
|
||||||
self.pane.clone().into_any_element()
|
self.pane.clone().into_any_element()
|
||||||
};
|
};
|
||||||
@@ -545,117 +540,3 @@ impl Render for CommitDiffView {
|
|||||||
.child(div().flex_1().min_h_0().child(body))
|
.child(div().flex_1().min_h_0().child(body))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const GUTTER_WIDTH: f32 = 44.;
|
|
||||||
pub const DIFF_ROW_HEIGHT: f32 = 20.;
|
|
||||||
pub const COMMIT_ROW_HEIGHT: f32 = 56.;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
enum DiffRow {
|
|
||||||
Hunk {
|
|
||||||
old_start: u32,
|
|
||||||
old_lines: u32,
|
|
||||||
new_start: u32,
|
|
||||||
new_lines: u32,
|
|
||||||
},
|
|
||||||
Line {
|
|
||||||
hunk: usize,
|
|
||||||
line: usize,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
fn diff_rows(file: &FileDiff) -> Vec<DiffRow> {
|
|
||||||
let mut rows = Vec::new();
|
|
||||||
for (hunk_ix, hunk) in file.hunks.iter().enumerate() {
|
|
||||||
rows.push(DiffRow::Hunk {
|
|
||||||
old_start: hunk.old_start,
|
|
||||||
old_lines: hunk.old_lines,
|
|
||||||
new_start: hunk.new_start,
|
|
||||||
new_lines: hunk.new_lines,
|
|
||||||
});
|
|
||||||
rows.extend((0..hunk.lines.len()).map(|line| DiffRow::Line {
|
|
||||||
hunk: hunk_ix,
|
|
||||||
line,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
rows
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_diff_row(hunks: &[DiffHunk], row: DiffRow, cx: &App) -> AnyElement {
|
|
||||||
match row {
|
|
||||||
DiffRow::Hunk {
|
|
||||||
old_start,
|
|
||||||
old_lines,
|
|
||||||
new_start,
|
|
||||||
new_lines,
|
|
||||||
} => div()
|
|
||||||
.px_2()
|
|
||||||
.w_full()
|
|
||||||
.h(px(DIFF_ROW_HEIGHT))
|
|
||||||
.font_family(cx.theme().mono_font)
|
|
||||||
.text_xs()
|
|
||||||
.bg(cx.theme().colors.muted)
|
|
||||||
.border_y_1()
|
|
||||||
.border_color(cx.theme().colors.border)
|
|
||||||
.text_color(cx.theme().colors.muted_foreground)
|
|
||||||
.child(SharedString::from(format!(
|
|
||||||
"@@ -{},{} +{},{} @@",
|
|
||||||
old_start, old_lines, new_start, new_lines
|
|
||||||
)))
|
|
||||||
.into_any_element(),
|
|
||||||
DiffRow::Line { hunk, line } => render_diff_line(&hunks[hunk].lines[line], cx),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_diff_line(line: &DiffLine, cx: &App) -> AnyElement {
|
|
||||||
let bg = match line.kind {
|
|
||||||
DiffLineKind::Addition => Some(cx.theme().ramps.success.base.opacity(0.2)),
|
|
||||||
DiffLineKind::Deletion => Some(cx.theme().ramps.destructive.base.opacity(0.2)),
|
|
||||||
DiffLineKind::Context => None,
|
|
||||||
};
|
|
||||||
let gutter = cx.theme().colors.muted_foreground;
|
|
||||||
|
|
||||||
h_flex()
|
|
||||||
.w_full()
|
|
||||||
.h(px(DIFF_ROW_HEIGHT))
|
|
||||||
.font_family(cx.theme().mono_font)
|
|
||||||
.text_xs()
|
|
||||||
.when_some(bg, |this, bg| this.bg(bg))
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.w(px(GUTTER_WIDTH))
|
|
||||||
.flex_none()
|
|
||||||
.pr_2()
|
|
||||||
.text_right()
|
|
||||||
.text_color(gutter)
|
|
||||||
.child(line.old.map(|n| n.to_string()).unwrap_or_default()),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.w(px(GUTTER_WIDTH))
|
|
||||||
.flex_none()
|
|
||||||
.pr_2()
|
|
||||||
.text_right()
|
|
||||||
.text_color(gutter)
|
|
||||||
.child(line.new.map(|n| n.to_string()).unwrap_or_default()),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.flex_1()
|
|
||||||
.min_w_0()
|
|
||||||
.overflow_hidden()
|
|
||||||
.whitespace_nowrap()
|
|
||||||
.text_color(cx.theme().colors.foreground)
|
|
||||||
.child(line.text.clone()),
|
|
||||||
)
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn placeholder(message: impl Into<SharedString>) -> AnyElement {
|
|
||||||
v_flex()
|
|
||||||
.size_full()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.child(EmptyState::new(message))
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
+1
-1
@@ -12,7 +12,7 @@ use nostr::prelude::PublicKey;
|
|||||||
use signed_core::Announcement;
|
use signed_core::Announcement;
|
||||||
use utils::middle_truncate;
|
use utils::middle_truncate;
|
||||||
|
|
||||||
use super::{close_global, open_global};
|
use crate::shared::dialogs::{close_global, open_global};
|
||||||
|
|
||||||
pub struct AboutDialog {
|
pub struct AboutDialog {
|
||||||
announcement: Announcement,
|
announcement: Announcement,
|
||||||
+3
-3
@@ -18,9 +18,9 @@ use settings::SettingsStore;
|
|||||||
use signed_state::{Backend, CheckoutsStore};
|
use signed_state::{Backend, CheckoutsStore};
|
||||||
|
|
||||||
use super::grasp_servers::{GraspServersState, grasp_servers_field, load_user_grasp_servers};
|
use super::grasp_servers::{GraspServersState, grasp_servers_field, load_user_grasp_servers};
|
||||||
use super::progress::{DialogProgress, error_row, push_steps_row};
|
use crate::repo::actions::open_repo_panel;
|
||||||
use super::{close_global, open_global};
|
use crate::shared::dialogs::{close_global, open_global};
|
||||||
use crate::views::repo::actions::open_repo_panel;
|
use crate::shared::progress::{DialogProgress, error_row, push_steps_row};
|
||||||
|
|
||||||
/// Progress of the create-repository flow, so async results can be rendered.
|
/// Progress of the create-repository flow, so async results can be rendered.
|
||||||
pub struct CreateRepoDialog {
|
pub struct CreateRepoDialog {
|
||||||
+3
-3
@@ -19,9 +19,9 @@ use signed_git::Repo;
|
|||||||
use signed_state::Backend;
|
use signed_state::Backend;
|
||||||
|
|
||||||
use super::grasp_servers::{GraspServersState, grasp_servers_field, load_user_grasp_servers};
|
use super::grasp_servers::{GraspServersState, grasp_servers_field, load_user_grasp_servers};
|
||||||
use super::progress::{DialogProgress, error_row, push_steps_row};
|
use crate::repo::RepoDetailView;
|
||||||
use super::{close_global, open_global};
|
use crate::shared::dialogs::{close_global, open_global};
|
||||||
use crate::views::repo::RepoDetailView;
|
use crate::shared::progress::{DialogProgress, error_row, push_steps_row};
|
||||||
|
|
||||||
pub struct InitRepoDialog {
|
pub struct InitRepoDialog {
|
||||||
local_path: PathBuf,
|
local_path: PathBuf,
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod about;
|
||||||
|
pub mod create;
|
||||||
|
pub mod grasp_servers;
|
||||||
|
pub mod init;
|
||||||
|
pub mod reset;
|
||||||
+1
-1
@@ -8,7 +8,7 @@ use gpui_native_kit::dialog::Dialog;
|
|||||||
use gpui_native_kit::theme::ActiveTheme as _;
|
use gpui_native_kit::theme::ActiveTheme as _;
|
||||||
use signed_state::RepoStore;
|
use signed_state::RepoStore;
|
||||||
|
|
||||||
use super::{close_global, open_global};
|
use crate::shared::dialogs::{close_global, open_global};
|
||||||
|
|
||||||
pub struct ResetRemoteDialog {
|
pub struct ResetRemoteDialog {
|
||||||
store: Entity<RepoStore>,
|
store: Entity<RepoStore>,
|
||||||
@@ -10,15 +10,14 @@ use gpui::{
|
|||||||
use gpui_native_kit::common::{h_flex, v_flex};
|
use gpui_native_kit::common::{h_flex, v_flex};
|
||||||
use gpui_native_kit::data::tree::{Tree, TreeItem};
|
use gpui_native_kit::data::tree::{Tree, TreeItem};
|
||||||
use gpui_native_kit::editor::code::CodeViewState;
|
use gpui_native_kit::editor::code::CodeViewState;
|
||||||
use gpui_native_kit::editor::highlight::Language;
|
|
||||||
use gpui_native_kit::editor::markdown::MarkdownViewState;
|
use gpui_native_kit::editor::markdown::MarkdownViewState;
|
||||||
use gpui_native_kit::empty_state::EmptyState;
|
|
||||||
use gpui_native_kit::icons::{Icon, IconRole};
|
use gpui_native_kit::icons::{Icon, IconRole};
|
||||||
use gpui_native_kit::spinner::Spinner;
|
|
||||||
use gpui_native_kit::theme::ActiveTheme as _;
|
use gpui_native_kit::theme::ActiveTheme as _;
|
||||||
use signed_git::{FileCommit, Repo, WorktreeSnapshot};
|
use signed_git::{FileCommit, Repo, WorktreeSnapshot};
|
||||||
|
|
||||||
use crate::views::tree::{TreeItemSeed, TreeRow, flatten};
|
use super::helpers::{code_language, is_markdown_path, preview_spinner, source_hash};
|
||||||
|
use crate::shared::tree::{TreeItemSeed, TreeRow, collect_expanded, flatten};
|
||||||
|
use crate::shared::widgets::empty_state;
|
||||||
|
|
||||||
const TREE_WIDTH: f32 = 240.;
|
const TREE_WIDTH: f32 = 240.;
|
||||||
const MAX_PREVIEW_BYTES: usize = 1024 * 1024;
|
const MAX_PREVIEW_BYTES: usize = 1024 * 1024;
|
||||||
@@ -106,7 +105,7 @@ impl RepoFilesView {
|
|||||||
) {
|
) {
|
||||||
self.worktree_paths = paths;
|
self.worktree_paths = paths;
|
||||||
self.seeds = tree;
|
self.seeds = tree;
|
||||||
let folder_ids = folder_ids(&self.seeds);
|
let folder_ids = collect_expanded(&self.seeds);
|
||||||
self.expanded.retain(|id| folder_ids.contains(id));
|
self.expanded.retain(|id| folder_ids.contains(id));
|
||||||
self.rebuild_rows();
|
self.rebuild_rows();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -323,15 +322,15 @@ impl RepoFilesView {
|
|||||||
self.code_element(path.as_ref())
|
self.code_element(path.as_ref())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(FileContent::Binary) => placeholder("Binary file - preview not supported"),
|
Some(FileContent::Binary) => empty_state("Binary file - preview not supported"),
|
||||||
Some(FileContent::TooLarge) => placeholder("File is too large to preview"),
|
Some(FileContent::TooLarge) => empty_state("File is too large to preview"),
|
||||||
Some(FileContent::Failed(message)) => placeholder(message),
|
Some(FileContent::Failed(message)) => empty_state(message),
|
||||||
None => preview_spinner(),
|
None => preview_spinner(),
|
||||||
}
|
}
|
||||||
} else if self.readme_name.is_some() {
|
} else if self.readme_name.is_some() {
|
||||||
self.markdown_element(None)
|
self.markdown_element(None)
|
||||||
} else {
|
} else {
|
||||||
placeholder("No README found")
|
empty_state("No README found")
|
||||||
};
|
};
|
||||||
|
|
||||||
let commit = match &self.selected_file {
|
let commit = match &self.selected_file {
|
||||||
@@ -735,75 +734,3 @@ impl Render for RepoFilesView {
|
|||||||
.child(self.render_content_column(pane_title, cx))
|
.child(self.render_content_column(pane_title, cx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn folder_ids(seeds: &[TreeItemSeed]) -> HashSet<String> {
|
|
||||||
fn push(seed: &TreeItemSeed, ids: &mut HashSet<String>) {
|
|
||||||
if !seed.children.is_empty() {
|
|
||||||
ids.insert(seed.id.clone());
|
|
||||||
for child in &seed.children {
|
|
||||||
push(child, ids);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut ids = HashSet::new();
|
|
||||||
for seed in seeds {
|
|
||||||
push(seed, &mut ids);
|
|
||||||
}
|
|
||||||
ids
|
|
||||||
}
|
|
||||||
|
|
||||||
fn source_hash(text: &str) -> u64 {
|
|
||||||
use std::hash::{Hash, Hasher};
|
|
||||||
|
|
||||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
|
||||||
text.hash(&mut hasher);
|
|
||||||
hasher.finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn preview_spinner() -> AnyElement {
|
|
||||||
v_flex()
|
|
||||||
.size_full()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.child(Spinner::new("repo-preview-spinner"))
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn placeholder(message: impl Into<SharedString>) -> AnyElement {
|
|
||||||
v_flex()
|
|
||||||
.size_full()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.child(EmptyState::new(message))
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The kit grammar for a path; only four languages are supported.
|
|
||||||
fn code_language(path: &str) -> Language {
|
|
||||||
match Path::new(path)
|
|
||||||
.extension()
|
|
||||||
.and_then(|ext| ext.to_str())
|
|
||||||
.map(|ext| ext.to_ascii_lowercase())
|
|
||||||
.as_deref()
|
|
||||||
{
|
|
||||||
Some("rs") => Language::Rust,
|
|
||||||
Some("ts" | "mts" | "cts" | "tsx") => Language::TypeScript,
|
|
||||||
Some("js" | "mjs" | "cjs" | "jsx") => Language::JavaScript,
|
|
||||||
Some("py") => Language::Python,
|
|
||||||
_ => Language::Plain,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether a file path has a markdown extension.
|
|
||||||
fn is_markdown_path(path: &str) -> bool {
|
|
||||||
Path::new(path)
|
|
||||||
.extension()
|
|
||||||
.and_then(|ext| ext.to_str())
|
|
||||||
.is_some_and(|ext| {
|
|
||||||
matches!(
|
|
||||||
ext.to_ascii_lowercase().as_str(),
|
|
||||||
"md" | "markdown" | "mdown" | "mkdn"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,514 @@
|
|||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use anyhow::Error;
|
||||||
|
use assets::CustomIconName;
|
||||||
|
use gpui::prelude::FluentBuilder as _;
|
||||||
|
use gpui::{
|
||||||
|
AnyElement, App, ClipboardItem, Context, Entity, InteractiveElement as _, IntoElement,
|
||||||
|
ParentElement as _, SharedString, StatefulInteractiveElement as _, Styled as _, Window, div,
|
||||||
|
px,
|
||||||
|
};
|
||||||
|
use gpui_native_kit::avatar::Avatar;
|
||||||
|
use gpui_native_kit::clipboard::ClipboardButton;
|
||||||
|
use gpui_native_kit::combobox::ComboboxState;
|
||||||
|
use gpui_native_kit::common::{Sizable as _, h_flex, v_flex};
|
||||||
|
use gpui_native_kit::icons::Icon;
|
||||||
|
use gpui_native_kit::menu::MenuItem;
|
||||||
|
use gpui_native_kit::theme::ActiveTheme as _;
|
||||||
|
use metadata::ProfileDirectory;
|
||||||
|
use nostr::nips::nip19::Nip19Coordinate;
|
||||||
|
use nostr::prelude::{RelayUrl, ToBech32};
|
||||||
|
use signed_core::Announcement;
|
||||||
|
use signed_git::{DiffHunk, DiffLine, DiffLineKind, FileCommit, FileDiff, Repo};
|
||||||
|
use signed_state::{Nip34Binding, RepoListStore, RepoStore};
|
||||||
|
use utils::{middle_truncate, relative_time_secs};
|
||||||
|
|
||||||
|
use super::RepoDetailView;
|
||||||
|
use crate::shared::tree::{TreeItemSeed, build_tree_items};
|
||||||
|
|
||||||
|
pub(super) struct RepoData {
|
||||||
|
pub(super) tree: Vec<TreeItemSeed>,
|
||||||
|
pub(super) entries: Vec<PathBuf>,
|
||||||
|
pub(super) readme_path: Option<PathBuf>,
|
||||||
|
pub(super) readme: Option<Vec<u8>>,
|
||||||
|
pub(super) worktree: Option<PathBuf>,
|
||||||
|
pub(super) branches: Vec<String>,
|
||||||
|
pub(super) tags: Vec<String>,
|
||||||
|
pub(super) current_branch: Option<String>,
|
||||||
|
pub(super) head_commit: Option<FileCommit>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn clone_group(label: &str, id: &'static str, command: &SharedString) -> AnyElement {
|
||||||
|
v_flex()
|
||||||
|
.gap_1()
|
||||||
|
.child(div().text_xs().child(SharedString::from(label)))
|
||||||
|
.child(ClipboardButton::new(id, command.clone()))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn nostr_clone_url(announcement: &Announcement, nip05: Option<&str>) -> SharedString {
|
||||||
|
let owner = announcement.owner;
|
||||||
|
let user = nip05
|
||||||
|
.map(str::to_owned)
|
||||||
|
.unwrap_or_else(|| owner.to_bech32().unwrap_or_else(|_| owner.to_string()));
|
||||||
|
|
||||||
|
let mut url = format!("nostr://{user}");
|
||||||
|
if let Some(hint) = announcement.relays.first().and_then(RelayUrl::domain) {
|
||||||
|
url.push('/');
|
||||||
|
url.push_str(hint);
|
||||||
|
}
|
||||||
|
url.push('/');
|
||||||
|
url.push_str(&announcement.id);
|
||||||
|
|
||||||
|
SharedString::from(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn fork_row(
|
||||||
|
announcement: &Announcement,
|
||||||
|
cx: &mut Context<RepoDetailView>,
|
||||||
|
) -> Option<AnyElement> {
|
||||||
|
let upstream = announcement.upstream.as_ref()?;
|
||||||
|
|
||||||
|
let (label, clickable) = match &upstream.addr {
|
||||||
|
Some(addr) => {
|
||||||
|
let name = RepoListStore::global(cx)
|
||||||
|
.read(cx)
|
||||||
|
.announcements
|
||||||
|
.iter()
|
||||||
|
.find(|a| a.addr() == *addr)
|
||||||
|
.map(|a| {
|
||||||
|
a.name
|
||||||
|
.as_deref()
|
||||||
|
.map(SharedString::from)
|
||||||
|
.unwrap_or_else(|| SharedString::from(a.id.clone()))
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| SharedString::from(addr.identifier().to_owned()));
|
||||||
|
(SharedString::from(format!("Forked from {name}")), true)
|
||||||
|
}
|
||||||
|
None => (SharedString::from(upstream.display().as_str()), false),
|
||||||
|
};
|
||||||
|
|
||||||
|
let row = h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.min_w_0()
|
||||||
|
.text_sm()
|
||||||
|
.text_color(cx.theme().colors.muted_foreground)
|
||||||
|
.child(Icon::named(CustomIconName::GitBranch).size(px(14.)))
|
||||||
|
.child(div().whitespace_nowrap().text_ellipsis().child(label));
|
||||||
|
|
||||||
|
Some(if clickable {
|
||||||
|
row.id("fork-upstream")
|
||||||
|
.cursor_pointer()
|
||||||
|
.on_click(cx.listener(|this, _, window, cx| this.open_upstream(window, cx)))
|
||||||
|
.into_any_element()
|
||||||
|
} else {
|
||||||
|
row.into_any_element()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(super) struct ShareTargets {
|
||||||
|
/// NIP-19 `naddr1...` of the announcement, with its announced relays.
|
||||||
|
pub(super) naddr: String,
|
||||||
|
/// Hex ID of the announcement event itself.
|
||||||
|
event_id: String,
|
||||||
|
/// NIP-34 coordinate `30617:<pubkey>:<repo-id>`.
|
||||||
|
coordinate: String,
|
||||||
|
gitworkshop: String,
|
||||||
|
ditto: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShareTargets {
|
||||||
|
pub(super) fn from_announcement(announcement: &Announcement) -> Self {
|
||||||
|
let addr = announcement.addr();
|
||||||
|
let coordinate = addr.to_string();
|
||||||
|
let naddr = Nip19Coordinate::new(addr.into(), announcement.relays.iter().cloned())
|
||||||
|
.to_bech32()
|
||||||
|
.unwrap_or_else(|_| coordinate.clone());
|
||||||
|
|
||||||
|
Self {
|
||||||
|
naddr: naddr.clone(),
|
||||||
|
event_id: announcement
|
||||||
|
.event_id
|
||||||
|
.to_bech32()
|
||||||
|
.unwrap_or_else(|_| announcement.event_id.to_string()),
|
||||||
|
coordinate,
|
||||||
|
gitworkshop: format!("https://gitworkshop.dev/{naddr}"),
|
||||||
|
ditto: format!("https://ditto.pub/{naddr}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn menu(&self) -> Vec<MenuItem> {
|
||||||
|
let items = [
|
||||||
|
("GitWorkshop", truncate_naddr_link(&self.gitworkshop, 4)),
|
||||||
|
("Ditto", truncate_naddr_link(&self.ditto, 4)),
|
||||||
|
("Event ID", middle_truncate(&self.event_id, 10, 10)),
|
||||||
|
("Coordinate", middle_truncate(&self.coordinate, 10, 10)),
|
||||||
|
];
|
||||||
|
|
||||||
|
let values = [
|
||||||
|
self.gitworkshop.clone(),
|
||||||
|
self.ditto.clone(),
|
||||||
|
self.event_id.clone(),
|
||||||
|
self.coordinate.clone(),
|
||||||
|
];
|
||||||
|
|
||||||
|
items
|
||||||
|
.into_iter()
|
||||||
|
.zip(values)
|
||||||
|
.map(|((label, detail), value)| {
|
||||||
|
MenuItem::new(SharedString::from(format!("{label} {detail}"))).handler(
|
||||||
|
move |_, cx| {
|
||||||
|
cx.write_to_clipboard(ClipboardItem::new_string(value.clone()));
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn truncate_naddr_link(url: &str, tail: usize) -> String {
|
||||||
|
let Some(end) = url.find("naddr1").map(|i| i + "naddr1".len()) else {
|
||||||
|
return url.to_string();
|
||||||
|
};
|
||||||
|
if url.len() - end <= tail + 3 {
|
||||||
|
return url.to_string();
|
||||||
|
}
|
||||||
|
format!("{}...{}", &url[..end], &url[url.len() - tail..])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn load_repo_data(repo: &Repo) -> Result<RepoData, Error> {
|
||||||
|
let snapshot = repo.snapshot()?;
|
||||||
|
let worktree = repo.workdir().map(Path::to_path_buf);
|
||||||
|
let tree = build_tree_items(&snapshot.entries);
|
||||||
|
|
||||||
|
Ok(RepoData {
|
||||||
|
tree,
|
||||||
|
entries: snapshot.entries,
|
||||||
|
readme_path: snapshot.readme_path,
|
||||||
|
readme: snapshot.readme,
|
||||||
|
worktree,
|
||||||
|
branches: snapshot.branches,
|
||||||
|
tags: snapshot.tags,
|
||||||
|
current_branch: snapshot.current_branch,
|
||||||
|
head_commit: snapshot.head_commit,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The announcement's name or ID for announced repositories, the directory name for local ones.
|
||||||
|
pub fn repo_display_name(store: &RepoStore) -> SharedString {
|
||||||
|
if store.addr().is_none() {
|
||||||
|
return store
|
||||||
|
.path
|
||||||
|
.as_ref()
|
||||||
|
.map(|path| {
|
||||||
|
SharedString::from(
|
||||||
|
path.file_name()
|
||||||
|
.map(|name| name.to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_else(|| path.display().to_string()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
}
|
||||||
|
|
||||||
|
store
|
||||||
|
.announcement
|
||||||
|
.as_ref()
|
||||||
|
.map(|announcement| {
|
||||||
|
announcement
|
||||||
|
.name
|
||||||
|
.as_deref()
|
||||||
|
.map(SharedString::from)
|
||||||
|
.unwrap_or_else(|| SharedString::from(announcement.id.clone()))
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The owner and identifier a local repository is already bound to.
|
||||||
|
pub(super) fn bound_repo_label(
|
||||||
|
binding: &Nip34Binding,
|
||||||
|
directory: &Entity<ProfileDirectory>,
|
||||||
|
cx: &App,
|
||||||
|
) -> AnyElement {
|
||||||
|
let profile = binding.owner.map(|pk| directory.read(cx).profile(&pk, cx));
|
||||||
|
|
||||||
|
h_flex()
|
||||||
|
.flex_shrink_0()
|
||||||
|
.gap_2()
|
||||||
|
.text_sm()
|
||||||
|
.child("Initialized by")
|
||||||
|
.text_color(cx.theme().colors.muted_foreground)
|
||||||
|
.when_some(profile, |this, profile| {
|
||||||
|
let name = profile.name();
|
||||||
|
this.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.text_color(cx.theme().colors.foreground)
|
||||||
|
.child(
|
||||||
|
Avatar::new(profile.public_key())
|
||||||
|
.name(name.clone())
|
||||||
|
.picture(profile.picture())
|
||||||
|
.small(),
|
||||||
|
)
|
||||||
|
.child(name),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when_some(binding.identifier.as_deref(), |this, ident| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.font_weight(gpui::FontWeight::SEMIBOLD)
|
||||||
|
.child(SharedString::from(format!("/{ident}"))),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes a source hash from the given text.
|
||||||
|
pub(super) fn source_hash(text: &str) -> u64 {
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
|
||||||
|
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||||
|
text.hash(&mut hasher);
|
||||||
|
hasher.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn preview_spinner() -> AnyElement {
|
||||||
|
v_flex()
|
||||||
|
.size_full()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.child(gpui_native_kit::spinner::Spinner::new(
|
||||||
|
"repo-preview-spinner",
|
||||||
|
))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The kit grammar for a path; only four languages are supported.
|
||||||
|
pub(super) fn code_language(path: &str) -> gpui_native_kit::editor::highlight::Language {
|
||||||
|
use gpui_native_kit::editor::highlight::Language;
|
||||||
|
match Path::new(path)
|
||||||
|
.extension()
|
||||||
|
.and_then(|ext| ext.to_str())
|
||||||
|
.map(|ext| ext.to_ascii_lowercase())
|
||||||
|
.as_deref()
|
||||||
|
{
|
||||||
|
Some("rs") => Language::Rust,
|
||||||
|
Some("ts" | "mts" | "cts" | "tsx") => Language::TypeScript,
|
||||||
|
Some("js" | "mjs" | "cjs" | "jsx") => Language::JavaScript,
|
||||||
|
Some("py") => Language::Python,
|
||||||
|
_ => Language::Plain,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether a file path has a markdown extension.
|
||||||
|
pub(super) fn is_markdown_path(path: &str) -> bool {
|
||||||
|
Path::new(path)
|
||||||
|
.extension()
|
||||||
|
.and_then(|ext| ext.to_str())
|
||||||
|
.is_some_and(|ext| {
|
||||||
|
matches!(
|
||||||
|
ext.to_ascii_lowercase().as_str(),
|
||||||
|
"md" | "markdown" | "mdown" | "mkdn"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const GUTTER_WIDTH: f32 = 44.;
|
||||||
|
pub const DIFF_ROW_HEIGHT: f32 = 20.;
|
||||||
|
pub const COMMIT_ROW_HEIGHT: f32 = 56.;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub(super) enum DiffRow {
|
||||||
|
Hunk {
|
||||||
|
old_start: u32,
|
||||||
|
old_lines: u32,
|
||||||
|
new_start: u32,
|
||||||
|
new_lines: u32,
|
||||||
|
},
|
||||||
|
Line {
|
||||||
|
hunk: usize,
|
||||||
|
line: usize,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn diff_rows(file: &FileDiff) -> Vec<DiffRow> {
|
||||||
|
let mut rows = Vec::new();
|
||||||
|
for (hunk_ix, hunk) in file.hunks.iter().enumerate() {
|
||||||
|
rows.push(DiffRow::Hunk {
|
||||||
|
old_start: hunk.old_start,
|
||||||
|
old_lines: hunk.old_lines,
|
||||||
|
new_start: hunk.new_start,
|
||||||
|
new_lines: hunk.new_lines,
|
||||||
|
});
|
||||||
|
rows.extend((0..hunk.lines.len()).map(|line| DiffRow::Line {
|
||||||
|
hunk: hunk_ix,
|
||||||
|
line,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
rows
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn render_diff_row(hunks: &[DiffHunk], row: DiffRow, cx: &App) -> AnyElement {
|
||||||
|
match row {
|
||||||
|
DiffRow::Hunk {
|
||||||
|
old_start,
|
||||||
|
old_lines,
|
||||||
|
new_start,
|
||||||
|
new_lines,
|
||||||
|
} => div()
|
||||||
|
.px_2()
|
||||||
|
.w_full()
|
||||||
|
.h(px(DIFF_ROW_HEIGHT))
|
||||||
|
.font_family(cx.theme().mono_font)
|
||||||
|
.text_xs()
|
||||||
|
.bg(cx.theme().colors.muted)
|
||||||
|
.border_y_1()
|
||||||
|
.border_color(cx.theme().colors.border)
|
||||||
|
.text_color(cx.theme().colors.muted_foreground)
|
||||||
|
.child(SharedString::from(format!(
|
||||||
|
"@@ -{},{} +{},{} @@",
|
||||||
|
old_start, old_lines, new_start, new_lines
|
||||||
|
)))
|
||||||
|
.into_any_element(),
|
||||||
|
DiffRow::Line { hunk, line } => render_diff_line(&hunks[hunk].lines[line], cx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_diff_line(line: &DiffLine, cx: &App) -> AnyElement {
|
||||||
|
let bg = match line.kind {
|
||||||
|
DiffLineKind::Addition => Some(cx.theme().ramps.success.base.opacity(0.2)),
|
||||||
|
DiffLineKind::Deletion => Some(cx.theme().ramps.destructive.base.opacity(0.2)),
|
||||||
|
DiffLineKind::Context => None,
|
||||||
|
};
|
||||||
|
let gutter = cx.theme().colors.muted_foreground;
|
||||||
|
|
||||||
|
h_flex()
|
||||||
|
.w_full()
|
||||||
|
.h(px(DIFF_ROW_HEIGHT))
|
||||||
|
.font_family(cx.theme().mono_font)
|
||||||
|
.text_xs()
|
||||||
|
.when_some(bg, |this, bg| this.bg(bg))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.w(px(GUTTER_WIDTH))
|
||||||
|
.flex_none()
|
||||||
|
.pr_2()
|
||||||
|
.text_right()
|
||||||
|
.text_color(gutter)
|
||||||
|
.child(line.old.map(|n| n.to_string()).unwrap_or_default()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.w(px(GUTTER_WIDTH))
|
||||||
|
.flex_none()
|
||||||
|
.pr_2()
|
||||||
|
.text_right()
|
||||||
|
.text_color(gutter)
|
||||||
|
.child(line.new.map(|n| n.to_string()).unwrap_or_default()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex_1()
|
||||||
|
.min_w_0()
|
||||||
|
.overflow_hidden()
|
||||||
|
.whitespace_nowrap()
|
||||||
|
.text_color(cx.theme().colors.foreground)
|
||||||
|
.child(line.text.clone()),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn commit_row(
|
||||||
|
ix: usize,
|
||||||
|
commit: &FileCommit,
|
||||||
|
on_click: impl Fn(&mut Window, &mut gpui::App) + 'static,
|
||||||
|
cx: &gpui::App,
|
||||||
|
) -> gpui::AnyElement {
|
||||||
|
h_flex()
|
||||||
|
.id(ix)
|
||||||
|
.px_4()
|
||||||
|
.h(px(COMMIT_ROW_HEIGHT))
|
||||||
|
.w_full()
|
||||||
|
.gap_3()
|
||||||
|
.border_b_1()
|
||||||
|
.border_color(cx.theme().colors.border)
|
||||||
|
.hover(|this| this.bg(cx.theme().colors.muted_hover))
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.flex_1()
|
||||||
|
.min_w_0()
|
||||||
|
.gap_1()
|
||||||
|
.justify_center()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.overflow_hidden()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.font_family(cx.theme().mono_font)
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().colors.muted_foreground)
|
||||||
|
.child(commit.id.clone()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex_1()
|
||||||
|
.min_w_0()
|
||||||
|
.text_sm()
|
||||||
|
.text_ellipsis()
|
||||||
|
.whitespace_nowrap()
|
||||||
|
.child(commit.summary.clone()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().colors.muted_foreground)
|
||||||
|
.child(commit.author.clone())
|
||||||
|
.child(relative_time_secs(commit.time)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.on_click(move |_, window, cx| on_click(window, cx))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn sync_selector(
|
||||||
|
select: &Entity<ComboboxState<SharedString>>,
|
||||||
|
cached: &mut Vec<SharedString>,
|
||||||
|
items: Vec<SharedString>,
|
||||||
|
selected: Option<SharedString>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> bool {
|
||||||
|
let items_changed = *cached != items;
|
||||||
|
|
||||||
|
let selection_changed = selected
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|value| select.read(cx).selected() != Some(value));
|
||||||
|
|
||||||
|
if !items_changed && !selection_changed {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
select.update(cx, |state, cx| {
|
||||||
|
if items_changed {
|
||||||
|
state.set_options(
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.map(|item| (item.clone(), item.clone()))
|
||||||
|
.collect(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(value) = selected
|
||||||
|
&& (items_changed || selection_changed)
|
||||||
|
{
|
||||||
|
state.set_selected(Some(value), window, cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
*cached = items;
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
+10
-77
@@ -3,23 +3,21 @@ use std::path::PathBuf;
|
|||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AppContext as _, Context, Entity, InteractiveElement as _, IntoElement, ParentElement as _,
|
AppContext as _, Context, Entity, IntoElement, ParentElement as _, Render, SharedString,
|
||||||
Render, SharedString, StatefulInteractiveElement as _, Styled as _, Task,
|
Styled as _, Task, UniformListScrollHandle, WeakEntity, Window, div, uniform_list,
|
||||||
UniformListScrollHandle, WeakEntity, Window, div, px, uniform_list,
|
|
||||||
};
|
};
|
||||||
use gpui_native_kit::common::{h_flex, v_flex};
|
use gpui_native_kit::common::v_flex;
|
||||||
use gpui_native_kit::empty_state::EmptyState;
|
|
||||||
use gpui_native_kit::layout::dock::DockState;
|
use gpui_native_kit::layout::dock::DockState;
|
||||||
use gpui_native_kit::scrollbar::Scrollbar;
|
use gpui_native_kit::scrollbar::Scrollbar;
|
||||||
use gpui_native_kit::spinner::Spinner;
|
use gpui_native_kit::spinner::Spinner;
|
||||||
use gpui_native_kit::theme::ActiveTheme as _;
|
use gpui_native_kit::theme::ActiveTheme as _;
|
||||||
use signed_git::{CommitList, FileCommit, Repo};
|
use signed_git::{CommitList, Repo};
|
||||||
use signed_state::RepoStore;
|
use signed_state::RepoStore;
|
||||||
use utils::relative_time_secs;
|
|
||||||
|
|
||||||
use super::commit_diff::{COMMIT_ROW_HEIGHT, CommitDiffView};
|
use super::commit_diff::CommitDiffView;
|
||||||
use super::repo_display_name;
|
use super::helpers::commit_row;
|
||||||
use crate::panels;
|
use super::helpers::repo_display_name;
|
||||||
|
use crate::shared::panels;
|
||||||
|
|
||||||
pub struct RepoHistoryView {
|
pub struct RepoHistoryView {
|
||||||
store: Entity<RepoStore>,
|
store: Entity<RepoStore>,
|
||||||
@@ -144,12 +142,12 @@ impl Render for RepoHistoryView {
|
|||||||
.child(Spinner::new("commits-loading"))
|
.child(Spinner::new("commits-loading"))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
} else {
|
} else {
|
||||||
empty_state("Failed to load commits")
|
crate::shared::widgets::empty_state("Failed to load commits")
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
if list.commits.is_empty() {
|
if list.commits.is_empty() {
|
||||||
return empty_state("No commits found");
|
return crate::shared::widgets::empty_state("No commits found");
|
||||||
}
|
}
|
||||||
|
|
||||||
let commits = list.commits.clone();
|
let commits = list.commits.clone();
|
||||||
@@ -212,68 +210,3 @@ impl Render for RepoHistoryView {
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn empty_state(message: &'static str) -> gpui::AnyElement {
|
|
||||||
v_flex()
|
|
||||||
.size_full()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.child(EmptyState::new(message))
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// One commit row shared by the repository history and pull-request commit list.
|
|
||||||
pub fn commit_row(
|
|
||||||
ix: usize,
|
|
||||||
commit: &FileCommit,
|
|
||||||
on_click: impl Fn(&mut Window, &mut gpui::App) + 'static,
|
|
||||||
cx: &gpui::App,
|
|
||||||
) -> gpui::AnyElement {
|
|
||||||
h_flex()
|
|
||||||
.id(ix)
|
|
||||||
.px_4()
|
|
||||||
.h(px(COMMIT_ROW_HEIGHT))
|
|
||||||
.w_full()
|
|
||||||
.gap_3()
|
|
||||||
.border_b_1()
|
|
||||||
.border_color(cx.theme().colors.border)
|
|
||||||
.hover(|this| this.bg(cx.theme().colors.muted_hover))
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.flex_1()
|
|
||||||
.min_w_0()
|
|
||||||
.gap_1()
|
|
||||||
.justify_center()
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_2()
|
|
||||||
.overflow_hidden()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.font_family(cx.theme().mono_font)
|
|
||||||
.text_xs()
|
|
||||||
.text_color(cx.theme().colors.muted_foreground)
|
|
||||||
.child(commit.id.clone()),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.flex_1()
|
|
||||||
.min_w_0()
|
|
||||||
.text_sm()
|
|
||||||
.text_ellipsis()
|
|
||||||
.whitespace_nowrap()
|
|
||||||
.child(commit.summary.clone()),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_2()
|
|
||||||
.text_xs()
|
|
||||||
.text_color(cx.theme().colors.muted_foreground)
|
|
||||||
.child(commit.author.clone())
|
|
||||||
.child(relative_time_secs(commit.time)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.on_click(move |_, window, cx| on_click(window, cx))
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use assets::CustomIconName;
|
use assets::CustomIconName;
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, AppContext as _, ClipboardItem, Context, Entity, InteractiveElement as _,
|
AnyElement, App, AppContext as _, ClipboardItem, Context, Entity, InteractiveElement as _,
|
||||||
IntoElement, ParentElement as _, PathPromptOptions, Render, SharedString,
|
IntoElement, ParentElement as _, PathPromptOptions, Render, SharedString, Styled as _,
|
||||||
StatefulInteractiveElement as _, Styled as _, Subscription, Task, WeakEntity, Window, div, px,
|
Subscription, Task, WeakEntity, Window, div, px,
|
||||||
};
|
};
|
||||||
use gpui_native_kit::alert::{Alert, AlertSeverity};
|
use gpui_native_kit::alert::{Alert, AlertSeverity};
|
||||||
use gpui_native_kit::avatar::Avatar;
|
use gpui_native_kit::avatar::Avatar;
|
||||||
@@ -25,28 +25,32 @@ use gpui_native_kit::spinner::Spinner;
|
|||||||
use gpui_native_kit::theme::ActiveTheme as _;
|
use gpui_native_kit::theme::ActiveTheme as _;
|
||||||
use gpui_native_kit::tooltip::Tooltip;
|
use gpui_native_kit::tooltip::Tooltip;
|
||||||
use metadata::ProfileDirectory;
|
use metadata::ProfileDirectory;
|
||||||
use nostr::nips::nip19::Nip19Coordinate;
|
use nostr::prelude::{PublicKey, Url};
|
||||||
use nostr::prelude::{PublicKey, RelayUrl, ToBech32, Url};
|
|
||||||
use signed_core::{Announcement, RepoAddr};
|
use signed_core::{Announcement, RepoAddr};
|
||||||
use signed_git::{FileCommit, GitCache, Repo};
|
use signed_git::{FileCommit, GitCache, Repo};
|
||||||
use signed_state::{
|
use signed_state::{
|
||||||
Backend, CheckoutStatus, CheckoutSyncStatus, CheckoutsStore, LocalReposStore, Mirrors,
|
Backend, CheckoutStatus, CheckoutSyncStatus, CheckoutsStore, LocalReposStore, Mirrors,
|
||||||
Nip34Binding, Nip34Kind, PushStepStatus, RepoListStore, RepoStore, SyncStatusDirectory,
|
Nip34Binding, Nip34Kind, PushStepStatus, RepoStore, SyncStatusDirectory,
|
||||||
};
|
};
|
||||||
use utils::middle_truncate;
|
|
||||||
|
|
||||||
pub mod actions;
|
pub mod actions;
|
||||||
pub mod banners;
|
pub mod banners;
|
||||||
pub mod commit_diff;
|
pub mod commit_diff;
|
||||||
|
pub mod dialogs;
|
||||||
pub mod files;
|
pub mod files;
|
||||||
|
pub mod helpers;
|
||||||
pub mod history;
|
pub mod history;
|
||||||
pub mod refs;
|
pub mod refs;
|
||||||
|
|
||||||
use super::tree::{TreeItemSeed, build_tree_items, sorted_worktree_paths};
|
use crate::issues::IssuesView;
|
||||||
use crate::dialogs;
|
use crate::shared::tree::{build_tree_items, sorted_worktree_paths};
|
||||||
use crate::views::issues::IssuesView;
|
use crate::shared::widgets::repo_tab_avatar;
|
||||||
use banners::Banners;
|
use banners::Banners;
|
||||||
use files::RepoFilesView;
|
use files::RepoFilesView;
|
||||||
|
use helpers::{
|
||||||
|
RepoData, ShareTargets, bound_repo_label, clone_group, fork_row, load_repo_data,
|
||||||
|
nostr_clone_url, repo_display_name,
|
||||||
|
};
|
||||||
use history::RepoHistoryView;
|
use history::RepoHistoryView;
|
||||||
use refs::RefSwitcher;
|
use refs::RefSwitcher;
|
||||||
|
|
||||||
@@ -88,18 +92,6 @@ pub struct RepoDetailView {
|
|||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RepoData {
|
|
||||||
tree: Vec<TreeItemSeed>,
|
|
||||||
entries: Vec<PathBuf>,
|
|
||||||
readme_path: Option<PathBuf>,
|
|
||||||
readme: Option<Vec<u8>>,
|
|
||||||
worktree: Option<PathBuf>,
|
|
||||||
branches: Vec<String>,
|
|
||||||
tags: Vec<String>,
|
|
||||||
current_branch: Option<String>,
|
|
||||||
head_commit: Option<FileCommit>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RepoDetailView {
|
impl RepoDetailView {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
dock: WeakEntity<DockState>,
|
dock: WeakEntity<DockState>,
|
||||||
@@ -795,7 +787,7 @@ impl RepoDetailView {
|
|||||||
let deleted = probe.await;
|
let deleted = probe.await;
|
||||||
|
|
||||||
this.update_in(cx, |this, _window, cx| match deleted {
|
this.update_in(cx, |this, _window, cx| match deleted {
|
||||||
Ok(deleted) => dialogs::reset_remote::open(store, deleted, cx),
|
Ok(deleted) => dialogs::reset::open(store, deleted, cx),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
this.error = Some(error.to_string().into());
|
this.error = Some(error.to_string().into());
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -816,7 +808,7 @@ impl RepoDetailView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Open the upstream repository, the `u` tag of this fork's announcement.
|
/// Open the upstream repository, the `u` tag of this fork's announcement.
|
||||||
fn open_upstream(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
pub(super) fn open_upstream(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let Some(addr) = self
|
let Some(addr) = self
|
||||||
.announcement(cx)
|
.announcement(cx)
|
||||||
.and_then(|announcement| announcement.upstream.as_ref())
|
.and_then(|announcement| announcement.upstream.as_ref())
|
||||||
@@ -833,7 +825,7 @@ impl RepoDetailView {
|
|||||||
let Some(local_path) = self.store.read(cx).path.clone() else {
|
let Some(local_path) = self.store.read(cx).path.clone() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
dialogs::init_repo::open(local_path, cx.entity().downgrade(), window, cx);
|
dialogs::init::open(local_path, cx.entity().downgrade(), window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_issues(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn open_issues(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
@@ -845,7 +837,7 @@ impl RepoDetailView {
|
|||||||
let dock_for_build = dock.clone();
|
let dock_for_build = dock.clone();
|
||||||
let store = self.store.clone();
|
let store = self.store.clone();
|
||||||
|
|
||||||
crate::panels::open(
|
crate::shared::panels::open(
|
||||||
&dock,
|
&dock,
|
||||||
id,
|
id,
|
||||||
move |_, cx| cx.new(|cx| IssuesView::new(dock_for_build.clone(), store.clone(), cx)),
|
move |_, cx| cx.new(|cx| IssuesView::new(dock_for_build.clone(), store.clone(), cx)),
|
||||||
@@ -984,7 +976,7 @@ impl RepoDetailView {
|
|||||||
vec![MenuItem::new("New issue").handler({
|
vec![MenuItem::new("New issue").handler({
|
||||||
let store = store_for_issues.clone();
|
let store = store_for_issues.clone();
|
||||||
move |_, cx| {
|
move |_, cx| {
|
||||||
dialogs::new_issue::open(store.clone(), cx);
|
crate::issues::new_issue::open(store.clone(), cx);
|
||||||
}
|
}
|
||||||
})]
|
})]
|
||||||
})
|
})
|
||||||
@@ -1684,262 +1676,3 @@ impl Render for RepoDetailView {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clone_group(label: &str, id: &'static str, command: &SharedString) -> AnyElement {
|
|
||||||
v_flex()
|
|
||||||
.gap_1()
|
|
||||||
.child(div().text_xs().child(SharedString::from(label)))
|
|
||||||
.child(ClipboardButton::new(id, command.clone()))
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn nostr_clone_url(announcement: &Announcement, nip05: Option<&str>) -> SharedString {
|
|
||||||
let owner = announcement.owner;
|
|
||||||
let user = nip05
|
|
||||||
.map(str::to_owned)
|
|
||||||
.unwrap_or_else(|| owner.to_bech32().unwrap_or_else(|_| owner.to_string()));
|
|
||||||
|
|
||||||
let mut url = format!("nostr://{user}");
|
|
||||||
if let Some(hint) = announcement.relays.first().and_then(RelayUrl::domain) {
|
|
||||||
url.push('/');
|
|
||||||
url.push_str(hint);
|
|
||||||
}
|
|
||||||
url.push('/');
|
|
||||||
url.push_str(&announcement.id);
|
|
||||||
|
|
||||||
SharedString::from(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fork_row(announcement: &Announcement, cx: &mut Context<RepoDetailView>) -> Option<AnyElement> {
|
|
||||||
let upstream = announcement.upstream.as_ref()?;
|
|
||||||
|
|
||||||
let (label, clickable) = match &upstream.addr {
|
|
||||||
Some(addr) => {
|
|
||||||
let name = RepoListStore::global(cx)
|
|
||||||
.read(cx)
|
|
||||||
.announcements
|
|
||||||
.iter()
|
|
||||||
.find(|a| a.addr() == *addr)
|
|
||||||
.map(|a| {
|
|
||||||
a.name
|
|
||||||
.as_deref()
|
|
||||||
.map(SharedString::from)
|
|
||||||
.unwrap_or_else(|| SharedString::from(a.id.clone()))
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|| SharedString::from(addr.identifier().to_owned()));
|
|
||||||
(SharedString::from(format!("Forked from {name}")), true)
|
|
||||||
}
|
|
||||||
None => (SharedString::from(upstream.display().as_str()), false),
|
|
||||||
};
|
|
||||||
|
|
||||||
let row = h_flex()
|
|
||||||
.gap_1()
|
|
||||||
.min_w_0()
|
|
||||||
.text_sm()
|
|
||||||
.text_color(cx.theme().colors.muted_foreground)
|
|
||||||
.child(Icon::named(CustomIconName::GitBranch).size(px(14.)))
|
|
||||||
.child(div().whitespace_nowrap().text_ellipsis().child(label));
|
|
||||||
|
|
||||||
Some(if clickable {
|
|
||||||
row.id("fork-upstream")
|
|
||||||
.cursor_pointer()
|
|
||||||
.on_click(cx.listener(|this, _, window, cx| this.open_upstream(window, cx)))
|
|
||||||
.into_any_element()
|
|
||||||
} else {
|
|
||||||
row.into_any_element()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct ShareTargets {
|
|
||||||
/// NIP-19 `naddr1...` of the announcement, with its announced relays.
|
|
||||||
naddr: String,
|
|
||||||
/// Hex ID of the announcement event itself.
|
|
||||||
event_id: String,
|
|
||||||
/// NIP-34 coordinate `30617:<pubkey>:<repo-id>`.
|
|
||||||
coordinate: String,
|
|
||||||
gitworkshop: String,
|
|
||||||
ditto: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ShareTargets {
|
|
||||||
fn from_announcement(announcement: &Announcement) -> Self {
|
|
||||||
let addr = announcement.addr();
|
|
||||||
let coordinate = addr.to_string();
|
|
||||||
let naddr = Nip19Coordinate::new(addr.into(), announcement.relays.iter().cloned())
|
|
||||||
.to_bech32()
|
|
||||||
.unwrap_or_else(|_| coordinate.clone());
|
|
||||||
|
|
||||||
Self {
|
|
||||||
naddr: naddr.clone(),
|
|
||||||
event_id: announcement
|
|
||||||
.event_id
|
|
||||||
.to_bech32()
|
|
||||||
.unwrap_or_else(|_| announcement.event_id.to_string()),
|
|
||||||
coordinate,
|
|
||||||
gitworkshop: format!("https://gitworkshop.dev/{naddr}"),
|
|
||||||
ditto: format!("https://ditto.pub/{naddr}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn menu(&self) -> Vec<MenuItem> {
|
|
||||||
let items = [
|
|
||||||
("GitWorkshop", truncate_naddr_link(&self.gitworkshop, 4)),
|
|
||||||
("Ditto", truncate_naddr_link(&self.ditto, 4)),
|
|
||||||
("Event ID", middle_truncate(&self.event_id, 10, 10)),
|
|
||||||
("Coordinate", middle_truncate(&self.coordinate, 10, 10)),
|
|
||||||
];
|
|
||||||
|
|
||||||
let values = [
|
|
||||||
self.gitworkshop.clone(),
|
|
||||||
self.ditto.clone(),
|
|
||||||
self.event_id.clone(),
|
|
||||||
self.coordinate.clone(),
|
|
||||||
];
|
|
||||||
|
|
||||||
items
|
|
||||||
.into_iter()
|
|
||||||
.zip(values)
|
|
||||||
.map(|((label, detail), value)| {
|
|
||||||
MenuItem::new(SharedString::from(format!("{label} {detail}"))).handler(
|
|
||||||
move |_, cx| {
|
|
||||||
cx.write_to_clipboard(ClipboardItem::new_string(value.clone()));
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn truncate_naddr_link(url: &str, tail: usize) -> String {
|
|
||||||
let Some(end) = url.find("naddr1").map(|i| i + "naddr1".len()) else {
|
|
||||||
return url.to_string();
|
|
||||||
};
|
|
||||||
if url.len() - end <= tail + 3 {
|
|
||||||
return url.to_string();
|
|
||||||
}
|
|
||||||
format!("{}...{}", &url[..end], &url[url.len() - tail..])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_repo_data(repo: &Repo) -> Result<RepoData, Error> {
|
|
||||||
let snapshot = repo.snapshot()?;
|
|
||||||
let worktree = repo.workdir().map(Path::to_path_buf);
|
|
||||||
let tree = build_tree_items(&snapshot.entries);
|
|
||||||
|
|
||||||
Ok(RepoData {
|
|
||||||
tree,
|
|
||||||
entries: snapshot.entries,
|
|
||||||
readme_path: snapshot.readme_path,
|
|
||||||
readme: snapshot.readme,
|
|
||||||
worktree,
|
|
||||||
branches: snapshot.branches,
|
|
||||||
tags: snapshot.tags,
|
|
||||||
current_branch: snapshot.current_branch,
|
|
||||||
head_commit: snapshot.head_commit,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The announcement's name or ID for announced repositories, the directory name for local ones.
|
|
||||||
pub fn repo_display_name(store: &RepoStore) -> SharedString {
|
|
||||||
if store.addr().is_none() {
|
|
||||||
return store
|
|
||||||
.path
|
|
||||||
.as_ref()
|
|
||||||
.map(|path| {
|
|
||||||
SharedString::from(
|
|
||||||
path.file_name()
|
|
||||||
.map(|name| name.to_string_lossy().into_owned())
|
|
||||||
.unwrap_or_else(|| path.display().to_string()),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.unwrap_or_default();
|
|
||||||
}
|
|
||||||
|
|
||||||
store
|
|
||||||
.announcement
|
|
||||||
.as_ref()
|
|
||||||
.map(|announcement| {
|
|
||||||
announcement
|
|
||||||
.name
|
|
||||||
.as_deref()
|
|
||||||
.map(SharedString::from)
|
|
||||||
.unwrap_or_else(|| SharedString::from(announcement.id.clone()))
|
|
||||||
})
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The owner and identifier a local repository is already bound to.
|
|
||||||
fn bound_repo_label(
|
|
||||||
binding: &Nip34Binding,
|
|
||||||
directory: &Entity<ProfileDirectory>,
|
|
||||||
cx: &App,
|
|
||||||
) -> AnyElement {
|
|
||||||
let profile = binding.owner.map(|pk| directory.read(cx).profile(&pk, cx));
|
|
||||||
|
|
||||||
h_flex()
|
|
||||||
.flex_shrink_0()
|
|
||||||
.gap_2()
|
|
||||||
.text_sm()
|
|
||||||
.child("Initialized by")
|
|
||||||
.text_color(cx.theme().colors.muted_foreground)
|
|
||||||
.when_some(profile, |this, profile| {
|
|
||||||
let name = profile.name();
|
|
||||||
this.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_1()
|
|
||||||
.text_color(cx.theme().colors.foreground)
|
|
||||||
.child(
|
|
||||||
Avatar::new(profile.public_key())
|
|
||||||
.name(name.clone())
|
|
||||||
.picture(profile.picture())
|
|
||||||
.small(),
|
|
||||||
)
|
|
||||||
.child(name),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.when_some(binding.identifier.as_deref(), |this, ident| {
|
|
||||||
this.child(
|
|
||||||
div()
|
|
||||||
.text_xs()
|
|
||||||
.font_weight(gpui::FontWeight::SEMIBOLD)
|
|
||||||
.child(SharedString::from(format!("/{ident}"))),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The profile avatar shown on the repository tab.
|
|
||||||
pub(crate) fn repo_tab_avatar(
|
|
||||||
store: &RepoStore,
|
|
||||||
directory: &Entity<ProfileDirectory>,
|
|
||||||
cx: &App,
|
|
||||||
) -> AnyElement {
|
|
||||||
let seed = store
|
|
||||||
.announcement
|
|
||||||
.as_ref()
|
|
||||||
.map(|announcement| format!("{}:{}", announcement.owner, announcement.id))
|
|
||||||
.or_else(|| store.addr().map(|addr| addr.to_string()))
|
|
||||||
.or_else(|| {
|
|
||||||
store
|
|
||||||
.path
|
|
||||||
.as_ref()
|
|
||||||
.map(|path| path.to_string_lossy().into_owned())
|
|
||||||
})
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let owner = store
|
|
||||||
.announcement
|
|
||||||
.as_ref()
|
|
||||||
.map(|announcement| directory.read(cx).profile(&announcement.owner, cx));
|
|
||||||
|
|
||||||
let (name, picture) = owner
|
|
||||||
.map(|profile| (Some(profile.name()), profile.picture()))
|
|
||||||
.unwrap_or((None, None));
|
|
||||||
|
|
||||||
let avatar = match name {
|
|
||||||
Some(name) => Avatar::new(seed).name(name),
|
|
||||||
None => Avatar::new(seed),
|
|
||||||
};
|
|
||||||
|
|
||||||
avatar.picture(picture).small().into_any_element()
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,7 @@ use gpui::{App, AppContext as _, Entity, SharedString, WeakEntity, Window};
|
|||||||
use gpui_native_kit::combobox::{Combobox, ComboboxState};
|
use gpui_native_kit::combobox::{Combobox, ComboboxState};
|
||||||
|
|
||||||
use super::RepoDetailView;
|
use super::RepoDetailView;
|
||||||
|
use super::helpers::sync_selector;
|
||||||
|
|
||||||
pub(super) struct RefSwitcher {
|
pub(super) struct RefSwitcher {
|
||||||
pub(super) branch_select: Entity<ComboboxState<SharedString>>,
|
pub(super) branch_select: Entity<ComboboxState<SharedString>>,
|
||||||
@@ -98,42 +99,3 @@ impl RefSwitcher {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sync_selector(
|
|
||||||
select: &Entity<ComboboxState<SharedString>>,
|
|
||||||
cached: &mut Vec<SharedString>,
|
|
||||||
items: Vec<SharedString>,
|
|
||||||
selected: Option<SharedString>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> bool {
|
|
||||||
let items_changed = *cached != items;
|
|
||||||
|
|
||||||
let selection_changed = selected
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|value| select.read(cx).selected() != Some(value));
|
|
||||||
|
|
||||||
if !items_changed && !selection_changed {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
select.update(cx, |state, cx| {
|
|
||||||
if items_changed {
|
|
||||||
state.set_options(
|
|
||||||
items
|
|
||||||
.iter()
|
|
||||||
.map(|item| (item.clone(), item.clone()))
|
|
||||||
.collect(),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if let Some(value) = selected
|
|
||||||
&& (items_changed || selection_changed)
|
|
||||||
{
|
|
||||||
state.set_selected(Some(value), window, cx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
*cached = items;
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,3 @@
|
|||||||
pub mod about;
|
|
||||||
pub mod create_repo;
|
|
||||||
pub mod exit;
|
|
||||||
pub mod grasp_servers;
|
|
||||||
pub mod import;
|
|
||||||
pub mod init_repo;
|
|
||||||
pub mod new_issue;
|
|
||||||
pub mod onboarding;
|
|
||||||
pub mod passphrase;
|
|
||||||
pub mod progress;
|
|
||||||
pub mod reset_remote;
|
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyView, App, Context, Entity, Global, IntoElement, ParentElement as _, Render, Styled as _,
|
AnyView, App, Context, Entity, Global, IntoElement, ParentElement as _, Render, Styled as _,
|
||||||
@@ -61,16 +49,6 @@ pub fn close_global(cx: &mut App) {
|
|||||||
host.update(cx, |host, cx| host.close(cx));
|
host.update(cx, |host, cx| host.close(cx));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opens the onboarding form.
|
|
||||||
pub fn open_onboarding(cx: &mut App) {
|
|
||||||
open_global(onboarding::OnboardingDialog::new(cx), cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Opens the identity import form.
|
|
||||||
pub fn open_import(cx: &mut App) {
|
|
||||||
open_global(import::ImportDialog::new(cx), cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for DialogHost {
|
impl Render for DialogHost {
|
||||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
div()
|
div()
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod dialogs;
|
||||||
|
pub mod panels;
|
||||||
|
pub mod progress;
|
||||||
|
pub mod tree;
|
||||||
|
pub mod widgets;
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
use assets::CustomIconName;
|
||||||
|
use gpui::{
|
||||||
|
AnyElement, App, Entity, IntoElement, ParentElement as _, SharedString, Styled as _, div,
|
||||||
|
};
|
||||||
|
use gpui_native_kit::avatar::Avatar;
|
||||||
|
use gpui_native_kit::badge::{Badge, BadgeVariant};
|
||||||
|
use gpui_native_kit::common::Sizable as _;
|
||||||
|
use gpui_native_kit::empty_state::EmptyState;
|
||||||
|
use gpui_native_kit::icons::Icon;
|
||||||
|
use gpui_native_kit::theme::ActiveTheme as _;
|
||||||
|
use gpui_native_kit::tooltip::Tooltip;
|
||||||
|
use metadata::ProfileDirectory;
|
||||||
|
use signed_core::RepoStatus;
|
||||||
|
use signed_state::RepoStore;
|
||||||
|
|
||||||
|
/// The status pill shown on issue and pull-request rows.
|
||||||
|
pub fn status_badge(status: RepoStatus) -> Badge {
|
||||||
|
let (label, variant, icon) = match status {
|
||||||
|
RepoStatus::Open => ("Open", BadgeVariant::Neutral, CustomIconName::GitIssueOpen),
|
||||||
|
RepoStatus::Closed => (
|
||||||
|
"Closed",
|
||||||
|
BadgeVariant::Danger,
|
||||||
|
CustomIconName::GitIssueClosed,
|
||||||
|
),
|
||||||
|
RepoStatus::Draft => (
|
||||||
|
"Draft",
|
||||||
|
BadgeVariant::Warning,
|
||||||
|
CustomIconName::GitIssueOngoing,
|
||||||
|
),
|
||||||
|
RepoStatus::Applied => (
|
||||||
|
"Applied",
|
||||||
|
BadgeVariant::Success,
|
||||||
|
CustomIconName::GitIssueDone,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
Badge::new(format!("status-{label}"), label)
|
||||||
|
.variant(variant)
|
||||||
|
.icon(Icon::named(icon))
|
||||||
|
.tooltip(Tooltip::text(label))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A centered empty-state message filling the available space.
|
||||||
|
pub fn empty_state(message: impl Into<SharedString>) -> AnyElement {
|
||||||
|
let message = message.into();
|
||||||
|
gpui_native_kit::common::v_flex()
|
||||||
|
.size_full()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.child(EmptyState::new(message))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The profile avatar shown on a repository's panel tab.
|
||||||
|
pub fn repo_tab_avatar(
|
||||||
|
store: &RepoStore,
|
||||||
|
directory: &Entity<ProfileDirectory>,
|
||||||
|
cx: &App,
|
||||||
|
) -> AnyElement {
|
||||||
|
let seed = store
|
||||||
|
.announcement
|
||||||
|
.as_ref()
|
||||||
|
.map(|announcement| format!("{}:{}", announcement.owner, announcement.id))
|
||||||
|
.or_else(|| store.addr().map(|addr| addr.to_string()))
|
||||||
|
.or_else(|| {
|
||||||
|
store
|
||||||
|
.path
|
||||||
|
.as_ref()
|
||||||
|
.map(|path| path.to_string_lossy().into_owned())
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let owner = store
|
||||||
|
.announcement
|
||||||
|
.as_ref()
|
||||||
|
.map(|announcement| directory.read(cx).profile(&announcement.owner, cx));
|
||||||
|
|
||||||
|
let (name, picture) = owner
|
||||||
|
.map(|profile| (Some(profile.name()), profile.picture()))
|
||||||
|
.unwrap_or((None, None));
|
||||||
|
|
||||||
|
let avatar = match name {
|
||||||
|
Some(name) => Avatar::new(seed).name(name),
|
||||||
|
None => Avatar::new(seed),
|
||||||
|
};
|
||||||
|
|
||||||
|
avatar.picture(picture).small().into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A one-line muted label used by section headers.
|
||||||
|
pub fn section_label(text: impl Into<SharedString>, cx: &App) -> AnyElement {
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.font_weight(gpui::FontWeight::SEMIBOLD)
|
||||||
|
.text_color(cx.theme().colors.muted_foreground)
|
||||||
|
.child(text.into())
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
use gpui::{AnyElement, App, IntoElement, SharedString};
|
||||||
|
use gpui_native_kit::avatar::Avatar;
|
||||||
|
use gpui_native_kit::common::Sizable as _;
|
||||||
|
use metadata::ProfileDirectory;
|
||||||
|
use signed_state::{Nip34Binding, ResolvedLocalRepo};
|
||||||
|
|
||||||
|
pub(super) fn pick_banner() -> SharedString {
|
||||||
|
let num = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|duration| duration.subsec_nanos())
|
||||||
|
.unwrap_or(1)
|
||||||
|
% 3
|
||||||
|
+ 1;
|
||||||
|
format!("backgrounds/banner{num}.jpg").into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The platform that bound a repository, `"nak"` or `"ngit"`.
|
||||||
|
pub(super) fn local_platform(binding: &Nip34Binding) -> Option<&'static str> {
|
||||||
|
let signals = &binding.signals;
|
||||||
|
|
||||||
|
if signals.nip34_json || signals.nip34_grasp_remote || signals.nip34_state_refs {
|
||||||
|
Some("nak")
|
||||||
|
} else if signals.nostr_repo_config {
|
||||||
|
Some("ngit")
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The bound owner's avatar, falling back to the path's pixel avatar when unowned.
|
||||||
|
pub(super) fn local_avatar(
|
||||||
|
entry: &ResolvedLocalRepo,
|
||||||
|
directory: &gpui::Entity<ProfileDirectory>,
|
||||||
|
cx: &App,
|
||||||
|
) -> AnyElement {
|
||||||
|
let Some(owner) = entry.nip34.as_ref().and_then(|binding| binding.owner) else {
|
||||||
|
return Avatar::new(entry.path.to_string_lossy())
|
||||||
|
.small()
|
||||||
|
.into_any_element();
|
||||||
|
};
|
||||||
|
|
||||||
|
let profile = directory.read(cx).profile(&owner, cx);
|
||||||
|
|
||||||
|
Avatar::new(owner)
|
||||||
|
.name(profile.name())
|
||||||
|
.picture(profile.picture())
|
||||||
|
.small()
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
|
||||||
|
|
||||||
use assets::CustomIconName;
|
use assets::CustomIconName;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
@@ -22,12 +21,16 @@ use metadata::{Profile, ProfileDirectory};
|
|||||||
use nostr::prelude::PublicKey;
|
use nostr::prelude::PublicKey;
|
||||||
use signed_core::{Announcement, RepoAddr};
|
use signed_core::{Announcement, RepoAddr};
|
||||||
use signed_state::{
|
use signed_state::{
|
||||||
Backend, BackendEvent, CheckoutsStore, LocalReposStore, Nip34Binding, Nip34Kind, RepoListStore,
|
Backend, BackendEvent, CheckoutsStore, LocalReposStore, Nip34Kind, RepoListStore,
|
||||||
ResolvedLocalRepo, SyncStatusDirectory,
|
ResolvedLocalRepo, SyncStatusDirectory,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::dialogs;
|
mod helpers;
|
||||||
use crate::views::explore::ExploreView;
|
|
||||||
|
use crate::explore::ExploreView;
|
||||||
|
use crate::identity;
|
||||||
|
use crate::repo::dialogs;
|
||||||
|
use helpers::{local_avatar, local_platform, pick_banner};
|
||||||
|
|
||||||
pub struct SidebarView {
|
pub struct SidebarView {
|
||||||
weak: WeakEntity<Self>,
|
weak: WeakEntity<Self>,
|
||||||
@@ -351,7 +354,7 @@ impl SidebarView {
|
|||||||
if row < self.announcements.len() {
|
if row < self.announcements.len() {
|
||||||
let announcement = self.announcements[row].clone();
|
let announcement = self.announcements[row].clone();
|
||||||
let addr = announcement.addr();
|
let addr = announcement.addr();
|
||||||
crate::views::repo::actions::open_repo_panel(
|
crate::repo::actions::open_repo_panel(
|
||||||
&self.dock,
|
&self.dock,
|
||||||
&addr,
|
&addr,
|
||||||
Some(&announcement),
|
Some(&announcement),
|
||||||
@@ -384,12 +387,12 @@ impl SidebarView {
|
|||||||
let id = SharedString::from(format!("repo:{}", announcement.addr()));
|
let id = SharedString::from(format!("repo:{}", announcement.addr()));
|
||||||
let dock = self.dock.clone();
|
let dock = self.dock.clone();
|
||||||
let dock_for_build = dock.clone();
|
let dock_for_build = dock.clone();
|
||||||
crate::panels::open(
|
crate::shared::panels::open(
|
||||||
&dock,
|
&dock,
|
||||||
id,
|
id,
|
||||||
move |window, cx| {
|
move |window, cx| {
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
crate::views::repo::RepoDetailView::new_local_announced(
|
crate::repo::RepoDetailView::new_local_announced(
|
||||||
dock_for_build.clone(),
|
dock_for_build.clone(),
|
||||||
announcement.clone(),
|
announcement.clone(),
|
||||||
path.clone(),
|
path.clone(),
|
||||||
@@ -407,12 +410,12 @@ impl SidebarView {
|
|||||||
let id = SharedString::from(format!("repo-local:{}", path.display()));
|
let id = SharedString::from(format!("repo-local:{}", path.display()));
|
||||||
let dock = self.dock.clone();
|
let dock = self.dock.clone();
|
||||||
let dock_for_build = dock.clone();
|
let dock_for_build = dock.clone();
|
||||||
crate::panels::open(
|
crate::shared::panels::open(
|
||||||
&dock,
|
&dock,
|
||||||
id,
|
id,
|
||||||
move |window, cx| {
|
move |window, cx| {
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
crate::views::repo::RepoDetailView::new_local(
|
crate::repo::RepoDetailView::new_local(
|
||||||
dock_for_build.clone(),
|
dock_for_build.clone(),
|
||||||
path.clone(),
|
path.clone(),
|
||||||
nip34.clone(),
|
nip34.clone(),
|
||||||
@@ -429,7 +432,7 @@ impl SidebarView {
|
|||||||
fn open_explore(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn open_explore(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let dock = self.dock.clone();
|
let dock = self.dock.clone();
|
||||||
let dock_for_build = dock.clone();
|
let dock_for_build = dock.clone();
|
||||||
crate::panels::open(
|
crate::shared::panels::open(
|
||||||
&dock,
|
&dock,
|
||||||
"explore",
|
"explore",
|
||||||
move |_window, cx| cx.new(|cx| ExploreView::new(dock_for_build.clone(), cx)),
|
move |_window, cx| cx.new(|cx| ExploreView::new(dock_for_build.clone(), cx)),
|
||||||
@@ -517,7 +520,7 @@ impl SidebarView {
|
|||||||
MenuItem::separator(),
|
MenuItem::separator(),
|
||||||
MenuItem::new("Exit")
|
MenuItem::new("Exit")
|
||||||
.destructive(true)
|
.destructive(true)
|
||||||
.handler(|_, cx| dialogs::exit::open(cx)),
|
.handler(|_, cx| identity::helpers::open_exit(cx)),
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -544,7 +547,7 @@ impl SidebarView {
|
|||||||
.on_click({
|
.on_click({
|
||||||
let dock = dock.clone();
|
let dock = dock.clone();
|
||||||
move |_, window, cx| {
|
move |_, window, cx| {
|
||||||
dialogs::create_repo::open(dock.clone(), window, cx);
|
dialogs::create::open(dock.clone(), window, cx);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -581,7 +584,7 @@ impl SidebarView {
|
|||||||
move |_, window, cx| {
|
move |_, window, cx| {
|
||||||
if let Err(error) = weak.update(cx, |this, cx| {
|
if let Err(error) = weak.update(cx, |this, cx| {
|
||||||
this.open_panel("welcome", window, cx);
|
this.open_panel("welcome", window, cx);
|
||||||
crate::dialogs::open_onboarding(cx);
|
identity::helpers::open_onboarding(cx);
|
||||||
}) {
|
}) {
|
||||||
log::warn!("failed to open onboarding: {error}");
|
log::warn!("failed to open onboarding: {error}");
|
||||||
}
|
}
|
||||||
@@ -593,7 +596,7 @@ impl SidebarView {
|
|||||||
.variant(ButtonVariant::Secondary)
|
.variant(ButtonVariant::Secondary)
|
||||||
.on_click(move |_, _, cx| {
|
.on_click(move |_, _, cx| {
|
||||||
if let Err(error) = import_weak
|
if let Err(error) = import_weak
|
||||||
.update(cx, |_, cx| crate::dialogs::open_import(cx))
|
.update(cx, |_, cx| identity::helpers::open_import(cx))
|
||||||
{
|
{
|
||||||
log::warn!("failed to open import: {error}");
|
log::warn!("failed to open import: {error}");
|
||||||
}
|
}
|
||||||
@@ -603,47 +606,3 @@ impl SidebarView {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pick_banner() -> SharedString {
|
|
||||||
let num = SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.map(|duration| duration.subsec_nanos())
|
|
||||||
.unwrap_or(1)
|
|
||||||
% 3
|
|
||||||
+ 1;
|
|
||||||
format!("backgrounds/banner{num}.jpg").into()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The platform that bound a repository, `"nak"` or `"ngit"`.
|
|
||||||
fn local_platform(binding: &Nip34Binding) -> Option<&'static str> {
|
|
||||||
let signals = &binding.signals;
|
|
||||||
|
|
||||||
if signals.nip34_json || signals.nip34_grasp_remote || signals.nip34_state_refs {
|
|
||||||
Some("nak")
|
|
||||||
} else if signals.nostr_repo_config {
|
|
||||||
Some("ngit")
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The bound owner's avatar, falling back to the path's pixel avatar when unowned.
|
|
||||||
fn local_avatar(
|
|
||||||
entry: &ResolvedLocalRepo,
|
|
||||||
directory: &gpui::Entity<ProfileDirectory>,
|
|
||||||
cx: &App,
|
|
||||||
) -> AnyElement {
|
|
||||||
let Some(owner) = entry.nip34.as_ref().and_then(|binding| binding.owner) else {
|
|
||||||
return Avatar::new(entry.path.to_string_lossy())
|
|
||||||
.small()
|
|
||||||
.into_any_element();
|
|
||||||
};
|
|
||||||
|
|
||||||
let profile = directory.read(cx).profile(&owner, cx);
|
|
||||||
|
|
||||||
Avatar::new(owner)
|
|
||||||
.name(profile.name())
|
|
||||||
.picture(profile.picture())
|
|
||||||
.small()
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
pub mod discussion;
|
|
||||||
pub mod explore;
|
|
||||||
pub mod issues;
|
|
||||||
pub mod placeholder;
|
|
||||||
pub mod repo;
|
|
||||||
pub mod sidebar;
|
|
||||||
pub mod status_list;
|
|
||||||
pub mod tree;
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
use nostr::prelude::Event;
|
|
||||||
use signed_core::RepoStatus;
|
|
||||||
|
|
||||||
/// Root events counted by their resolved status.
|
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
||||||
pub struct StatusCounts {
|
|
||||||
pub total: usize,
|
|
||||||
pub open: usize,
|
|
||||||
pub closed: usize,
|
|
||||||
pub draft: usize,
|
|
||||||
pub applied: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StatusCounts {
|
|
||||||
fn record(&mut self, status: RepoStatus) {
|
|
||||||
self.total += 1;
|
|
||||||
match status {
|
|
||||||
RepoStatus::Open => self.open += 1,
|
|
||||||
RepoStatus::Closed => self.closed += 1,
|
|
||||||
RepoStatus::Draft => self.draft += 1,
|
|
||||||
RepoStatus::Applied => self.applied += 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Indices of `roots` whose status `keep` accepts, counting every root's status.
|
|
||||||
pub fn filter_by_status<'a>(
|
|
||||||
roots: impl IntoIterator<Item = &'a Event>,
|
|
||||||
status_of: impl Fn(&Event) -> RepoStatus,
|
|
||||||
keep: impl Fn(RepoStatus) -> bool,
|
|
||||||
) -> (Vec<usize>, StatusCounts) {
|
|
||||||
let mut counts = StatusCounts::default();
|
|
||||||
|
|
||||||
let visible = roots
|
|
||||||
.into_iter()
|
|
||||||
.enumerate()
|
|
||||||
.filter_map(|(index, root)| {
|
|
||||||
let status = status_of(root);
|
|
||||||
counts.record(status);
|
|
||||||
keep(status).then_some(index)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
(visible, counts)
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user