add repo info dialog
This commit is contained in:
Generated
+1
@@ -10869,6 +10869,7 @@ version = "1.0.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"assets",
|
"assets",
|
||||||
|
"chrono",
|
||||||
"dock",
|
"dock",
|
||||||
"futures",
|
"futures",
|
||||||
"gix",
|
"gix",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user