feat: add new first screen design #7
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M3.75 5.75C3.75 4.64543 4.64543 3.75 5.75 3.75H8.25C9.35457 3.75 10.25 4.64543 10.25 5.75V8.25C10.25 9.35457 9.35457 10.25 8.25 10.25H5.75C4.64543 10.25 3.75 9.35457 3.75 8.25V5.75Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M3.75 15.75C3.75 14.6454 4.64543 13.75 5.75 13.75H8.25C9.35457 13.75 10.25 14.6454 10.25 15.75V18.25C10.25 19.3546 9.35457 20.25 8.25 20.25H5.75C4.64543 20.25 3.75 19.3546 3.75 18.25V15.75Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M13.75 17C13.75 15.2051 15.2051 13.75 17 13.75C18.7949 13.75 20.25 15.2051 20.25 17C20.25 18.7949 18.7949 20.25 17 20.25C15.2051 20.25 13.75 18.7949 13.75 17Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M13.75 5.75C13.75 4.64543 14.6454 3.75 15.75 3.75H18.25C19.3546 3.75 20.25 4.64543 20.25 5.75V8.25C20.25 9.35457 19.3546 10.25 18.25 10.25H15.75C14.6454 10.25 13.75 9.35457 13.75 8.25V5.75Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 7.75V12L15.5 15.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M2.75 4.75V8.75H6.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M3.25 15.0833C4.52169 18.676 7.95303 21.25 11.9864 21.25C17.1026 21.25 21.25 17.1086 21.25 12C21.25 6.89137 17.1026 2.75 11.9864 2.75C8.14808 2.75 4.85497 5.08106 3.44947 8.40278" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 600 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M15.75 6.75H21.25V12.25M20.7361 7.275L14.4142 13.5878C13.633 14.3679 12.3675 14.3675 11.5868 13.5868L10.4142 12.4142C9.63316 11.6332 8.36684 11.6332 7.58579 12.4142L2.75 17.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 356 B |
@@ -93,6 +93,9 @@ pub enum CustomIconName {
|
||||
Tag,
|
||||
Markdown,
|
||||
Share,
|
||||
Trending,
|
||||
Recent,
|
||||
Grid,
|
||||
}
|
||||
|
||||
impl IconNamed for CustomIconName {
|
||||
@@ -118,6 +121,9 @@ impl IconNamed for CustomIconName {
|
||||
CustomIconName::Tag => "icons/tag.svg",
|
||||
CustomIconName::Markdown => "icons/markdown.svg",
|
||||
CustomIconName::Share => "icons/share.svg",
|
||||
CustomIconName::Trending => "icons/trending.svg",
|
||||
CustomIconName::Recent => "icons/recent.svg",
|
||||
CustomIconName::Grid => "icons/grid.svg",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -807,7 +807,7 @@ fn tree_diff(
|
||||
/// Parse a `git format-patch` output (a single patch or a patch series)
|
||||
/// into the same [`CommitDiff`] structure used for commit diffs.
|
||||
///
|
||||
/// The mbox envelope (From/Subject/… headers, commit body and diffstat)
|
||||
/// The mbox envelope (From/Subject/... headers, commit body and diffstat)
|
||||
/// is skipped; every `diff --git` section becomes one [`FileDiff`]. Paths
|
||||
/// are taken from the section headers, with git's C-style quoting undone.
|
||||
/// Sections without hunks (pure renames, mode changes, binary files) are
|
||||
@@ -895,7 +895,7 @@ fn name_from_address(from: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Strip the `[PATCH]`, `[PATCH 1/2]`, `[RFC PATCH]` … prefix from a patch
|
||||
/// Strip the `[PATCH]`, `[PATCH 1/2]`, `[RFC PATCH]` ... prefix from a patch
|
||||
/// `Subject:` header.
|
||||
fn strip_patch_prefix(subject: &str) -> String {
|
||||
let trimmed = subject.trim();
|
||||
@@ -1060,7 +1060,7 @@ fn parse_hunk(lines: &[&str], start: usize) -> Result<(DiffHunk, usize)> {
|
||||
}
|
||||
|
||||
/// The kind of a hunk body line, from its first character; lines that don't
|
||||
/// belong to the hunk (headers, `\ No newline…`, the next section) yield
|
||||
/// belong to the hunk (headers, `\ No newline...`, the next section) yield
|
||||
/// `None`.
|
||||
fn line_prefix_kind(line: &str) -> Option<DiffLineKind> {
|
||||
match line.as_bytes().first()? {
|
||||
|
||||
@@ -134,7 +134,7 @@ impl RepoDetailView {
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("Cloning repository…"),
|
||||
.child("Cloning repository..."),
|
||||
)
|
||||
.into_any_element()
|
||||
} else if let Some(error) = error {
|
||||
|
||||
@@ -37,7 +37,7 @@ impl IssueDetailView {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let comment_input =
|
||||
cx.new(|cx| TextareaState::new(window, cx).placeholder("Leave a comment…"));
|
||||
cx.new(|cx| TextareaState::new(window, cx).placeholder("Leave a comment..."));
|
||||
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
|
||||
@@ -347,7 +347,7 @@ impl IssuesView {
|
||||
/// through [`RepoStore::open_issue`] when confirmed.
|
||||
pub(super) fn open_new_issue_dialog(store: Entity<RepoStore>, window: &mut Window, cx: &mut App) {
|
||||
let subject = cx.new(|cx| InputState::new(window, cx).placeholder("Issue title"));
|
||||
let content = cx.new(|cx| TextareaState::new(window, cx).placeholder("Describe the issue…"));
|
||||
let content = cx.new(|cx| TextareaState::new(window, cx).placeholder("Describe the issue..."));
|
||||
|
||||
window.open_dialog(cx, move |dialog, _window, _cx| {
|
||||
let subject = subject.clone();
|
||||
|
||||
@@ -94,7 +94,7 @@ impl PullRequestDetailView {
|
||||
) -> Self {
|
||||
let tree_state = cx.new(|cx| TreeState::new(cx));
|
||||
let comment_input =
|
||||
cx.new(|cx| TextareaState::new(window, cx).placeholder("Leave a comment…"));
|
||||
cx.new(|cx| TextareaState::new(window, cx).placeholder("Leave a comment..."));
|
||||
|
||||
// Same display name as the repo detail panel's title.
|
||||
let repo_name = store
|
||||
|
||||
@@ -442,9 +442,9 @@ pub(super) fn open_new_pull_request_dialog(
|
||||
) {
|
||||
let subject = cx.new(|cx| InputState::new(window, cx).placeholder("Pull request title"));
|
||||
let description =
|
||||
cx.new(|cx| TextareaState::new(window, cx).placeholder("Describe the change…"));
|
||||
let patch =
|
||||
cx.new(|cx| TextareaState::new(window, cx).placeholder("Paste `git format-patch` output…"));
|
||||
cx.new(|cx| TextareaState::new(window, cx).placeholder("Describe the change..."));
|
||||
let patch = cx
|
||||
.new(|cx| TextareaState::new(window, cx).placeholder("Paste `git format-patch` output..."));
|
||||
|
||||
window.open_dialog(cx, move |dialog, _window, _cx| {
|
||||
let subject = subject.clone();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use assets::CustomIconName;
|
||||
use dock::{BasePanel, DockArea, DockPlacement, Panel, PanelEvent, panel_handle};
|
||||
use gpui::prelude::*;
|
||||
use gpui::{
|
||||
@@ -8,9 +9,11 @@ use gpui::{
|
||||
};
|
||||
use gpui_base::Button as BaseButton;
|
||||
use gpui_component::avatar::Avatar;
|
||||
use gpui_component::input::{Input, InputEvent, InputState};
|
||||
use gpui_component::scroll::Scrollbar;
|
||||
use gpui_component::{
|
||||
ActiveTheme, Sizable, StyledExt, VirtualListScrollHandle, h_flex, v_flex, v_virtual_list,
|
||||
ActiveTheme, Icon, IconName, Sizable, StyledExt, VirtualListScrollHandle, h_flex, v_flex,
|
||||
v_virtual_list,
|
||||
};
|
||||
use signed_core::Announcement;
|
||||
use signed_state::{ProfileStore, RepoListStore, Timestamp};
|
||||
@@ -38,10 +41,24 @@ enum RepoFilter {
|
||||
}
|
||||
|
||||
impl RepoFilter {
|
||||
fn visible(self, store: &RepoListStore) -> Vec<usize> {
|
||||
/// Indices into the store's `announcements` included by this filter, in
|
||||
/// display order, narrowed to repositories whose name (or id) contains
|
||||
/// `query`; an empty query matches everything.
|
||||
fn visible(self, store: &RepoListStore, query: &str) -> Vec<usize> {
|
||||
let announcements = &store.announcements;
|
||||
let mut indices: Vec<usize> = (0..announcements.len()).collect();
|
||||
|
||||
// Narrow by the search query first, so "Recent" limits the matches
|
||||
// and "Popular" ranks them.
|
||||
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),
|
||||
@@ -57,6 +74,14 @@ impl RepoFilter {
|
||||
|
||||
indices
|
||||
}
|
||||
|
||||
fn icon_name(self) -> CustomIconName {
|
||||
match self {
|
||||
Self::All => CustomIconName::Grid,
|
||||
Self::Recent => CustomIconName::Recent,
|
||||
Self::Popular => CustomIconName::Trending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Browse all announced repositories.
|
||||
@@ -72,22 +97,35 @@ pub struct RepoListView {
|
||||
/// Number of rows [`Self::item_sizes`] was built for (the filtered repo count).
|
||||
repo_len: usize,
|
||||
/// Indices into the store's `announcements` matching [`Self::filter`],
|
||||
/// in display order; rebuilt when the store changes or the filter is switched.
|
||||
/// The virtual list renders this slice.
|
||||
/// in display order; rebuilt when the store changes, the filter is
|
||||
/// switched, or the search text changes. The virtual list renders this
|
||||
/// slice.
|
||||
visible: Vec<usize>,
|
||||
/// Search box filtering repositories by name.
|
||||
search: Entity<InputState>,
|
||||
/// Rebuilds the visible slice as the search text changes.
|
||||
_search_subscription: Subscription,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl RepoListView {
|
||||
pub fn new(
|
||||
dock_area: WeakEntity<DockArea>,
|
||||
_window: &mut Window,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let store = RepoListStore::global(cx);
|
||||
|
||||
// Keep the visible slice and row sizes in sync with the store, so
|
||||
// newly announced repositories appear without waiting for a click.
|
||||
// Live search over repository names
|
||||
let search = cx.new(|cx| InputState::new(window, cx).placeholder("Search..."));
|
||||
let search_subscription = cx.subscribe(&search, |this, _search, event, cx| {
|
||||
if matches!(event, InputEvent::Change) {
|
||||
this.rebuild_rows(cx);
|
||||
}
|
||||
});
|
||||
|
||||
// Keep the visible slice and row sizes in sync with the store,
|
||||
// so newly announced repositories appear without waiting for a click.
|
||||
let subscription = cx.observe(&store, |this, _store, cx| {
|
||||
this.rebuild_rows(cx);
|
||||
});
|
||||
@@ -101,6 +139,8 @@ impl RepoListView {
|
||||
item_sizes: Rc::new(Vec::new()),
|
||||
repo_len: 0,
|
||||
visible: Vec::new(),
|
||||
search,
|
||||
_search_subscription: search_subscription,
|
||||
_subscription: subscription,
|
||||
};
|
||||
|
||||
@@ -113,13 +153,15 @@ impl RepoListView {
|
||||
}
|
||||
|
||||
/// Rebuild [`Self::visible`] and [`Self::item_sizes`] from the current
|
||||
/// store contents and [`Self::filter`]. Called when the view is created,
|
||||
/// when the store changes, and when the filter is switched,
|
||||
/// so the list is ready before the next render.
|
||||
/// store contents, [`Self::filter`] and the search query. Called when
|
||||
/// the view is created, when the store changes, when the filter is
|
||||
/// switched, and on every search keystroke, so the list is ready before
|
||||
/// the next render.
|
||||
fn rebuild_rows(&mut self, cx: &mut Context<Self>) {
|
||||
let filter = self.filter;
|
||||
let query = self.search.read(cx).value();
|
||||
let store = self.store.read(cx);
|
||||
self.visible = filter.visible(store);
|
||||
self.visible = filter.visible(store, &query);
|
||||
|
||||
// Each virtual list row holds `COLUMNS` repo cards.
|
||||
let rows = self.visible.len().div_ceil(COLUMNS);
|
||||
@@ -252,18 +294,44 @@ impl RepoListView {
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// Header of the explore list: title, the count of visible repositories and the sort filter buttons.
|
||||
fn render_header(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
fn render_header(&self, count: usize, cx: &mut Context<Self>) -> AnyElement {
|
||||
h_flex()
|
||||
.px_4()
|
||||
.py_2()
|
||||
.w_full()
|
||||
.gap_3()
|
||||
.items_center()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.text_xs()
|
||||
.child(div().font_semibold().child("Repositories"))
|
||||
.child(
|
||||
div()
|
||||
.w_10()
|
||||
.min_w_0()
|
||||
.truncate()
|
||||
.text_ellipsis()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(SharedString::from(format!("({count})"))),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Input::new(&self.search)
|
||||
.cleanable(true)
|
||||
.w(px(180.))
|
||||
.text_sm()
|
||||
.border_color(cx.theme().muted)
|
||||
.bg(cx.theme().muted)
|
||||
.prefix(Icon::new(IconName::Search).small()),
|
||||
)
|
||||
.child(div().flex_1())
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(self.filter_button(RepoFilter::All, "All", cx))
|
||||
.child(self.filter_button(RepoFilter::Popular, "Popular", cx))
|
||||
.child(self.filter_button(RepoFilter::Recent, "Recent", cx))
|
||||
.child(div().flex_1())
|
||||
.child(self.filter_button(RepoFilter::Recent, "Recent", cx)),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -275,22 +343,22 @@ impl RepoListView {
|
||||
label: &'static str,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let active = self.filter == filter;
|
||||
|
||||
BaseButton::new(label)
|
||||
.flex()
|
||||
.items_center()
|
||||
.h_7()
|
||||
.px_2()
|
||||
.gap_1()
|
||||
.child(Icon::new(filter.icon_name()))
|
||||
.child(div().text_sm().child(label))
|
||||
.text_color(cx.theme().button_foreground)
|
||||
.rounded(cx.theme().radius)
|
||||
.hover(|this| this.bg(cx.theme().button_hover))
|
||||
.active(|this| this.bg(cx.theme().primary_active))
|
||||
.selected(self.filter == filter)
|
||||
.when(self.filter == filter, |this| {
|
||||
this.bg(cx.theme().primary)
|
||||
.text_color(cx.theme().primary_foreground)
|
||||
})
|
||||
.active(|this| this.bg(cx.theme().button_active))
|
||||
.selected(active)
|
||||
.when(active, |this| this.bg(cx.theme().button_active))
|
||||
.on_click(cx.listener(move |this, _event, _window, cx| {
|
||||
this.filter = filter;
|
||||
this.rebuild_rows(cx);
|
||||
@@ -331,8 +399,7 @@ impl Render for RepoListView {
|
||||
.relative()
|
||||
.image_cache(image_cache("repos", MAX_IMAGES))
|
||||
.size_full()
|
||||
.pb_3()
|
||||
.child(self.render_header(cx))
|
||||
.child(self.render_header(count, cx))
|
||||
.when(!has_repos, |this| {
|
||||
this.child(
|
||||
v_flex().size_full().items_center().justify_center().child(
|
||||
|
||||
@@ -214,7 +214,7 @@ pub fn open(dock_area: WeakEntity<DockArea>, window: &mut Window, cx: &mut App)
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("Loading your grasp servers…"),
|
||||
.child("Loading your grasp servers..."),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user