add repo info dialog

This commit is contained in:
2026-08-28 10:25:09 +07:00
parent 5342252050
commit ae7be10dd3
7 changed files with 273 additions and 9 deletions
Generated
+1
View File
@@ -10869,6 +10869,7 @@ version = "1.0.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"assets", "assets",
"chrono",
"dock", "dock",
"futures", "futures",
"gix", "gix",
+1
View File
@@ -20,5 +20,6 @@ gix.workspace = true
nostr.workspace = true nostr.workspace = true
anyhow.workspace = true anyhow.workspace = true
chrono.workspace = true
futures.workspace = true futures.workspace = true
log.workspace = true log.workspace = true
+11 -1
View File
@@ -1,5 +1,6 @@
use gpui::prelude::*; use gpui::prelude::*;
use gpui::{App, Pixels, Window, div, px}; use gpui::{App, Pixels, StyleRefinement, Window, div, px};
use gpui_base::StyledExt;
use gpui_component::{ActiveTheme, Colorize}; use gpui_component::{ActiveTheme, Colorize};
/// Number of rows and columns in the pixel grid. /// Number of rows and columns in the pixel grid.
@@ -19,6 +20,7 @@ const MIN_FILLED: usize = 5;
pub(crate) struct PixelAvatar { pub(crate) struct PixelAvatar {
seed: u64, seed: u64,
size: Pixels, size: Pixels,
style: StyleRefinement,
} }
impl PixelAvatar { impl PixelAvatar {
@@ -28,10 +30,17 @@ impl PixelAvatar {
Self { Self {
seed: fnv1a(seed.as_ref().as_bytes()), seed: fnv1a(seed.as_ref().as_bytes()),
size: px(16.), size: px(16.),
style: StyleRefinement::default(),
} }
} }
} }
impl Styled for PixelAvatar {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl RenderOnce for PixelAvatar { impl RenderOnce for PixelAvatar {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme(); let theme = cx.theme();
@@ -64,6 +73,7 @@ impl RenderOnce for PixelAvatar {
} }
div() div()
.refine_style(&self.style)
.grid() .grid()
.grid_cols(GRID_SIZE as u16) .grid_cols(GRID_SIZE as u16)
.grid_rows(GRID_SIZE as u16) .grid_rows(GRID_SIZE as u16)
@@ -0,0 +1,232 @@
use gpui::prelude::*;
use gpui::{AnyElement, App, SharedString, Window, div, px};
use gpui_component::avatar::Avatar;
use gpui_component::clipboard::Clipboard;
use gpui_component::{ActiveTheme, Sizable, StyledExt, WindowExt, h_flex, v_flex};
use nostr::prelude::PublicKey;
use signed_core::Announcement;
use signed_state::ProfileStore;
use super::helpers::middle_truncate;
/// Open the "About" dialog: every field of the repository's announcement
/// event (NIP-34, kind 30617), as parsed into [`Announcement`].
pub(super) fn open_about_dialog(announcement: Announcement, window: &mut Window, cx: &mut App) {
window.open_dialog(cx, move |dialog, _window, cx| {
let announcement = announcement.clone();
dialog
.w(px(500.))
.h(px(600.))
.keyboard(true)
.close_button(true)
.title("About")
.child(announcement_rows(&announcement, cx))
});
}
/// The announcement's fields as labeled rows; hex identifiers carry a copy
/// button, multi-value tags one line per value.
fn announcement_rows(announcement: &Announcement, cx: &App) -> AnyElement {
let mut rows: Vec<AnyElement> = Vec::new();
rows.push(row(
"Name",
text(
announcement
.name
.clone()
.unwrap_or_else(|| SharedString::from("")),
),
cx,
));
rows.push(row(
"Description",
text(
announcement
.description
.clone()
.unwrap_or_else(|| SharedString::from("")),
),
cx,
));
if !announcement.web.is_empty() {
rows.push(row(
"Web",
list(
"about-web",
announcement.web.iter().map(|url| url.to_string()),
cx,
),
cx,
));
}
if let Some(euc) = &announcement.euc {
rows.push(row(
"Earliest Commit",
copy_value("about-euc", euc.clone(), cx),
cx,
));
}
if let Some(upstream) = &announcement.upstream {
rows.push(row(
"Upstream",
text(SharedString::from(upstream.clone())),
cx,
));
}
if !announcement.hashtags.is_empty() {
rows.push(row(
"Hashtags",
text(SharedString::from(announcement.hashtags.join(", "))),
cx,
));
}
if !announcement.clone.is_empty() {
rows.push(row(
"Clone URLs",
list(
"about-clone",
announcement.clone.iter().map(|url| url.to_string()),
cx,
),
cx,
));
}
if !announcement.relays.is_empty() {
rows.push(row(
"Grasp Relays",
list(
"about-relays",
announcement.relays.iter().map(|url| url.to_string()),
cx,
),
cx,
));
}
if !announcement.maintainers.is_empty() {
rows.push(row(
"Maintainers",
maintainers(&announcement.maintainers, cx),
cx,
));
}
v_flex().gap_3().w_full().children(rows).into_any_element()
}
/// One info row: a small muted label above the value.
fn row(label: &'static str, value: AnyElement, cx: &App) -> AnyElement {
v_flex()
.gap_1()
.min_w_0()
.child(
div()
.text_xs()
.font_semibold()
.text_color(cx.theme().muted_foreground)
.child(label),
)
.child(value)
.into_any_element()
}
/// Plain text value, wrapping within the dialog.
fn text(value: SharedString) -> AnyElement {
div()
.text_sm()
.w_full()
.min_w_0()
.child(value)
.into_any_element()
}
/// A mono-spaced value with a copy button, for hex identifiers.
fn copy_value(id: &'static str, value: String, cx: &App) -> AnyElement {
h_flex()
.gap_2()
.items_center()
.min_w_0()
.child(
div()
.font_family(cx.theme().mono_font_family.clone())
.text_xs()
.min_w_0()
.child(SharedString::from(value.clone())),
)
.child(Clipboard::new(id).tooltip("Copy").value(value))
.into_any_element()
}
/// One row per maintainer: avatar and display name (falling back to a
/// shortened npub), with a copy button for the full pubkey.
fn maintainers(maintainers: &[PublicKey], cx: &App) -> AnyElement {
let profile_store = ProfileStore::global(cx);
v_flex()
.gap_2()
.min_w_0()
.children(maintainers.iter().map(|pubkey| {
let profile = profile_store.read(cx).get(pubkey);
let name = profile.name();
let picture = profile.picture();
h_flex()
.gap_2()
.items_center()
.min_w_0()
.child(
Avatar::new()
.name(name.clone())
.when_some(picture, |this, url| this.src(url))
.rounded(cx.theme().radius)
.small(),
)
.child(
div()
.flex_1()
.min_w_0()
.overflow_hidden()
.whitespace_nowrap()
.text_ellipsis()
.text_sm()
.child(name),
)
}))
.into_any_element()
}
/// One row per item of a multi-value tag: the value is truncated to a single
/// line, with a copy button that copies the full value.
fn list(id: &'static str, items: impl IntoIterator<Item = String>, cx: &App) -> AnyElement {
v_flex()
.gap_2()
.min_w_0()
.children(items.into_iter().enumerate().map(|(ix, item)| {
h_flex()
.gap_2()
.items_center()
.min_w_0()
.child(
h_flex()
.h_5()
.px_1()
.flex_1()
.min_w_0()
.overflow_hidden()
.whitespace_nowrap()
.text_ellipsis()
.text_sm()
.bg(cx.theme().muted)
.rounded(cx.theme().radius)
.child(SharedString::from(middle_truncate(&item, 28, 16))),
)
.child(
Clipboard::new(format!("{id}-{ix}"))
.tooltip("Copy")
.value(item),
)
.into_any_element()
}))
.into_any_element()
}
@@ -528,7 +528,7 @@ pub(super) fn share_menu_row(
/// `[head chars]...[tail chars]` middle truncation; the value is left alone /// `[head chars]...[tail chars]` middle truncation; the value is left alone
/// when it is too short for the ellipsis to save space. /// when it is too short for the ellipsis to save space.
fn middle_truncate(value: &str, head: usize, tail: usize) -> String { pub(super) fn middle_truncate(value: &str, head: usize, tail: usize) -> String {
let len = value.chars().count(); let len = value.chars().count();
if len <= head + tail + 3 { if len <= head + tail + 3 {
return value.to_string(); return value.to_string();
+26 -2
View File
@@ -29,7 +29,9 @@ use signed_git::{CommitList, FileCommit};
use signed_state::{GitStore, ProfileStore, RepoStore}; use signed_state::{GitStore, ProfileStore, RepoStore};
use crate::image_cache::{MAX_IMAGES, image_cache}; use crate::image_cache::{MAX_IMAGES, image_cache};
use crate::pixel_avatar::PixelAvatar;
mod about;
mod browser; mod browser;
mod commits; mod commits;
mod diff; mod diff;
@@ -39,6 +41,7 @@ mod issues;
mod pull_request_detail; mod pull_request_detail;
mod pull_requests; mod pull_requests;
use about::open_about_dialog;
use browser::{ use browser::{
CodeView, FileContent, MAX_PREVIEW_BYTES, MAX_PREVIEW_CACHE_BYTES, MAX_PREVIEWED_FILES, CodeView, FileContent, MAX_PREVIEW_BYTES, MAX_PREVIEW_CACHE_BYTES, MAX_PREVIEWED_FILES,
MarkdownView, MarkdownView,
@@ -1046,6 +1049,7 @@ impl RepoDetailView {
let name = self.display_name(cx); let name = self.display_name(cx);
let description = announcement.description(); let description = announcement.description();
let avatar = PixelAvatar::new(format!("{}:{}", announcement.owner, announcement.id));
let share = ShareTargets::from_announcement(announcement); let share = ShareTargets::from_announcement(announcement);
let commits_count = self.all_commits.as_ref().map(|list| list.total); let commits_count = self.all_commits.as_ref().map(|list| list.total);
@@ -1079,14 +1083,21 @@ impl RepoDetailView {
.flex_1() .flex_1()
.min_w_0() .min_w_0()
.gap_1() .gap_1()
.child(div().font_semibold().child(name)) .child(
h_flex()
.gap_2()
.min_h_8()
.font_semibold()
.child(avatar.size_6())
.child(name),
)
.child( .child(
div() div()
.min_w_0() .min_w_0()
.text_sm() .text_sm()
.text_color(cx.theme().muted_foreground) .text_color(cx.theme().muted_foreground)
.line_clamp(2) .line_clamp(2)
.line_height(relative(1.2)) .line_height(relative(1.25))
.text_ellipsis() .text_ellipsis()
.child(description), .child(description),
) )
@@ -1208,6 +1219,19 @@ impl RepoDetailView {
) )
.dropdown_menu(move |menu, _, _| share.menu(menu)), .dropdown_menu(move |menu, _, _| share.menu(menu)),
) )
.child(
Button::new("info")
.icon(IconName::Info)
.tooltip("About")
.secondary()
.on_click(cx.listener(|this, _event, window, cx| {
open_about_dialog(
this.announcement(cx).clone(),
window,
cx,
);
})),
)
.child( .child(
Button::new("clone") Button::new("clone")
.icon(CustomIconName::GitClone) .icon(CustomIconName::GitClone)
+1 -5
View File
@@ -239,11 +239,7 @@ impl SidebarPanel {
.name .name
.clone() .clone()
.unwrap_or_else(|| SharedString::from(announcement.id.clone())); .unwrap_or_else(|| SharedString::from(announcement.id.clone()));
let avatar = PixelAvatar::new(format!( let avatar = PixelAvatar::new(format!("{}:{}", announcement.owner, announcement.id));
"{}:{}",
announcement.owner.to_hex(),
announcement.id
));
let announcement = announcement.clone(); let announcement = announcement.clone();
NavItem::new(format!("my-repo:{}", announcement.id), name, avatar).on_click( NavItem::new(format!("my-repo:{}", announcement.id), name, avatar).on_click(