feat: add repo about dialog #5
@@ -10869,6 +10869,7 @@ version = "1.0.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"assets",
|
"assets",
|
||||||
|
"chrono",
|
||||||
"dock",
|
"dock",
|
||||||
"futures",
|
"futures",
|
||||||
"gix",
|
"gix",
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"><path d="M20 9L13.4142 15.5858C12.6332 16.3668 11.3669 16.3668 10.5858 15.5858L4 9" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M5.75 9.5L12 15.75L18.25 9.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 244 B After Width: | Height: | Size: 209 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<circle cx="12" cy="12" r="5.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M6.5 12H1.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M22.25 12H17.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 357 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M20.255 17.05V10.35C20.255 9.78995 20.255 9.50992 20.146 9.29601C20.0501 9.10785 19.8972 8.95487 19.709 8.85899C19.4951 8.75 19.2151 8.75 18.655 8.75H5.35C4.78995 8.75 4.50992 8.75 4.29601 8.85899C4.10785 8.95487 3.95487 9.10785 3.85899 9.29601C3.75 9.50992 3.75 9.78995 3.75 10.35V17.05C3.75 18.1701 3.75 18.7302 3.96799 19.158C4.15973 19.5343 4.46569 19.8403 4.84202 20.032C5.26984 20.25 5.82989 20.25 6.95 20.25H17.055C18.1751 20.25 18.7352 20.25 19.163 20.032C19.5393 19.8403 19.8453 19.5343 20.037 19.158C20.255 18.7302 20.255 18.1701 20.255 17.05Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M5.75 8V3.75C5.75 3.19772 6.19772 2.75 6.75 2.75H9.67157C10.202 2.75 10.7107 2.96071 11.0858 3.33579L12.2071 4.45711C12.3946 4.64464 12.649 4.75 12.9142 4.75H17.25C17.8023 4.75 18.25 5.19772 18.25 5.75V8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M7.75 12.75L11.75 12.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -1 +1,3 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"><path d="M12 5.25V12M12 12V18.75M12 12H5.25M12 12H18.75" stroke="black" stroke-width="1.5" stroke-linecap="round"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M12 5.25V12M12 12V18.75M12 12H5.25M12 12H18.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 193 B After Width: | Height: | Size: 203 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M9.00003 10.4286L14 7.57141M9.00003 13.5714L14 16.4286M20.25 6C20.25 7.79493 18.7949 9.25 17 9.25C15.2051 9.25 13.75 7.79493 13.75 6C13.75 4.20507 15.2051 2.75 17 2.75C18.7949 2.75 20.25 4.20507 20.25 6ZM20.25 18C20.25 19.7949 18.7949 21.25 17 21.25C15.2051 21.25 13.75 19.7949 13.75 18C13.75 16.2051 15.2051 14.75 17 14.75C18.7949 14.75 20.25 16.2051 20.25 18ZM9.25 12C9.25 13.7949 7.79493 15.25 6 15.25C4.20507 15.25 2.75 13.7949 2.75 12C2.75 10.2051 4.20507 8.75 6 8.75C7.79493 8.75 9.25 10.2051 9.25 12Z" stroke="currentColor" stroke-width="1.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 641 B |
@@ -1,576 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://github.com/longbridge/gpui-component/raw/refs/heads/main/.theme-schema.json",
|
|
||||||
"name": "Signed",
|
|
||||||
"author": "Signed",
|
|
||||||
"url": "https://git.reya.su/reya/signed",
|
|
||||||
"themes": [
|
|
||||||
{
|
|
||||||
"name": "Signed Light",
|
|
||||||
"mode": "light",
|
|
||||||
"colors": {
|
|
||||||
"accent.background": "#F4F4F5",
|
|
||||||
"accent.foreground": "#18181B",
|
|
||||||
"accordion.background": "#FFFFFF",
|
|
||||||
"background": "#FFFFFF",
|
|
||||||
"border": "#E4E4E7",
|
|
||||||
"button.background": "#FFFFFF",
|
|
||||||
"button.active.background": "#E4E4E7",
|
|
||||||
"button.foreground": "#18181B",
|
|
||||||
"button.hover.background": "#F4F4F5",
|
|
||||||
"button.danger.background": "#DC2626",
|
|
||||||
"button.danger.active.background": "#B91C1C",
|
|
||||||
"button.danger.foreground": "#FFFFFF",
|
|
||||||
"button.danger.hover.background": "#EF4444",
|
|
||||||
"button.info.background": "#0284C7",
|
|
||||||
"button.info.active.background": "#0369A1",
|
|
||||||
"button.info.foreground": "#FFFFFF",
|
|
||||||
"button.info.hover.background": "#0EA5E9",
|
|
||||||
"button.primary.background": "#C6FF4D",
|
|
||||||
"button.primary.active.background": "#65A30D",
|
|
||||||
"button.primary.foreground": "#0B0F0C",
|
|
||||||
"button.primary.hover.background": "#B2EC3C",
|
|
||||||
"button.secondary.background": "#F4F4F5",
|
|
||||||
"button.secondary.active.background": "#D4D4D8",
|
|
||||||
"button.secondary.foreground": "#18181B",
|
|
||||||
"button.secondary.hover.background": "#E4E4E7",
|
|
||||||
"button.success.background": "#2FBF71",
|
|
||||||
"button.success.active.background": "#1F9A58",
|
|
||||||
"button.success.foreground": "#0B0F0C",
|
|
||||||
"button.success.hover.background": "#2AB568",
|
|
||||||
"button.warning.background": "#D97706",
|
|
||||||
"button.warning.active.background": "#B45309",
|
|
||||||
"button.warning.foreground": "#FFFFFF",
|
|
||||||
"button.warning.hover.background": "#F59E0B",
|
|
||||||
"group_box.background": "#F4F4F5",
|
|
||||||
"group_box.foreground": "#18181B",
|
|
||||||
"group_box.title.foreground": "#18181B",
|
|
||||||
"caret": "#18181B",
|
|
||||||
"chart.1": "#65A30D",
|
|
||||||
"chart.2": "#2FBF71",
|
|
||||||
"chart.3": "#3F6212",
|
|
||||||
"chart.4": "#7A8B5F",
|
|
||||||
"chart.5": "#A1A1AA",
|
|
||||||
"chart_bullish": "#2FBF71",
|
|
||||||
"chart_bearish": "#DC2626",
|
|
||||||
"danger.background": "#DC2626",
|
|
||||||
"danger.active.background": "#B91C1C",
|
|
||||||
"danger.foreground": "#FFFFFF",
|
|
||||||
"danger.hover.background": "#EF4444",
|
|
||||||
"description_list.label.background": "#F4F4F5",
|
|
||||||
"description_list.label.foreground": "#71717A",
|
|
||||||
"drag.border": "#65A30D",
|
|
||||||
"drop_target.background": "#65A30D33",
|
|
||||||
"foreground": "#18181B",
|
|
||||||
"info.background": "#0284C7",
|
|
||||||
"info.active.background": "#0369A1",
|
|
||||||
"info.foreground": "#FFFFFF",
|
|
||||||
"info.hover.background": "#0EA5E9",
|
|
||||||
"input.border": "#D4D4D8",
|
|
||||||
"link": "#65A30D",
|
|
||||||
"link.active": "#3F6212",
|
|
||||||
"link.hover": "#4A7A0B",
|
|
||||||
"list.background": "#FFFFFF",
|
|
||||||
"list.active.background": "#C6FF4D40",
|
|
||||||
"list.active.border": "#65A30D80",
|
|
||||||
"list.even.background": "#FAFAFA",
|
|
||||||
"list.head.background": "#FAFAFA",
|
|
||||||
"list.hover.background": "#F4F4F5",
|
|
||||||
"muted.background": "#F4F4F5",
|
|
||||||
"muted.foreground": "#71717A",
|
|
||||||
"popover.background": "#FFFFFF",
|
|
||||||
"popover.foreground": "#18181B",
|
|
||||||
"primary.background": "#C6FF4D",
|
|
||||||
"primary.active.background": "#65A30D",
|
|
||||||
"primary.foreground": "#0B0F0C",
|
|
||||||
"primary.hover.background": "#B2EC3C",
|
|
||||||
"progress.bar.background": "#65A30D",
|
|
||||||
"ring": "#65A30D",
|
|
||||||
"scrollbar.background": "#FFFFFF00",
|
|
||||||
"scrollbar.thumb.background": "#A1A1AA99",
|
|
||||||
"scrollbar.thumb.hover.background": "#A1A1AA",
|
|
||||||
"secondary.background": "#F4F4F5",
|
|
||||||
"secondary.active.background": "#D4D4D8",
|
|
||||||
"secondary.foreground": "#18181B",
|
|
||||||
"secondary.hover.background": "#E4E4E7",
|
|
||||||
"selection.background": "#C6FF4D59",
|
|
||||||
"sidebar.background": "#FAFAFA",
|
|
||||||
"sidebar.accent.background": "#C6FF4D40",
|
|
||||||
"sidebar.accent.foreground": "#65A30D",
|
|
||||||
"sidebar.border": "#E4E4E7",
|
|
||||||
"sidebar.foreground": "#18181B",
|
|
||||||
"sidebar.primary.background": "#F4F4F5",
|
|
||||||
"sidebar.primary.foreground": "#18181B",
|
|
||||||
"skeleton.background": "#E4E4E7",
|
|
||||||
"slider.background": "#E4E4E7",
|
|
||||||
"slider.thumb.background": "#65A30D",
|
|
||||||
"success.background": "#2FBF71",
|
|
||||||
"success.active.background": "#1F9A58",
|
|
||||||
"success.foreground": "#0B0F0C",
|
|
||||||
"success.hover.background": "#2AB568",
|
|
||||||
"switch.background": "#D4D4D8",
|
|
||||||
"switch.thumb.background": "#FFFFFF",
|
|
||||||
"tab.background": "#F4F4F5",
|
|
||||||
"tab.active.background": "#EBFFC1",
|
|
||||||
"tab.active.foreground": "#3F6212",
|
|
||||||
"tab.foreground": "#71717A",
|
|
||||||
"tab_bar.background": "#F4F4F5",
|
|
||||||
"tab_bar.segmented.background": "#E4E4E7",
|
|
||||||
"table.background": "#FFFFFF",
|
|
||||||
"table.active.background": "#C6FF4D40",
|
|
||||||
"table.active.border": "#65A30D80",
|
|
||||||
"table.even.background": "#FAFAFA",
|
|
||||||
"table.head.background": "#FAFAFA",
|
|
||||||
"table.head.foreground": "#71717A",
|
|
||||||
"table.foot.background": "#FAFAFA",
|
|
||||||
"table.foot.foreground": "#71717A",
|
|
||||||
"table.hover.background": "#F4F4F5",
|
|
||||||
"table.row.border": "#E4E4E7",
|
|
||||||
"title_bar.background": "#FAFAFA",
|
|
||||||
"title_bar.border": "#E4E4E7",
|
|
||||||
"status_bar.background": "#FAFAFA",
|
|
||||||
"status_bar.border": "#E4E4E7",
|
|
||||||
"tiles.background": "#FFFFFF",
|
|
||||||
"warning.background": "#D97706",
|
|
||||||
"warning.active.background": "#B45309",
|
|
||||||
"warning.foreground": "#FFFFFF",
|
|
||||||
"warning.hover.background": "#F59E0B",
|
|
||||||
"overlay": "#0000004D",
|
|
||||||
"window.border": "#E4E4E7",
|
|
||||||
"base.red": "#DC2626",
|
|
||||||
"base.green": "#16A34A",
|
|
||||||
"base.yellow": "#CA8A04",
|
|
||||||
"base.blue": "#2563EB",
|
|
||||||
"base.magenta": "#9333EA",
|
|
||||||
"base.cyan": "#0891B2"
|
|
||||||
},
|
|
||||||
"highlight": {
|
|
||||||
"editor.background": "#FFFFFF",
|
|
||||||
"editor.foreground": "#383A42",
|
|
||||||
"editor.active_line.background": "#F2F3F4",
|
|
||||||
"editor.line_number": "#A0A1A7",
|
|
||||||
"editor.active_line_number": "#383A42",
|
|
||||||
"editor.invisible": "#A0A1A766",
|
|
||||||
"conflict": "#DC2626",
|
|
||||||
"created": "#16A34A",
|
|
||||||
"deleted": "#DC2626",
|
|
||||||
"error": "#DC2626",
|
|
||||||
"error.background": "#FEF2F2",
|
|
||||||
"error.border": "#F87171",
|
|
||||||
"hidden": "#A0A1A7",
|
|
||||||
"hint": "#A626A4",
|
|
||||||
"hint.background": "#FAF5FF",
|
|
||||||
"hint.border": "#C4B5FD",
|
|
||||||
"ignored": "#A0A1A7",
|
|
||||||
"info": "#4078F2",
|
|
||||||
"info.background": "#EFF6FF",
|
|
||||||
"info.border": "#93C5FD",
|
|
||||||
"modified": "#C18401",
|
|
||||||
"modified.background": "#FFFBEB",
|
|
||||||
"predictive": "#A0A1A7",
|
|
||||||
"renamed": "#A626A4",
|
|
||||||
"success": "#16A34A",
|
|
||||||
"success.background": "#F0FDF4",
|
|
||||||
"unreachable": "#A0A1A7",
|
|
||||||
"warning": "#C18401",
|
|
||||||
"warning.background": "#FFFBEB",
|
|
||||||
"warning.border": "#FCD34D",
|
|
||||||
"syntax": {
|
|
||||||
"attribute": {
|
|
||||||
"color": "#986801"
|
|
||||||
},
|
|
||||||
"boolean": {
|
|
||||||
"color": "#986801"
|
|
||||||
},
|
|
||||||
"comment": {
|
|
||||||
"color": "#A0A1A7",
|
|
||||||
"font_style": "italic"
|
|
||||||
},
|
|
||||||
"comment.doc": {
|
|
||||||
"color": "#A0A1A7",
|
|
||||||
"font_style": "italic"
|
|
||||||
},
|
|
||||||
"constant": {
|
|
||||||
"color": "#986801"
|
|
||||||
},
|
|
||||||
"constructor": {
|
|
||||||
"color": "#4078F2"
|
|
||||||
},
|
|
||||||
"embedded": {
|
|
||||||
"color": "#C18401"
|
|
||||||
},
|
|
||||||
"function": {
|
|
||||||
"color": "#4078F2"
|
|
||||||
},
|
|
||||||
"keyword": {
|
|
||||||
"color": "#A626A4"
|
|
||||||
},
|
|
||||||
"label": {
|
|
||||||
"color": "#4078F2"
|
|
||||||
},
|
|
||||||
"link_text": {
|
|
||||||
"color": "#4078F2",
|
|
||||||
"font_style": "underline"
|
|
||||||
},
|
|
||||||
"link_uri": {
|
|
||||||
"color": "#4078F2",
|
|
||||||
"font_style": "italic"
|
|
||||||
},
|
|
||||||
"number": {
|
|
||||||
"color": "#986801"
|
|
||||||
},
|
|
||||||
"operator": {
|
|
||||||
"color": "#383A42"
|
|
||||||
},
|
|
||||||
"preproc": {
|
|
||||||
"color": "#C18401"
|
|
||||||
},
|
|
||||||
"property": {
|
|
||||||
"color": "#E45649"
|
|
||||||
},
|
|
||||||
"punctuation": {
|
|
||||||
"color": "#383A42"
|
|
||||||
},
|
|
||||||
"punctuation.bracket": {
|
|
||||||
"color": "#383A42"
|
|
||||||
},
|
|
||||||
"punctuation.delimiter": {
|
|
||||||
"color": "#383A42"
|
|
||||||
},
|
|
||||||
"punctuation.list_marker": {
|
|
||||||
"color": "#986801"
|
|
||||||
},
|
|
||||||
"punctuation.special": {
|
|
||||||
"color": "#A626A4"
|
|
||||||
},
|
|
||||||
"string": {
|
|
||||||
"color": "#50A14F"
|
|
||||||
},
|
|
||||||
"string.escape": {
|
|
||||||
"color": "#A626A4"
|
|
||||||
},
|
|
||||||
"string.regex": {
|
|
||||||
"color": "#50A14F"
|
|
||||||
},
|
|
||||||
"string.special": {
|
|
||||||
"color": "#50A14F"
|
|
||||||
},
|
|
||||||
"string.special.symbol": {
|
|
||||||
"color": "#986801"
|
|
||||||
},
|
|
||||||
"tag": {
|
|
||||||
"color": "#E45649"
|
|
||||||
},
|
|
||||||
"tag.doctype": {
|
|
||||||
"color": "#A0A1A7"
|
|
||||||
},
|
|
||||||
"text.code.span": {
|
|
||||||
"color": "#383A42"
|
|
||||||
},
|
|
||||||
"text.literal": {
|
|
||||||
"color": "#50A14F"
|
|
||||||
},
|
|
||||||
"title": {
|
|
||||||
"color": "#E45649",
|
|
||||||
"font_weight": 700
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"color": "#0184BC"
|
|
||||||
},
|
|
||||||
"variable": {
|
|
||||||
"color": "#383A42"
|
|
||||||
},
|
|
||||||
"variable.special": {
|
|
||||||
"color": "#E45649"
|
|
||||||
},
|
|
||||||
"variant": {
|
|
||||||
"color": "#0184BC"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Signed Dark",
|
|
||||||
"mode": "dark",
|
|
||||||
"colors": {
|
|
||||||
"accent.background": "#18181B",
|
|
||||||
"accent.foreground": "#FAFAFA",
|
|
||||||
"accordion.background": "#0A0A0A",
|
|
||||||
"background": "#0A0A0A",
|
|
||||||
"border": "#27272A",
|
|
||||||
"button.background": "#18181B",
|
|
||||||
"button.active.background": "#3F3F46",
|
|
||||||
"button.foreground": "#FAFAFA",
|
|
||||||
"button.hover.background": "#27272A",
|
|
||||||
"button.danger.background": "#EF4444",
|
|
||||||
"button.danger.active.background": "#DC2626",
|
|
||||||
"button.danger.foreground": "#FFFFFF",
|
|
||||||
"button.danger.hover.background": "#F87171",
|
|
||||||
"button.info.background": "#0EA5E9",
|
|
||||||
"button.info.active.background": "#0284C7",
|
|
||||||
"button.info.foreground": "#FFFFFF",
|
|
||||||
"button.info.hover.background": "#38BDF8",
|
|
||||||
"button.primary.background": "#C6FF4D",
|
|
||||||
"button.primary.active.background": "#65A30D",
|
|
||||||
"button.primary.foreground": "#0B0F0C",
|
|
||||||
"button.primary.hover.background": "#B2EC3C",
|
|
||||||
"button.secondary.background": "#18181B",
|
|
||||||
"button.secondary.active.background": "#3F3F46",
|
|
||||||
"button.secondary.foreground": "#FAFAFA",
|
|
||||||
"button.secondary.hover.background": "#27272A",
|
|
||||||
"button.success.background": "#2FBF71",
|
|
||||||
"button.success.active.background": "#24A35D",
|
|
||||||
"button.success.foreground": "#0B0F0C",
|
|
||||||
"button.success.hover.background": "#2AB568",
|
|
||||||
"button.warning.background": "#F59E0B",
|
|
||||||
"button.warning.active.background": "#D97706",
|
|
||||||
"button.warning.foreground": "#18181B",
|
|
||||||
"button.warning.hover.background": "#FBBF24",
|
|
||||||
"group_box.background": "#18181B",
|
|
||||||
"group_box.foreground": "#FAFAFA",
|
|
||||||
"group_box.title.foreground": "#FAFAFA",
|
|
||||||
"caret": "#FAFAFA",
|
|
||||||
"chart.1": "#C6FF4D",
|
|
||||||
"chart.2": "#A6E22E",
|
|
||||||
"chart.3": "#2FBF71",
|
|
||||||
"chart.4": "#7A8B5F",
|
|
||||||
"chart.5": "#A1A1AA",
|
|
||||||
"chart_bullish": "#2FBF71",
|
|
||||||
"chart_bearish": "#EF4444",
|
|
||||||
"danger.background": "#EF4444",
|
|
||||||
"danger.active.background": "#DC2626",
|
|
||||||
"danger.foreground": "#FFFFFF",
|
|
||||||
"danger.hover.background": "#F87171",
|
|
||||||
"description_list.label.background": "#18181B",
|
|
||||||
"description_list.label.foreground": "#A1A1AA",
|
|
||||||
"drag.border": "#C6FF4D",
|
|
||||||
"drop_target.background": "#C6FF4D33",
|
|
||||||
"foreground": "#FAFAFA",
|
|
||||||
"info.background": "#0EA5E9",
|
|
||||||
"info.active.background": "#0284C7",
|
|
||||||
"info.foreground": "#FFFFFF",
|
|
||||||
"info.hover.background": "#38BDF8",
|
|
||||||
"input.border": "#3F3F46",
|
|
||||||
"link": "#C6FF4D",
|
|
||||||
"link.active": "#A6E22E",
|
|
||||||
"link.hover": "#B2EC3C",
|
|
||||||
"list.background": "#0A0A0A",
|
|
||||||
"list.active.background": "#C6FF4D40",
|
|
||||||
"list.active.border": "#C6FF4D80",
|
|
||||||
"list.even.background": "#18181B",
|
|
||||||
"list.head.background": "#18181B",
|
|
||||||
"list.hover.background": "#27272A",
|
|
||||||
"muted.background": "#18181B",
|
|
||||||
"muted.foreground": "#A1A1AA",
|
|
||||||
"popover.background": "#18181B",
|
|
||||||
"popover.foreground": "#FAFAFA",
|
|
||||||
"primary.background": "#C6FF4D",
|
|
||||||
"primary.active.background": "#65A30D",
|
|
||||||
"primary.foreground": "#0B0F0C",
|
|
||||||
"primary.hover.background": "#B2EC3C",
|
|
||||||
"progress.bar.background": "#C6FF4D",
|
|
||||||
"ring": "#C6FF4D",
|
|
||||||
"scrollbar.background": "#0A0A0A00",
|
|
||||||
"scrollbar.thumb.background": "#3F3F46",
|
|
||||||
"scrollbar.thumb.hover.background": "#52525B",
|
|
||||||
"secondary.background": "#18181B",
|
|
||||||
"secondary.active.background": "#3F3F46",
|
|
||||||
"secondary.foreground": "#FAFAFA",
|
|
||||||
"secondary.hover.background": "#27272A",
|
|
||||||
"selection.background": "#C6FF4D59",
|
|
||||||
"sidebar.background": "#0A0A0A",
|
|
||||||
"sidebar.accent.background": "#C6FF4D40",
|
|
||||||
"sidebar.accent.foreground": "#C6FF4D",
|
|
||||||
"sidebar.border": "#27272A",
|
|
||||||
"sidebar.foreground": "#FAFAFA",
|
|
||||||
"sidebar.primary.background": "#18181B",
|
|
||||||
"sidebar.primary.foreground": "#FAFAFA",
|
|
||||||
"skeleton.background": "#27272A",
|
|
||||||
"slider.background": "#27272A",
|
|
||||||
"slider.thumb.background": "#C6FF4D",
|
|
||||||
"success.background": "#2FBF71",
|
|
||||||
"success.active.background": "#24A35D",
|
|
||||||
"success.foreground": "#0B0F0C",
|
|
||||||
"success.hover.background": "#2AB568",
|
|
||||||
"switch.background": "#3F3F46",
|
|
||||||
"switch.thumb.background": "#FAFAFA",
|
|
||||||
"tab.background": "#18181B",
|
|
||||||
"tab.active.background": "#19200A",
|
|
||||||
"tab.active.foreground": "#C6FF4D",
|
|
||||||
"tab.foreground": "#A1A1AA",
|
|
||||||
"tab_bar.background": "#18181B",
|
|
||||||
"tab_bar.segmented.background": "#27272A",
|
|
||||||
"table.background": "#0A0A0A",
|
|
||||||
"table.active.background": "#C6FF4D40",
|
|
||||||
"table.active.border": "#C6FF4D80",
|
|
||||||
"table.even.background": "#18181B",
|
|
||||||
"table.head.background": "#18181B",
|
|
||||||
"table.head.foreground": "#A1A1AA",
|
|
||||||
"table.foot.background": "#18181B",
|
|
||||||
"table.foot.foreground": "#A1A1AA",
|
|
||||||
"table.hover.background": "#27272A",
|
|
||||||
"table.row.border": "#27272A",
|
|
||||||
"title_bar.background": "#18181B",
|
|
||||||
"title_bar.border": "#27272A",
|
|
||||||
"status_bar.background": "#18181B",
|
|
||||||
"status_bar.border": "#27272A",
|
|
||||||
"tiles.background": "#0A0A0A",
|
|
||||||
"warning.background": "#F59E0B",
|
|
||||||
"warning.active.background": "#D97706",
|
|
||||||
"warning.foreground": "#18181B",
|
|
||||||
"warning.hover.background": "#FBBF24",
|
|
||||||
"overlay": "#00000080",
|
|
||||||
"window.border": "#27272A",
|
|
||||||
"base.red": "#EF4444",
|
|
||||||
"base.green": "#22C55E",
|
|
||||||
"base.yellow": "#EAB308",
|
|
||||||
"base.blue": "#3B82F6",
|
|
||||||
"base.magenta": "#A855F7",
|
|
||||||
"base.cyan": "#06B6D4"
|
|
||||||
},
|
|
||||||
"highlight": {
|
|
||||||
"editor.background": "#0A0A0A",
|
|
||||||
"editor.foreground": "#ABB2BF",
|
|
||||||
"editor.active_line.background": "#18181B",
|
|
||||||
"editor.line_number": "#71717A",
|
|
||||||
"editor.active_line_number": "#FAFAFA",
|
|
||||||
"editor.invisible": "#71717A66",
|
|
||||||
"conflict": "#E06C75",
|
|
||||||
"created": "#98C379",
|
|
||||||
"deleted": "#E06C75",
|
|
||||||
"error": "#E06C75",
|
|
||||||
"error.background": "#3A1D1D",
|
|
||||||
"error.border": "#E06C75",
|
|
||||||
"hidden": "#5C6370",
|
|
||||||
"hint": "#C678DD",
|
|
||||||
"hint.background": "#2E2740",
|
|
||||||
"hint.border": "#C678DD",
|
|
||||||
"ignored": "#5C6370",
|
|
||||||
"info": "#61AFEF",
|
|
||||||
"info.background": "#1D2E3A",
|
|
||||||
"info.border": "#61AFEF",
|
|
||||||
"modified": "#E5C07B",
|
|
||||||
"modified.background": "#3A320E",
|
|
||||||
"predictive": "#5C6370",
|
|
||||||
"renamed": "#C678DD",
|
|
||||||
"success": "#98C379",
|
|
||||||
"success.background": "#1D3A2E",
|
|
||||||
"unreachable": "#5C6370",
|
|
||||||
"warning": "#E5C07B",
|
|
||||||
"warning.background": "#3A320E",
|
|
||||||
"warning.border": "#E5C07B",
|
|
||||||
"syntax": {
|
|
||||||
"attribute": {
|
|
||||||
"color": "#D19A66"
|
|
||||||
},
|
|
||||||
"boolean": {
|
|
||||||
"color": "#D19A66"
|
|
||||||
},
|
|
||||||
"comment": {
|
|
||||||
"color": "#5C6370",
|
|
||||||
"font_style": "italic"
|
|
||||||
},
|
|
||||||
"comment.doc": {
|
|
||||||
"color": "#5C6370",
|
|
||||||
"font_style": "italic"
|
|
||||||
},
|
|
||||||
"constant": {
|
|
||||||
"color": "#D19A66"
|
|
||||||
},
|
|
||||||
"constructor": {
|
|
||||||
"color": "#E5C07B"
|
|
||||||
},
|
|
||||||
"embedded": {
|
|
||||||
"color": "#98C379"
|
|
||||||
},
|
|
||||||
"function": {
|
|
||||||
"color": "#61AFEF"
|
|
||||||
},
|
|
||||||
"keyword": {
|
|
||||||
"color": "#C678DD"
|
|
||||||
},
|
|
||||||
"label": {
|
|
||||||
"color": "#61AFEF"
|
|
||||||
},
|
|
||||||
"link_text": {
|
|
||||||
"color": "#61AFEF",
|
|
||||||
"font_style": "underline"
|
|
||||||
},
|
|
||||||
"link_uri": {
|
|
||||||
"color": "#61AFEF",
|
|
||||||
"font_style": "italic"
|
|
||||||
},
|
|
||||||
"number": {
|
|
||||||
"color": "#D19A66"
|
|
||||||
},
|
|
||||||
"operator": {
|
|
||||||
"color": "#56B6C2"
|
|
||||||
},
|
|
||||||
"preproc": {
|
|
||||||
"color": "#E5C07B"
|
|
||||||
},
|
|
||||||
"property": {
|
|
||||||
"color": "#E06C75"
|
|
||||||
},
|
|
||||||
"punctuation": {
|
|
||||||
"color": "#ABB2BF"
|
|
||||||
},
|
|
||||||
"punctuation.bracket": {
|
|
||||||
"color": "#ABB2BF"
|
|
||||||
},
|
|
||||||
"punctuation.delimiter": {
|
|
||||||
"color": "#ABB2BF"
|
|
||||||
},
|
|
||||||
"punctuation.list_marker": {
|
|
||||||
"color": "#98C379"
|
|
||||||
},
|
|
||||||
"punctuation.special": {
|
|
||||||
"color": "#ABB2BF"
|
|
||||||
},
|
|
||||||
"string": {
|
|
||||||
"color": "#98C379"
|
|
||||||
},
|
|
||||||
"string.escape": {
|
|
||||||
"color": "#56B6C2"
|
|
||||||
},
|
|
||||||
"string.regex": {
|
|
||||||
"color": "#E06C75"
|
|
||||||
},
|
|
||||||
"string.special": {
|
|
||||||
"color": "#98C379"
|
|
||||||
},
|
|
||||||
"string.special.symbol": {
|
|
||||||
"color": "#D19A66"
|
|
||||||
},
|
|
||||||
"tag": {
|
|
||||||
"color": "#E06C75"
|
|
||||||
},
|
|
||||||
"tag.doctype": {
|
|
||||||
"color": "#5C6370"
|
|
||||||
},
|
|
||||||
"text.code.span": {
|
|
||||||
"color": "#ABB2BF"
|
|
||||||
},
|
|
||||||
"text.literal": {
|
|
||||||
"color": "#98C379"
|
|
||||||
},
|
|
||||||
"title": {
|
|
||||||
"color": "#61AFEF",
|
|
||||||
"font_weight": 700
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"color": "#E5C07B"
|
|
||||||
},
|
|
||||||
"variable": {
|
|
||||||
"color": "#E06C75"
|
|
||||||
},
|
|
||||||
"variable.special": {
|
|
||||||
"color": "#D19A66"
|
|
||||||
},
|
|
||||||
"variant": {
|
|
||||||
"color": "#E5C07B"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -76,6 +76,8 @@ pub enum CustomIconName {
|
|||||||
Filter,
|
Filter,
|
||||||
GlobalOn,
|
GlobalOn,
|
||||||
GlobalOff,
|
GlobalOff,
|
||||||
|
GitFile,
|
||||||
|
GitCommit,
|
||||||
GitIssueDone,
|
GitIssueDone,
|
||||||
GitIssueOpen,
|
GitIssueOpen,
|
||||||
GitIssueClosed,
|
GitIssueClosed,
|
||||||
@@ -88,6 +90,7 @@ pub enum CustomIconName {
|
|||||||
GitBranch,
|
GitBranch,
|
||||||
Tag,
|
Tag,
|
||||||
Markdown,
|
Markdown,
|
||||||
|
Share,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IconNamed for CustomIconName {
|
impl IconNamed for CustomIconName {
|
||||||
@@ -98,6 +101,8 @@ impl IconNamed for CustomIconName {
|
|||||||
CustomIconName::Filter => "icons/filter.svg",
|
CustomIconName::Filter => "icons/filter.svg",
|
||||||
CustomIconName::GlobalOn => "icons/global-on.svg",
|
CustomIconName::GlobalOn => "icons/global-on.svg",
|
||||||
CustomIconName::GlobalOff => "icons/global-off.svg",
|
CustomIconName::GlobalOff => "icons/global-off.svg",
|
||||||
|
CustomIconName::GitCommit => "icons/git-commit.svg",
|
||||||
|
CustomIconName::GitFile => "icons/git-file.svg",
|
||||||
CustomIconName::GitIssueDone => "icons/git-issue-done.svg",
|
CustomIconName::GitIssueDone => "icons/git-issue-done.svg",
|
||||||
CustomIconName::GitIssueOpen => "icons/git-issue-open.svg",
|
CustomIconName::GitIssueOpen => "icons/git-issue-open.svg",
|
||||||
CustomIconName::GitIssueClosed => "icons/git-issue-close.svg",
|
CustomIconName::GitIssueClosed => "icons/git-issue-close.svg",
|
||||||
@@ -110,6 +115,7 @@ impl IconNamed for CustomIconName {
|
|||||||
CustomIconName::GitBranch => "icons/git-branch.svg",
|
CustomIconName::GitBranch => "icons/git-branch.svg",
|
||||||
CustomIconName::Tag => "icons/tag.svg",
|
CustomIconName::Tag => "icons/tag.svg",
|
||||||
CustomIconName::Markdown => "icons/markdown.svg",
|
CustomIconName::Markdown => "icons/markdown.svg",
|
||||||
|
CustomIconName::Share => "icons/share.svg",
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ use nostr::prelude::*;
|
|||||||
/// Parsed NIP-34 repository announcement (plain data, ready for the UI).
|
/// Parsed NIP-34 repository announcement (plain data, ready for the UI).
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct Announcement {
|
pub struct Announcement {
|
||||||
|
/// ID of the announcement event itself.
|
||||||
|
pub event_id: EventId,
|
||||||
/// Repository ID (`d` tag).
|
/// Repository ID (`d` tag).
|
||||||
pub id: String,
|
pub id: String,
|
||||||
/// Author of the announcement event.
|
/// Author of the announcement event.
|
||||||
@@ -211,6 +213,7 @@ impl Announcement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Some(Self {
|
Some(Self {
|
||||||
|
event_id: event.id,
|
||||||
owner: event.pubkey,
|
owner: event.pubkey,
|
||||||
created_at: event.created_at,
|
created_at: event.created_at,
|
||||||
id,
|
id,
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -3,17 +3,24 @@ use std::path::{Path, PathBuf};
|
|||||||
|
|
||||||
use assets::CustomIconName;
|
use assets::CustomIconName;
|
||||||
use gpui::prelude::*;
|
use gpui::prelude::*;
|
||||||
use gpui::{AnyElement, App, SharedString, Window, div, px};
|
use gpui::{
|
||||||
|
Anchor, AnyElement, App, ClipboardItem, DismissEvent, ElementId, Entity, Focusable,
|
||||||
|
SharedString, StyleRefinement, Window, div, px,
|
||||||
|
};
|
||||||
|
use gpui_base::{Button as BaseButton, Popover, Selectable, StyledExt};
|
||||||
|
use gpui_component::clipboard::Clipboard;
|
||||||
use gpui_component::list::ListItem;
|
use gpui_component::list::ListItem;
|
||||||
|
use gpui_component::menu::{PopupMenu, PopupMenuItem};
|
||||||
use gpui_component::tooltip::Tooltip;
|
use gpui_component::tooltip::Tooltip;
|
||||||
use gpui_component::tree::{TreeEntry, TreeItem};
|
use gpui_component::tree::{TreeEntry, TreeItem};
|
||||||
use gpui_component::{ActiveTheme, Icon, IconName, Sizable, h_flex, v_flex};
|
use gpui_component::{ActiveTheme, Icon, IconName, Sizable, h_flex, v_flex};
|
||||||
use signed_core::RepoStatus;
|
use nostr::nips::nip19::{Nip19Coordinate, ToBech32};
|
||||||
|
use signed_core::{Announcement, RepoStatus};
|
||||||
use signed_git::{DiffHunk, DiffLine, DiffLineKind, FileDiff};
|
use signed_git::{DiffHunk, DiffLine, DiffLineKind, FileDiff};
|
||||||
|
|
||||||
/// A `Send` file-tree node: the tree is built on a background thread and
|
/// A `Send` file-tree node: the tree is built on a background thread and
|
||||||
/// converted into [`TreeItem`]s (which hold `Rc` state, so they cannot
|
/// converted into [`TreeItem`]s (which hold `Rc` state,
|
||||||
/// cross threads) on the main thread.
|
/// so they cannot cross threads) on the main thread.
|
||||||
pub(super) struct TreeItemSeed {
|
pub(super) struct TreeItemSeed {
|
||||||
/// Path of the node, relative to the worktree root.
|
/// Path of the node, relative to the worktree root.
|
||||||
id: String,
|
id: String,
|
||||||
@@ -22,12 +29,12 @@ pub(super) struct TreeItemSeed {
|
|||||||
children: Vec<TreeItemSeed>,
|
children: Vec<TreeItemSeed>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert tree seeds into [`TreeItem`]s, expanding every folder when
|
/// Convert tree seeds into [`TreeItem`]s, expanding every folder
|
||||||
/// `expand_folders` is set.
|
/// when `expand_folders` is set.
|
||||||
///
|
///
|
||||||
/// The commit diff explorer shows only changed files, which is typically a
|
/// The commit diff explorer shows only changed files,
|
||||||
/// handful of paths, so its folders start expanded; the worktree explorer
|
/// which is typically a handful of paths, so its folders start expanded;
|
||||||
/// starts collapsed instead.
|
/// the worktree explorer starts collapsed instead.
|
||||||
pub(super) fn tree_items(seeds: Vec<TreeItemSeed>, expand_folders: bool) -> Vec<TreeItem> {
|
pub(super) fn tree_items(seeds: Vec<TreeItemSeed>, expand_folders: bool) -> Vec<TreeItem> {
|
||||||
fn convert(seed: TreeItemSeed, expand_folders: bool) -> TreeItem {
|
fn convert(seed: TreeItemSeed, expand_folders: bool) -> TreeItem {
|
||||||
let mut item = TreeItem::new(seed.id, seed.label);
|
let mut item = TreeItem::new(seed.id, seed.label);
|
||||||
@@ -48,8 +55,8 @@ pub(super) fn tree_items(seeds: Vec<TreeItemSeed>, expand_folders: bool) -> Vec<
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One row of a file tree: icon + name, indented by depth. Clicking a file
|
/// One row of a file tree: icon + name, indented by depth.
|
||||||
/// runs `on_click`; folders expand/collapse via the tree itself.
|
/// Clicking a file runs `on_click`; folders expand/collapse via the tree itself.
|
||||||
pub(super) fn tree_row<F>(ix: usize, entry: &TreeEntry, selected: bool, on_click: F) -> ListItem
|
pub(super) fn tree_row<F>(ix: usize, entry: &TreeEntry, selected: bool, on_click: F) -> ListItem
|
||||||
where
|
where
|
||||||
F: Fn(&mut Window, &mut App) + 'static,
|
F: Fn(&mut Window, &mut App) + 'static,
|
||||||
@@ -139,8 +146,7 @@ pub(super) fn build_tree_items(entries: &[PathBuf]) -> Vec<TreeItemSeed> {
|
|||||||
/// The markdown fence language for a file path, or `None` for plain text.
|
/// The markdown fence language for a file path, or `None` for plain text.
|
||||||
///
|
///
|
||||||
/// Names are chosen so `gpui_component`'s highlighter can resolve them
|
/// Names are chosen so `gpui_component`'s highlighter can resolve them
|
||||||
/// (`highlighter::Language::from_name` accepts short aliases such as `rs`
|
/// (`highlighter::Language::from_name` accepts short aliases such as `rs` and `js`).
|
||||||
/// and `js`).
|
|
||||||
pub(super) fn code_language(path: &str) -> Option<&'static str> {
|
pub(super) fn code_language(path: &str) -> Option<&'static str> {
|
||||||
let name = Path::new(path)
|
let name = Path::new(path)
|
||||||
.file_name()
|
.file_name()
|
||||||
@@ -220,8 +226,8 @@ pub(super) fn placeholder(message: &str, cx: &App) -> AnyElement {
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The status badge shown next to an issue or pull request: icon + colored
|
/// The status badge shown next to an issue or pull request: icon + colored square,
|
||||||
/// square, with a tooltip describing the status.
|
/// with a tooltip describing the status.
|
||||||
pub(super) fn status_badge(status: RepoStatus, cx: &App) -> AnyElement {
|
pub(super) fn status_badge(status: RepoStatus, cx: &App) -> AnyElement {
|
||||||
let (icon, label, tooltip, bg, fg) = match status {
|
let (icon, label, tooltip, bg, fg) = match status {
|
||||||
RepoStatus::Open => (
|
RepoStatus::Open => (
|
||||||
@@ -267,6 +273,284 @@ pub(super) fn status_badge(status: RepoStatus, cx: &App) -> AnyElement {
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A split dropdown button built on `gpui_base::Popover`: an action element
|
||||||
|
/// with a separate caret trigger that opens a [`PopupMenu`].
|
||||||
|
///
|
||||||
|
/// The action and the caret are ordinary elements supplied by the caller, so
|
||||||
|
/// the look — icons, borders, hover states, sizes — stays fully in the
|
||||||
|
/// application. The component only owns the popover wiring: opening on caret
|
||||||
|
/// click, Escape/outside dismissal, focus movement into the menu, and the
|
||||||
|
/// menu entity's lifecycle.
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
pub(super) struct BaseDropdownButton {
|
||||||
|
id: ElementId,
|
||||||
|
style: StyleRefinement,
|
||||||
|
anchor: Anchor,
|
||||||
|
action: Option<AnyElement>,
|
||||||
|
caret: Option<CaretBuilder>,
|
||||||
|
menu: Option<MenuBuilder>,
|
||||||
|
}
|
||||||
|
|
||||||
|
type MenuBuilder =
|
||||||
|
Box<dyn Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static>;
|
||||||
|
type CaretBuilder = Box<dyn FnOnce(bool, &Window, &App) -> AnyElement>;
|
||||||
|
|
||||||
|
impl BaseDropdownButton {
|
||||||
|
pub(super) fn new(id: impl Into<ElementId>) -> Self {
|
||||||
|
Self {
|
||||||
|
id: id.into(),
|
||||||
|
style: StyleRefinement::default(),
|
||||||
|
anchor: Anchor::TopRight,
|
||||||
|
action: None,
|
||||||
|
caret: None,
|
||||||
|
menu: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The action half of the button. It keeps its own icon, label, tooltip
|
||||||
|
/// and click handler.
|
||||||
|
pub(super) fn action(mut self, action: impl IntoElement + 'static) -> Self {
|
||||||
|
self.action = Some(action.into_any_element());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The menu built by `builder` — the same signature as gpui-component's
|
||||||
|
/// `DropdownButton::dropdown_menu`, so existing menu code keeps working.
|
||||||
|
pub(super) fn dropdown_menu(
|
||||||
|
mut self,
|
||||||
|
builder: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.menu = Some(Box::new(builder));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Which corner of the caret the menu anchors to. Defaults to
|
||||||
|
/// [`Anchor::TopRight`], so the menu's right edge lines up with the
|
||||||
|
/// caret's.
|
||||||
|
#[allow(dead_code)] // API knob; current call sites use the default anchor.
|
||||||
|
pub(super) fn anchor(mut self, anchor: impl Into<Anchor>) -> Self {
|
||||||
|
self.anchor = anchor.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Styled for BaseDropdownButton {
|
||||||
|
fn style(&mut self) -> &mut StyleRefinement {
|
||||||
|
&mut self.style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Holds the [`PopupMenu`] entity of one popover between renders. Dismissal
|
||||||
|
/// drops it, so the menu is rebuilt with fresh items on the next open.
|
||||||
|
#[derive(Default)]
|
||||||
|
struct DropdownMenuState {
|
||||||
|
menu: Option<Entity<PopupMenu>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for BaseDropdownButton {
|
||||||
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
debug_assert!(
|
||||||
|
self.menu.is_some(),
|
||||||
|
"a BaseDropdownButton needs a `dropdown_menu`"
|
||||||
|
);
|
||||||
|
|
||||||
|
// The popover needs its own id: both the container and the popover register keyed state on this window.
|
||||||
|
let popover_id = SharedString::from(format!("{}-popover", self.id));
|
||||||
|
let anchor = self.anchor;
|
||||||
|
let menu_state =
|
||||||
|
window.use_keyed_state(popover_id.clone(), cx, |_, _| DropdownMenuState::default());
|
||||||
|
|
||||||
|
let caret = self.caret.unwrap_or_else(|| {
|
||||||
|
let id = popover_id.clone();
|
||||||
|
Box::new(move |is_open, _, cx| {
|
||||||
|
let caret = default_caret(id.clone(), cx);
|
||||||
|
let selected = caret.is_selected();
|
||||||
|
caret.selected(selected || is_open).into_any_element()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
h_flex()
|
||||||
|
.id(self.id)
|
||||||
|
.refine_style(&self.style)
|
||||||
|
.gap_0p5()
|
||||||
|
.when_some(self.action, |this, action| this.child(action))
|
||||||
|
.when_some(self.menu, |this, builder| {
|
||||||
|
this.child(
|
||||||
|
Popover::new(popover_id)
|
||||||
|
.anchor(anchor)
|
||||||
|
// The menu dismisses itself on outside click or Escape;
|
||||||
|
// the subscription below closes the popover along with it.
|
||||||
|
.overlay_closable(false)
|
||||||
|
.trigger_with(caret)
|
||||||
|
.content(
|
||||||
|
move |_, window, cx| match menu_state.read(cx).menu.clone() {
|
||||||
|
Some(menu) => menu,
|
||||||
|
None => {
|
||||||
|
let menu = PopupMenu::build(window, cx, |menu, window, cx| {
|
||||||
|
builder(menu, window, cx)
|
||||||
|
});
|
||||||
|
menu_state
|
||||||
|
.update(cx, |state, _| state.menu = Some(menu.clone()));
|
||||||
|
menu.focus_handle(cx).focus(window, cx);
|
||||||
|
|
||||||
|
let popover_state = cx.entity();
|
||||||
|
window
|
||||||
|
.subscribe(&menu, cx, {
|
||||||
|
let menu_state = menu_state.clone();
|
||||||
|
move |_, _: &DismissEvent, window, cx| {
|
||||||
|
popover_state.update(cx, |state, cx| {
|
||||||
|
state.dismiss(window, cx);
|
||||||
|
});
|
||||||
|
menu_state.update(cx, |state, _| {
|
||||||
|
state.menu = None;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
menu.clone()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The default caret: a chevron button the height of a medium button, tinted
|
||||||
|
/// by the theme, with hover and menu-open states.
|
||||||
|
fn default_caret(id: impl Into<ElementId>, cx: &App) -> BaseButton {
|
||||||
|
BaseButton::new(id)
|
||||||
|
.h(px(32.))
|
||||||
|
.px_1p5()
|
||||||
|
.text_color(cx.theme().muted_foreground)
|
||||||
|
.hover(|style| style.bg(cx.theme().secondary_hover))
|
||||||
|
.styles(|this| {
|
||||||
|
this.selected(|style| style.bg(cx.theme().secondary_active))
|
||||||
|
.disabled(|style| style.opacity(0.5))
|
||||||
|
})
|
||||||
|
.child(Icon::new(IconName::ChevronDown).xsmall())
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
pub(super) event_id: String,
|
||||||
|
/// NIP-34 coordinate `30617:<pubkey>:<repo-id>`.
|
||||||
|
pub(super) coordinate: String,
|
||||||
|
/// `https://gitworkshop.dev/<naddr>`
|
||||||
|
pub(super) gitworkshop: String,
|
||||||
|
/// `https://ditto.pub/<naddr>`
|
||||||
|
pub(super) 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, announcement.relays.iter().cloned())
|
||||||
|
.to_bech32()
|
||||||
|
.expect("a complete coordinate always encodes to naddr");
|
||||||
|
|
||||||
|
Self {
|
||||||
|
naddr: naddr.clone(),
|
||||||
|
event_id: announcement.event_id.to_bech32().unwrap(),
|
||||||
|
coordinate,
|
||||||
|
gitworkshop: format!("https://gitworkshop.dev/{naddr}"),
|
||||||
|
ditto: format!("https://ditto.pub/{naddr}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The share dropdown menu: one row per target, each showing a compact
|
||||||
|
/// label while the copy button (and row click) copy the full value.
|
||||||
|
pub(super) fn menu(&self, menu: PopupMenu) -> PopupMenu {
|
||||||
|
menu.min_w(px(340.))
|
||||||
|
.item(share_menu_row(
|
||||||
|
"copy-gitworkshop",
|
||||||
|
"GitWorkshop",
|
||||||
|
truncate_naddr_link(&self.gitworkshop, 4),
|
||||||
|
self.gitworkshop.clone(),
|
||||||
|
))
|
||||||
|
.item(share_menu_row(
|
||||||
|
"copy-ditto",
|
||||||
|
"Ditto",
|
||||||
|
truncate_naddr_link(&self.ditto, 4),
|
||||||
|
self.ditto.clone(),
|
||||||
|
))
|
||||||
|
.item(share_menu_row(
|
||||||
|
"copy-event-id",
|
||||||
|
"Event ID",
|
||||||
|
middle_truncate(&self.event_id, 10, 10),
|
||||||
|
self.event_id.clone(),
|
||||||
|
))
|
||||||
|
.item(share_menu_row(
|
||||||
|
"copy-coordinate",
|
||||||
|
"Coordinate",
|
||||||
|
middle_truncate(&self.coordinate, 10, 10),
|
||||||
|
self.coordinate.clone(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One row of the share menu: a small title on top of the compact label,
|
||||||
|
/// with a copy button that flips to a check while the value is on the
|
||||||
|
/// clipboard. Clicking the row text copies and dismisses the menu; the copy
|
||||||
|
/// button stops propagation, so the menu stays open for further copies.
|
||||||
|
/// Both copy `copy`, never the truncated label.
|
||||||
|
pub(super) fn share_menu_row(
|
||||||
|
id: &'static str,
|
||||||
|
title: &'static str,
|
||||||
|
label: String,
|
||||||
|
copy: String,
|
||||||
|
) -> PopupMenuItem {
|
||||||
|
let row_copy = copy.clone();
|
||||||
|
PopupMenuItem::element(move |_window, _cx| {
|
||||||
|
let button_copy = copy.clone();
|
||||||
|
h_flex()
|
||||||
|
.flex_1()
|
||||||
|
.gap_2()
|
||||||
|
.items_end()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.flex_1()
|
||||||
|
.gap_1()
|
||||||
|
.text_xs()
|
||||||
|
.child(div().flex_shrink_0().w_20().font_semibold().child(title))
|
||||||
|
.child(div().flex_1().text_ellipsis().child(label.clone())),
|
||||||
|
)
|
||||||
|
.child(Clipboard::new(id).tooltip("Copy").value(button_copy))
|
||||||
|
})
|
||||||
|
.on_click(move |_, _, cx| {
|
||||||
|
cx.write_to_clipboard(ClipboardItem::new_string(row_copy.clone()));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `[head chars]...[tail chars]` middle truncation; the value is left alone
|
||||||
|
/// when it is too short for the ellipsis to save space.
|
||||||
|
pub(super) fn middle_truncate(value: &str, head: usize, tail: usize) -> String {
|
||||||
|
let len = value.chars().count();
|
||||||
|
if len <= head + tail + 3 {
|
||||||
|
return value.to_string();
|
||||||
|
}
|
||||||
|
let head: String = value.chars().take(head).collect();
|
||||||
|
let tail: String = value.chars().skip(len - tail).collect();
|
||||||
|
format!("{head}...{tail}")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shorten an naddr link to `<url>/naddr1...[last tail chars]`, e.g.
|
||||||
|
/// `https://gitworkshop.dev/naddr1...abcd`. Only the label is shortened;
|
||||||
|
/// the value to be copied stays the full URL.
|
||||||
|
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..])
|
||||||
|
}
|
||||||
|
|
||||||
/// Width of one line-number gutter in a diff row.
|
/// Width of one line-number gutter in a diff row.
|
||||||
pub(super) const GUTTER_WIDTH: f32 = 44.;
|
pub(super) const GUTTER_WIDTH: f32 = 44.;
|
||||||
/// Height of one row in a virtual diff list.
|
/// Height of one row in a virtual diff list.
|
||||||
@@ -482,4 +766,52 @@ mod tests {
|
|||||||
assert_eq!(code_language("LICENSE"), None);
|
assert_eq!(code_language("LICENSE"), None);
|
||||||
assert_eq!(code_language("README.md"), None);
|
assert_eq!(code_language("README.md"), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn middle_truncates_long_values_only() {
|
||||||
|
assert_eq!(
|
||||||
|
middle_truncate(
|
||||||
|
"a008def15796fba9a0d6fab04e8fd57089285d9fd505da5a83fe8aad57a3564d",
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
),
|
||||||
|
"a008def157...ad57a3564d"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
middle_truncate(
|
||||||
|
"30617:a008def15796fba9a0d6fab04e8fd57089285d9fd505da5a83fe8aad57a3564d:ngit",
|
||||||
|
10,
|
||||||
|
10
|
||||||
|
),
|
||||||
|
"30617:a008...3564d:ngit"
|
||||||
|
);
|
||||||
|
// Too short to save space with the ellipsis: left alone.
|
||||||
|
assert_eq!(middle_truncate("short", 10, 10), "short");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn naddr_link_keeps_url_and_tail() {
|
||||||
|
assert_eq!(
|
||||||
|
truncate_naddr_link("https://gitworkshop.dev/naddr1qqqxyzabc1234", 4),
|
||||||
|
"https://gitworkshop.dev/naddr1...1234"
|
||||||
|
);
|
||||||
|
// No naddr1 prefix: unchanged.
|
||||||
|
assert_eq!(
|
||||||
|
truncate_naddr_link("https://example.com/x", 4),
|
||||||
|
"https://example.com/x"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn base_dropdown_button_builder_state() {
|
||||||
|
let button = BaseDropdownButton::new("issues")
|
||||||
|
.action(div())
|
||||||
|
.anchor(Anchor::BottomLeft)
|
||||||
|
.dropdown_menu(|menu, _, _| menu);
|
||||||
|
|
||||||
|
assert!(button.action.is_some());
|
||||||
|
assert!(button.caret.is_some());
|
||||||
|
assert!(button.menu.is_some());
|
||||||
|
assert_eq!(button.anchor, Anchor::BottomLeft);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
//! Issues panel: a bottom panel listing every issue of the repository with
|
|
||||||
//! its title, event id, author, age and status, filterable by status via
|
|
||||||
//! the header's All/Open/Closed filter.
|
|
||||||
|
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use assets::CustomIconName;
|
use assets::CustomIconName;
|
||||||
@@ -43,8 +39,8 @@ enum IssueFilter {
|
|||||||
All,
|
All,
|
||||||
/// Issues whose resolved status is [`RepoStatus::Open`].
|
/// Issues whose resolved status is [`RepoStatus::Open`].
|
||||||
Open,
|
Open,
|
||||||
/// Issues whose resolved status is [`RepoStatus::Closed`] or
|
/// Issues whose resolved status is
|
||||||
/// [`RepoStatus::Applied`] (both are "done" states).
|
/// [`RepoStatus::Closed`] or [`RepoStatus::Applied`] (both are "done" states).
|
||||||
Closed,
|
Closed,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,8 +70,7 @@ pub struct IssuesView {
|
|||||||
filter: IssueFilter,
|
filter: IssueFilter,
|
||||||
/// Per-row heights of the virtual list.
|
/// Per-row heights of the virtual list.
|
||||||
item_sizes: Rc<Vec<Size<Pixels>>>,
|
item_sizes: Rc<Vec<Size<Pixels>>>,
|
||||||
/// Number of rows [`Self::item_sizes`] was built for (the filtered
|
/// Number of rows [`Self::item_sizes`] was built for (the filtered issue count).
|
||||||
/// issue count); rebuilt on change.
|
|
||||||
issue_len: usize,
|
issue_len: usize,
|
||||||
/// Indices into the store's `issues` matching [`Self::filter`], rebuilt
|
/// Indices into the store's `issues` matching [`Self::filter`], rebuilt
|
||||||
/// every render; the virtual list renders this slice.
|
/// every render; the virtual list renders this slice.
|
||||||
@@ -350,7 +345,7 @@ impl IssuesView {
|
|||||||
|
|
||||||
/// Open the "new issue" dialog: a title and a content input that submit
|
/// Open the "new issue" dialog: a title and a content input that submit
|
||||||
/// through [`RepoStore::open_issue`] when confirmed.
|
/// through [`RepoStore::open_issue`] when confirmed.
|
||||||
fn open_new_issue_dialog(store: Entity<RepoStore>, window: &mut Window, cx: &mut App) {
|
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 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…"));
|
||||||
|
|
||||||
|
|||||||
@@ -8,20 +8,20 @@ use dock::{BasePanel, DockArea, DockPlacement, Panel, PanelEvent, panel_handle};
|
|||||||
use gix::Repository;
|
use gix::Repository;
|
||||||
use gpui::prelude::*;
|
use gpui::prelude::*;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, PathPromptOptions,
|
Action, AnyElement, App, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||||
Pixels, Render, SharedString, Size, Subscription, Task, WeakEntity, Window, div, px, size,
|
PathPromptOptions, Pixels, Render, SharedString, Size, Subscription, Task, WeakEntity, Window,
|
||||||
|
div, px, relative, size,
|
||||||
};
|
};
|
||||||
use gpui_base::Disableable;
|
use gpui_base::{Button as BaseButton, Disableable};
|
||||||
use gpui_component::avatar::{Avatar, AvatarGroup};
|
use gpui_component::avatar::Avatar;
|
||||||
use gpui_component::button::{Button, ButtonVariants};
|
use gpui_component::button::{Button, ButtonVariants};
|
||||||
use gpui_component::combobox::{
|
use gpui_component::combobox::{
|
||||||
Caret, Combobox, ComboboxEvent, ComboboxState, ComboboxTriggerContext,
|
Caret, Combobox, ComboboxEvent, ComboboxState, ComboboxTriggerContext,
|
||||||
};
|
};
|
||||||
use gpui_component::searchable_list::SearchableVec;
|
use gpui_component::searchable_list::SearchableVec;
|
||||||
use gpui_component::tag::Tag;
|
|
||||||
use gpui_component::tree::TreeState;
|
use gpui_component::tree::TreeState;
|
||||||
use gpui_component::{
|
use gpui_component::{
|
||||||
ActiveTheme, Icon, IconName, Selectable, Sizable, StyledExt, VirtualListScrollHandle, h_flex,
|
ActiveTheme, Colorize, Icon, IconName, Sizable, StyledExt, VirtualListScrollHandle, h_flex,
|
||||||
v_flex,
|
v_flex,
|
||||||
};
|
};
|
||||||
use signed_core::Announcement;
|
use signed_core::Announcement;
|
||||||
@@ -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,15 +41,18 @@ 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,
|
||||||
};
|
};
|
||||||
use commits::COMMIT_ROW_HEIGHT;
|
use commits::COMMIT_ROW_HEIGHT;
|
||||||
use diff::CommitDiffView;
|
use diff::CommitDiffView;
|
||||||
use helpers::{TreeItemSeed, build_tree_items, is_markdown_path, tree_items};
|
use helpers::{
|
||||||
use issues::IssuesView;
|
BaseDropdownButton, ShareTargets, TreeItemSeed, build_tree_items, is_markdown_path, tree_items,
|
||||||
use pull_requests::PullRequestsView;
|
};
|
||||||
|
use issues::{IssuesView, open_new_issue_dialog};
|
||||||
|
use pull_requests::{PullRequestsView, open_new_pull_request_dialog};
|
||||||
|
|
||||||
/// What kind of ref the header selectors switch to.
|
/// What kind of ref the header selectors switch to.
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -58,6 +63,16 @@ enum RefKind {
|
|||||||
Tag,
|
Tag,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Header actions dispatched by the dropdown menus of the header buttons.
|
||||||
|
#[derive(Clone, Action, PartialEq, Eq)]
|
||||||
|
#[action(namespace = repo_detail, no_json)]
|
||||||
|
enum RepoAction {
|
||||||
|
/// Open the "new issue" dialog.
|
||||||
|
NewIssue,
|
||||||
|
/// Open the "new pull request" dialog.
|
||||||
|
NewPR,
|
||||||
|
}
|
||||||
|
|
||||||
/// Everything loaded from the local clone for the explorer: the tree seeds,
|
/// Everything loaded from the local clone for the explorer: the tree seeds,
|
||||||
/// README, refs and HEAD commit. Computed on a background thread (see
|
/// README, refs and HEAD commit. Computed on a background thread (see
|
||||||
/// [`load_repo_data`]) and applied on the main thread.
|
/// [`load_repo_data`]) and applied on the main thread.
|
||||||
@@ -1029,16 +1044,28 @@ impl RepoDetailView {
|
|||||||
fn render_header(&self, cx: &mut Context<Self>) -> AnyElement {
|
fn render_header(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
let store = self.store.read(cx);
|
let store = self.store.read(cx);
|
||||||
let announcement = store.announcement.as_ref().unwrap_or(&self.initial);
|
let announcement = store.announcement.as_ref().unwrap_or(&self.initial);
|
||||||
let issue_count = store.issue_count();
|
let issue_count = SharedString::from(store.issue_count().to_string());
|
||||||
let pull_request_count = store.pull_request_count();
|
let pr_count = SharedString::from(store.pull_request_count().to_string());
|
||||||
|
|
||||||
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 commits_count = self.all_commits.as_ref().map(|list| list.total);
|
let commits_count = self.all_commits.as_ref().map(|list| list.total);
|
||||||
let worktree_empty = self.switching_ref || self.worktree.is_none();
|
let worktree_empty = self.switching_ref || self.worktree.is_none();
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
|
.on_action(
|
||||||
|
cx.listener(|this, action: &RepoAction, window, cx| match action {
|
||||||
|
RepoAction::NewIssue => {
|
||||||
|
open_new_issue_dialog(this.store.clone(), window, cx);
|
||||||
|
}
|
||||||
|
RepoAction::NewPR => {
|
||||||
|
open_new_pull_request_dialog(this.store.clone(), window, cx);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
.px_4()
|
.px_4()
|
||||||
.pb_4()
|
.pb_4()
|
||||||
.w_full()
|
.w_full()
|
||||||
@@ -1048,20 +1075,29 @@ impl RepoDetailView {
|
|||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.w_full()
|
.w_full()
|
||||||
.gap_2()
|
.gap_4()
|
||||||
.items_start()
|
.items_start()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.min_w_0()
|
.min_w_0()
|
||||||
.child(div().font_semibold().child(name))
|
.gap_1()
|
||||||
|
.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.25))
|
||||||
.text_ellipsis()
|
.text_ellipsis()
|
||||||
.child(description),
|
.child(description),
|
||||||
)
|
)
|
||||||
@@ -1069,7 +1105,7 @@ impl RepoDetailView {
|
|||||||
h_flex()
|
h_flex()
|
||||||
.mt_2()
|
.mt_2()
|
||||||
.w_full()
|
.w_full()
|
||||||
.gap_2()
|
.gap_0p5()
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
@@ -1086,42 +1122,115 @@ impl RepoDetailView {
|
|||||||
.gap_2()
|
.gap_2()
|
||||||
.justify_end()
|
.justify_end()
|
||||||
.child(
|
.child(
|
||||||
Button::new("issues")
|
BaseDropdownButton::new("issues")
|
||||||
.child(
|
.action(
|
||||||
h_flex()
|
BaseButton::new("issues-open")
|
||||||
.gap_2()
|
.child(
|
||||||
.text_sm()
|
h_flex()
|
||||||
.child(SharedString::from("Issues"))
|
.h_8()
|
||||||
.child(Tag::new().xsmall().child(SharedString::from(
|
.px_2()
|
||||||
issue_count.to_string(),
|
.gap_1()
|
||||||
))),
|
.rounded(cx.theme().radius)
|
||||||
|
.bg(cx.theme().secondary)
|
||||||
|
.hover(|this| {
|
||||||
|
this.bg(cx.theme().secondary_hover)
|
||||||
|
})
|
||||||
|
.text_sm()
|
||||||
|
.text_color(cx.theme().secondary_foreground)
|
||||||
|
.child(Icon::new(CustomIconName::GitIssueDone))
|
||||||
|
.child("Issues")
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.mx_1()
|
||||||
|
.h_5()
|
||||||
|
.w_px()
|
||||||
|
.bg(cx.theme().border.darken(0.1)),
|
||||||
|
)
|
||||||
|
.child(issue_count),
|
||||||
|
)
|
||||||
|
.on_click(cx.listener(|this, _event, window, cx| {
|
||||||
|
this.open_issue_detail(window, cx);
|
||||||
|
})),
|
||||||
)
|
)
|
||||||
.outline()
|
.dropdown_menu(|menu, _, _| {
|
||||||
.on_click(cx.listener(|this, _event, window, cx| {
|
menu.menu_element_with_icon(
|
||||||
this.open_issue_detail(window, cx);
|
IconName::Plus,
|
||||||
})),
|
Box::new(RepoAction::NewIssue),
|
||||||
|
|_, _| div().text_xs().child("New issue"),
|
||||||
|
)
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("prs")
|
BaseDropdownButton::new("prs")
|
||||||
.child(
|
.action(
|
||||||
h_flex()
|
BaseButton::new("prs-open")
|
||||||
.gap_2()
|
.child(
|
||||||
.text_sm()
|
h_flex()
|
||||||
.child(SharedString::from("Pull Requests"))
|
.h_8()
|
||||||
.child(Tag::new().xsmall().child(SharedString::from(
|
.px_2()
|
||||||
pull_request_count.to_string(),
|
.gap_1()
|
||||||
))),
|
.rounded(cx.theme().radius)
|
||||||
|
.bg(cx.theme().secondary)
|
||||||
|
.hover(|this| {
|
||||||
|
this.bg(cx.theme().secondary_hover)
|
||||||
|
})
|
||||||
|
.text_sm()
|
||||||
|
.text_color(cx.theme().secondary_foreground)
|
||||||
|
.child(Icon::new(
|
||||||
|
CustomIconName::GitPullRequest,
|
||||||
|
))
|
||||||
|
.child("Pull Requests")
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.mx_1()
|
||||||
|
.h_5()
|
||||||
|
.w_px()
|
||||||
|
.bg(cx.theme().border.darken(0.1)),
|
||||||
|
)
|
||||||
|
.child(pr_count),
|
||||||
|
)
|
||||||
|
.on_click(cx.listener(|this, _event, window, cx| {
|
||||||
|
this.open_pull_request_detail(window, cx);
|
||||||
|
})),
|
||||||
)
|
)
|
||||||
.outline()
|
.dropdown_menu(|menu, _, _| {
|
||||||
.on_click(cx.listener(|this, _event, window, cx| {
|
menu.menu_element_with_icon(
|
||||||
this.open_pull_request_detail(window, cx);
|
IconName::Plus,
|
||||||
})),
|
Box::new(RepoAction::NewPR),
|
||||||
|
|_, _| div().text_xs().child("New PR"),
|
||||||
|
)
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("link")
|
BaseDropdownButton::new("share")
|
||||||
.icon(IconName::ExternalLink)
|
.action(
|
||||||
.tooltip("Open in gitworkshop.dev")
|
Button::new("link")
|
||||||
.secondary(),
|
.icon(IconName::Copy)
|
||||||
|
.tooltip("Copy ID")
|
||||||
|
.secondary()
|
||||||
|
.on_click({
|
||||||
|
let naddr = share.naddr.clone();
|
||||||
|
move |_, _, cx| {
|
||||||
|
cx.write_to_clipboard(
|
||||||
|
ClipboardItem::new_string(naddr.clone()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.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")
|
||||||
@@ -1141,27 +1250,69 @@ impl RepoDetailView {
|
|||||||
.items_center()
|
.items_center()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(
|
.child(
|
||||||
Button::new("files-tab")
|
BaseButton::new("files-tab")
|
||||||
.label("Files")
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.h_8()
|
||||||
|
.px_2()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.text_sm()
|
||||||
|
.child(Icon::new(CustomIconName::GitFile).small())
|
||||||
|
.child("Files"),
|
||||||
|
)
|
||||||
|
.text_color(cx.theme().button_foreground)
|
||||||
|
.rounded(cx.theme().radius)
|
||||||
|
.hover(|this| this.bg(cx.theme().button_hover))
|
||||||
|
.active(|this| this.bg(cx.theme().button_active))
|
||||||
.selected(self.active_tab == 0)
|
.selected(self.active_tab == 0)
|
||||||
.toggled(self.active_tab == 0)
|
.when(self.active_tab == 0, |this| {
|
||||||
|
this.bg(cx.theme().button_active)
|
||||||
|
})
|
||||||
.on_click(cx.listener(|this, _event, _window, cx| {
|
.on_click(cx.listener(|this, _event, _window, cx| {
|
||||||
this.active_tab = 0;
|
this.active_tab = 0;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("commits-tab")
|
BaseButton::new("commits-tab")
|
||||||
.label("Commits")
|
.flex()
|
||||||
.selected(self.active_tab == 1)
|
.items_center()
|
||||||
.toggled(self.active_tab == 1)
|
.h_8()
|
||||||
|
.px_2()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.text_sm()
|
||||||
|
.child(Icon::new(CustomIconName::GitCommit).small())
|
||||||
|
.child("Commits"),
|
||||||
|
)
|
||||||
.when_some(commits_count, |this, count| {
|
.when_some(commits_count, |this, count| {
|
||||||
this.child(
|
this.child(
|
||||||
Tag::secondary()
|
h_flex()
|
||||||
.xsmall()
|
.justify_center()
|
||||||
|
.px_1()
|
||||||
|
.py_0p5()
|
||||||
|
.min_w_4()
|
||||||
|
.text_size(px(8.))
|
||||||
|
.bg(cx.theme().muted)
|
||||||
|
.text_color(cx.theme().muted_foreground)
|
||||||
|
.rounded(cx.theme().radius)
|
||||||
|
.line_height(relative(1.))
|
||||||
.child(SharedString::from(count.to_string())),
|
.child(SharedString::from(count.to_string())),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
.text_color(cx.theme().button_foreground)
|
||||||
|
.rounded(cx.theme().radius)
|
||||||
|
.hover(|this| this.bg(cx.theme().button_hover))
|
||||||
|
.active(|this| this.bg(cx.theme().button_active))
|
||||||
|
.selected(self.active_tab == 1)
|
||||||
|
.when(self.active_tab == 1, |this| {
|
||||||
|
this.bg(cx.theme().button_active)
|
||||||
|
})
|
||||||
.on_click(cx.listener(|this, _event, _window, cx| {
|
.on_click(cx.listener(|this, _event, _window, cx| {
|
||||||
this.active_tab = 1;
|
this.active_tab = 1;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -1243,8 +1394,6 @@ impl RepoDetailView {
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Horizontal list of everyone who maintains the repository: the owner
|
|
||||||
/// shown in full, and any additional maintainers as a compact overlapping avatar group.
|
|
||||||
fn render_maintainers(&self, cx: &mut Context<Self>) -> AnyElement {
|
fn render_maintainers(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
let announcement = self.announcement(cx);
|
let announcement = self.announcement(cx);
|
||||||
let profile_store = ProfileStore::global(cx);
|
let profile_store = ProfileStore::global(cx);
|
||||||
@@ -1254,7 +1403,7 @@ impl RepoDetailView {
|
|||||||
.maintainers
|
.maintainers
|
||||||
.iter()
|
.iter()
|
||||||
.copied()
|
.copied()
|
||||||
.filter(|key| *key != announcement.owner && seen.insert(*key))
|
.filter(|key| key != &announcement.owner && seen.insert(*key))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let owner = profile_store.read(cx).get(&announcement.owner);
|
let owner = profile_store.read(cx).get(&announcement.owner);
|
||||||
@@ -1264,31 +1413,32 @@ impl RepoDetailView {
|
|||||||
h_flex()
|
h_flex()
|
||||||
.w_full()
|
.w_full()
|
||||||
.gap_3()
|
.gap_3()
|
||||||
.items_center()
|
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
Button::new("maintainers").compact().ghost().child(
|
||||||
.gap_1()
|
h_flex()
|
||||||
.items_center()
|
.gap_2()
|
||||||
.child(
|
.child(
|
||||||
Avatar::new()
|
h_flex()
|
||||||
.name(owner_name.clone())
|
.gap_1()
|
||||||
.when_some(owner_picture, |this, url| this.src(url))
|
.child(
|
||||||
.rounded(cx.theme().radius)
|
Avatar::new()
|
||||||
.small(),
|
.name(owner_name.clone())
|
||||||
)
|
.when_some(owner_picture, |this, url| this.src(url))
|
||||||
.child(div().text_xs().whitespace_nowrap().child(owner_name)),
|
.rounded(cx.theme().radius)
|
||||||
|
.small(),
|
||||||
|
)
|
||||||
|
.child(div().text_xs().whitespace_nowrap().child(owner_name)),
|
||||||
|
)
|
||||||
|
.when(!rest.is_empty(), |this| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().muted_foreground)
|
||||||
|
.child(SharedString::from(format!("+{}", rest.len()))),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.when(!rest.is_empty(), |this| {
|
|
||||||
this.child(AvatarGroup::new().small().limit(5).ellipsis().children(
|
|
||||||
rest.into_iter().map(|key| {
|
|
||||||
let profile = profile_store.read(cx).get(&key);
|
|
||||||
Avatar::new()
|
|
||||||
.name(profile.name())
|
|
||||||
.when_some(profile.picture(), |this, url| this.src(url))
|
|
||||||
.rounded(cx.theme().radius)
|
|
||||||
}),
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -435,7 +435,11 @@ impl PullRequestsView {
|
|||||||
/// Open the "new pull request" dialog: a title, an optional description and
|
/// Open the "new pull request" dialog: a title, an optional description and
|
||||||
/// a patch input that submit through [`RepoStore::open_pull_request`] when
|
/// a patch input that submit through [`RepoStore::open_pull_request`] when
|
||||||
/// confirmed.
|
/// confirmed.
|
||||||
fn open_new_pull_request_dialog(store: Entity<RepoStore>, window: &mut Window, cx: &mut App) {
|
pub(super) fn open_new_pull_request_dialog(
|
||||||
|
store: Entity<RepoStore>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
let subject = cx.new(|cx| InputState::new(window, cx).placeholder("Pull request title"));
|
let subject = cx.new(|cx| InputState::new(window, cx).placeholder("Pull request title"));
|
||||||
let description =
|
let description =
|
||||||
cx.new(|cx| TextareaState::new(window, cx).placeholder("Describe the change…"));
|
cx.new(|cx| TextareaState::new(window, cx).placeholder("Describe the change…"));
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -32,21 +32,21 @@ fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let light_theme = registry.themes().get("Signed Light").cloned();
|
//let light_theme = registry.themes().get("Signed Light").cloned();
|
||||||
let dark_theme = registry.themes().get("Signed Dark").cloned();
|
//let dark_theme = registry.themes().get("Signed Dark").cloned();
|
||||||
let theme = Theme::global_mut(cx);
|
let theme = Theme::global_mut(cx);
|
||||||
theme.radius = px(2.);
|
theme.radius = px(2.);
|
||||||
theme.radius_lg = px(6.);
|
theme.radius_lg = px(6.);
|
||||||
theme.focus_ring = false;
|
theme.focus_ring = false;
|
||||||
theme.shadow = false;
|
theme.shadow = false;
|
||||||
|
|
||||||
if let Some(light) = light_theme {
|
//if let Some(light) = light_theme {
|
||||||
theme.light_theme = light;
|
// theme.light_theme = light;
|
||||||
}
|
//}
|
||||||
|
|
||||||
if let Some(dark) = dark_theme {
|
//if let Some(dark) = dark_theme {
|
||||||
theme.dark_theme = dark;
|
// theme.dark_theme = dark;
|
||||||
}
|
//}
|
||||||
|
|
||||||
// Sync the theme with the system appearance
|
// Sync the theme with the system appearance
|
||||||
Theme::sync_system_appearance(None, cx);
|
Theme::sync_system_appearance(None, cx);
|
||||||
|
|||||||