mirror of
https://seed.radicle.dev/rad:z4D5UCArafTzTQpDZNQRuqswh3ury.git
synced 2026-10-18 19:10:45 +08:00
Implement repo seeding
This commit is contained in:
@@ -129,3 +129,18 @@ pub(crate) async fn create_repo(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn seed(ctx: tauri::State<AppState>, rid: RepoId) -> Result<(), Error> {
|
||||
ctx.seed(rid)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn unseed(ctx: tauri::State<AppState>, rid: RepoId) -> Result<(), Error> {
|
||||
ctx.unseed(rid)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn seeded_not_replicated(ctx: tauri::State<AppState>) -> Result<Vec<RepoId>, Error> {
|
||||
ctx.seeded_not_replicated()
|
||||
}
|
||||
|
||||
@@ -57,6 +57,9 @@ pub fn run() {
|
||||
repo::repo_by_id,
|
||||
repo::repo_count,
|
||||
repo::repo_readme,
|
||||
repo::seed,
|
||||
repo::seeded_not_replicated,
|
||||
repo::unseed,
|
||||
startup::startup,
|
||||
startup::version,
|
||||
startup::check_radicle_cli,
|
||||
|
||||
@@ -8,7 +8,7 @@ use radicle::node::routing::Store;
|
||||
use radicle::patch::cache::Patches as _;
|
||||
use radicle::storage;
|
||||
use radicle::storage::{ReadRepository, ReadStorage, RepositoryInfo};
|
||||
use radicle::{git, identity};
|
||||
use radicle::{git, identity, node};
|
||||
|
||||
use crate::cobs;
|
||||
use crate::diff;
|
||||
@@ -295,4 +295,35 @@ pub trait Repo: Profile {
|
||||
|
||||
Ok(commits)
|
||||
}
|
||||
|
||||
fn unseed(&self, rid: identity::RepoId) -> Result<(), Error> {
|
||||
let profile = self.profile();
|
||||
let mut node = radicle::Node::new(profile.socket());
|
||||
|
||||
profile.unseed(rid, &mut node)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn seed(&self, rid: identity::RepoId) -> Result<(), Error> {
|
||||
let profile = self.profile();
|
||||
let mut node = radicle::Node::new(profile.socket());
|
||||
|
||||
profile.seed(rid, node::policy::Scope::All, &mut node)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn seeded_not_replicated(&self) -> Result<Vec<identity::RepoId>, Error> {
|
||||
let profile = &self.profile();
|
||||
let storage = &profile.storage;
|
||||
let policies = profile.policies()?;
|
||||
let entries = policies
|
||||
.seed_policies()?
|
||||
.filter(|policy| !storage.contains(&policy.rid).unwrap_or(false))
|
||||
.map(|policy| policy.rid)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
<script lang="ts" module>
|
||||
export const addRepoPopoverToggleId = "add-repo-popover-toggle";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { RepoInfo } from "@bindings/repo/RepoInfo";
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import useLocalStorage from "@app/lib/useLocalStorage.svelte";
|
||||
import { announce } from "@app/components/AnnounceSwitch.svelte";
|
||||
import { closeFocused } from "./Popover.svelte";
|
||||
import { invoke } from "@app/lib/invoke";
|
||||
import { nodeRunning } from "@app/lib/events";
|
||||
import { parseRepositoryId, twemoji } from "@app/lib/utils";
|
||||
|
||||
import Border from "./Border.svelte";
|
||||
import Button from "./Button.svelte";
|
||||
import Command from "./Command.svelte";
|
||||
import ExternalLink from "./ExternalLink.svelte";
|
||||
import Icon from "./Icon.svelte";
|
||||
import Popover from "./Popover.svelte";
|
||||
import Tab from "./Tab.svelte";
|
||||
import TextInput from "./TextInput.svelte";
|
||||
|
||||
interface Props {
|
||||
onOpen: () => void;
|
||||
reload: () => Promise<void>;
|
||||
repos: RepoInfo[];
|
||||
seededNotReplicated: string[];
|
||||
}
|
||||
|
||||
const { onOpen, reload, repos, seededNotReplicated }: Props = $props();
|
||||
|
||||
let popoverExpanded: boolean = $state(false);
|
||||
let rid = $state("");
|
||||
let validationMessage: string | undefined = $state(undefined);
|
||||
|
||||
// Clear validation message when changing the input.
|
||||
$effect(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
rid;
|
||||
|
||||
validationMessage = undefined;
|
||||
});
|
||||
|
||||
// Clear input when the popover is closed.
|
||||
$effect(() => {
|
||||
if (!popoverExpanded) {
|
||||
rid = "";
|
||||
}
|
||||
});
|
||||
|
||||
const tab = useLocalStorage(
|
||||
"addRepoPopoverSelectedTab",
|
||||
z.union([z.literal("seed"), z.literal("publish")]),
|
||||
"seed",
|
||||
!window.localStorage,
|
||||
);
|
||||
|
||||
async function submit() {
|
||||
const trimmedRid = rid.trim();
|
||||
|
||||
if (trimmedRid === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
validationMessage = validate(trimmedRid);
|
||||
|
||||
if (validationMessage === undefined) {
|
||||
await seed(trimmedRid);
|
||||
await reload();
|
||||
rid = "";
|
||||
closeFocused();
|
||||
}
|
||||
}
|
||||
|
||||
async function seed(rid: string) {
|
||||
try {
|
||||
await invoke<null>("seed", {
|
||||
rid: rid,
|
||||
opts: { announce: $nodeRunning && $announce },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Seeding failed", error);
|
||||
}
|
||||
}
|
||||
|
||||
function validate(rid: string): string | undefined {
|
||||
const parsedRid = parseRepositoryId(rid);
|
||||
if (parsedRid === undefined) {
|
||||
return "RID is not valid";
|
||||
}
|
||||
|
||||
if (seededNotReplicated.includes(rid)) {
|
||||
return "This repo is already queued for fetching";
|
||||
}
|
||||
if (repos.map(r => r.rid).includes(rid)) {
|
||||
return "This repo is already seeded";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
li {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<Popover
|
||||
popoverPositionRight="0"
|
||||
popoverPositionTop="3rem"
|
||||
bind:expanded={popoverExpanded}>
|
||||
{#snippet toggle(onclick)}
|
||||
<Button
|
||||
id={addRepoPopoverToggleId}
|
||||
styleHeight="2.5rem"
|
||||
variant="secondary"
|
||||
onclick={() => {
|
||||
onOpen();
|
||||
onclick();
|
||||
}}
|
||||
active={popoverExpanded}>
|
||||
<Icon name="add" />Add repo
|
||||
</Button>
|
||||
{/snippet}
|
||||
|
||||
{#snippet popover()}
|
||||
<Border
|
||||
stylePosition="relative"
|
||||
variant="ghost"
|
||||
flatBottom
|
||||
styleDisplay="flex"
|
||||
styleWidth="100%"
|
||||
styleGap="1rem"
|
||||
styleMinWidth="27rem"
|
||||
stylePadding="0 1rem">
|
||||
<Tab
|
||||
active={tab.value === "seed"}
|
||||
onclick={() => {
|
||||
tab.value = "seed";
|
||||
}}>
|
||||
Seed a repo
|
||||
</Tab>
|
||||
<Tab
|
||||
active={tab.value === "publish"}
|
||||
onclick={() => {
|
||||
tab.value = "publish";
|
||||
}}>
|
||||
Publish existing repo
|
||||
</Tab>
|
||||
</Border>
|
||||
|
||||
<div style:margin-top="-2px">
|
||||
<Border
|
||||
variant="ghost"
|
||||
flatTop
|
||||
stylePadding="1rem"
|
||||
styleDisplay="block"
|
||||
styleFlexDirection="column"
|
||||
styleAlignItems="flex-start">
|
||||
<div class="txt-small" style:line-height="1.625rem">
|
||||
{#if tab.value === "seed"}
|
||||
<!-- prettier-ignore -->
|
||||
<div style:margin-bottom="1rem">
|
||||
You can search for Radicle repos by name or description at
|
||||
<ExternalLink href="https://search.radicle.xyz">
|
||||
search.radicle.xyz
|
||||
</ExternalLink>.
|
||||
</div>
|
||||
<div style:width="100%">
|
||||
<div class="txt-semibold" style:margin-bottom="0.5rem"></div>
|
||||
<div
|
||||
class="global-flex"
|
||||
style:flex-direction="column"
|
||||
style:align-items="flex-start"
|
||||
style:gap="1rem">
|
||||
<div style:width="100%">
|
||||
<div class="global-flex" style:width="100%">
|
||||
<TextInput
|
||||
autofocus
|
||||
valid={validationMessage === undefined}
|
||||
bind:value={rid}
|
||||
onSubmit={submit}
|
||||
placeholder="RID, e.g. rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
styleHeight="2.5rem"
|
||||
onclick={submit}
|
||||
disabled={rid.trim() === ""}>
|
||||
Seed
|
||||
</Button>
|
||||
</div>
|
||||
{#if validationMessage}
|
||||
<div
|
||||
class="txt-small global-flex"
|
||||
style:color="var(--color-foreground-red)"
|
||||
style:padding="0.25rem 0 0 0.25rem"
|
||||
style:gap="0.25rem">
|
||||
<Icon name="warning" />
|
||||
{validationMessage}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="global-flex txt-missing"
|
||||
style:align-items="flex-start"
|
||||
style:margin-top="2rem">
|
||||
<span style:margin-top="0.25rem">
|
||||
<Icon name="info" />
|
||||
</span>
|
||||
By seeding a repository, your node fetches it from the network, allowing
|
||||
you to interact with it locally while also making it available to others.
|
||||
</div>
|
||||
{#if !$nodeRunning}
|
||||
<div
|
||||
class="global-flex txt-missing"
|
||||
style:align-items="flex-start"
|
||||
style:margin-top="1rem">
|
||||
<span style:margin-top="0.25rem">
|
||||
<Icon name="bulb" />
|
||||
</span>
|
||||
|
||||
<div>
|
||||
Your node is
|
||||
<span class="txt-semibold">
|
||||
<span
|
||||
style:display="inline-block"
|
||||
style:vertical-align="text-top">
|
||||
<Icon name="offline" />
|
||||
</span>
|
||||
Offline.
|
||||
</span>
|
||||
You can still add repos, but they will only be fetched once your
|
||||
node is back online.
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if tab.value === "publish"}
|
||||
<p style="margin: 0 0 1rem 0">
|
||||
Navigate to an existing Git repo in your terminal
|
||||
<code
|
||||
style:white-space="nowrap"
|
||||
style:padding="0.125rem 0.25rem"
|
||||
style:background-color="var(--color-fill-ghost)">
|
||||
cd path/to/your/repo
|
||||
</code>
|
||||
and run the following command:
|
||||
</p>
|
||||
|
||||
<Command styleWidth="fit-content" command="rad init" />
|
||||
|
||||
<p style="margin: 1rem 0 0 0">
|
||||
Follow the setup prompts to initialize the repo and publish it on
|
||||
the Radicle network:
|
||||
</p>
|
||||
|
||||
<ul style:padding="0 1rem">
|
||||
<li>
|
||||
<strong>Repository Name:</strong>
|
||||
The name of your repo.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Description:</strong>
|
||||
A brief summary of what your repo does.
|
||||
</li>
|
||||
<!-- prettier-ignore -->
|
||||
<li>
|
||||
<strong>Default Branch:</strong>
|
||||
Typically
|
||||
<strong>main</strong>
|
||||
or
|
||||
<strong>master</strong>.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Visibility:</strong>
|
||||
Choose
|
||||
<strong>public</strong>
|
||||
to share with others or
|
||||
<strong>private</strong>
|
||||
to not publish it to the network yet.
|
||||
</li>
|
||||
</ul>
|
||||
<p use:twemoji style:margin="2rem 0 0 0">
|
||||
That's it! Your repo is now on the Radicle network. 🚀
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</Border>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Popover>
|
||||
@@ -2,6 +2,7 @@
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
interface Props {
|
||||
id?: string;
|
||||
children: Snippet;
|
||||
variant: "primary" | "secondary" | "ghost" | "success" | "danger";
|
||||
onclick?: () => void;
|
||||
@@ -14,6 +15,7 @@
|
||||
}
|
||||
|
||||
const {
|
||||
id,
|
||||
children,
|
||||
variant,
|
||||
onclick = undefined,
|
||||
@@ -371,6 +373,7 @@
|
||||
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
{id}
|
||||
class="container active"
|
||||
style:cursor={!disabled ? "pointer" : "default"}
|
||||
style:height={styleHeight}
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
<script lang="ts" module>
|
||||
export const guidePopoverToggleId = "guide-popover-toggle";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { Config } from "@bindings/config/Config";
|
||||
|
||||
import { activeRouteStore, push } from "@app/lib/router";
|
||||
import { addRepoPopoverToggleId } from "./AddRepoButton.svelte";
|
||||
import { didFromPublicKey, truncateDid } from "@app/lib/utils";
|
||||
import { nodeRunning } from "@app/lib/events";
|
||||
import { radicleInstalled } from "@app/lib/checkRadicleCLI.svelte";
|
||||
import { sleep } from "@app/lib/sleep";
|
||||
|
||||
import Border from "@app/components/Border.svelte";
|
||||
import Command from "@app/components/Command.svelte";
|
||||
@@ -11,7 +19,6 @@
|
||||
import NakedButton from "@app/components/NakedButton.svelte";
|
||||
import NodeId from "@app/components/NodeId.svelte";
|
||||
import Popover from "@app/components/Popover.svelte";
|
||||
import RepoGuide from "@app/components/RepoGuide.svelte";
|
||||
|
||||
interface Props {
|
||||
config: Config;
|
||||
@@ -22,28 +29,35 @@
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.guide-header {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
.spacer {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid var(--color-border-default);
|
||||
height: 1px;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
button {
|
||||
text-decoration: underline;
|
||||
border: 0;
|
||||
color: var(--color-foreground-contrast);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<Popover
|
||||
popoverId="popover-guide"
|
||||
popoverPadding="0"
|
||||
popoverPositionTop="2.5rem"
|
||||
bind:expanded={popoverExpanded}
|
||||
popoverPositionRight="-9.3rem">
|
||||
{#snippet toggle(onclick)}
|
||||
<NakedButton
|
||||
id={guidePopoverToggleId}
|
||||
variant="ghost"
|
||||
{onclick}
|
||||
stylePadding="0 4px"
|
||||
stylePadding="0 0.25rem"
|
||||
active={popoverExpanded}>
|
||||
<Icon name="info" /> Guide
|
||||
</NakedButton>
|
||||
@@ -53,7 +67,8 @@
|
||||
variant="ghost"
|
||||
styleGap="0"
|
||||
stylePadding="1rem"
|
||||
styleMinWidth="36rem"
|
||||
styleMinWidth="32rem"
|
||||
styleBackgroundColor="var(--color-background-float)"
|
||||
styleOverflow="auto"
|
||||
styleMaxHeight="calc(100vh - 5rem)"
|
||||
styleAlignItems="flex-start"
|
||||
@@ -61,14 +76,12 @@
|
||||
<div
|
||||
style:position="relative"
|
||||
style:display="flex"
|
||||
style:line-height="1.625rem"
|
||||
style:gap="0.5rem"
|
||||
style:flex-direction="column"
|
||||
style:padding="1rem"
|
||||
style:margin-bottom="1rem"
|
||||
style:width="100%"
|
||||
style:background-color="var(--color-background-float)">
|
||||
style:width="100%">
|
||||
<div class="txt-semibold txt-medium" style:margin-bottom="1rem">
|
||||
Getting started
|
||||
Get started
|
||||
</div>
|
||||
<div class="txt-small" style:display="inline">
|
||||
Hello <span style:padding-left="0.25rem">
|
||||
@@ -85,23 +98,44 @@
|
||||
you can share this with anyone to find you on the network.
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
{#if radicleInstalled()}
|
||||
{#if radicleInstalled() || $nodeRunning}
|
||||
<div class="global-flex txt-small">
|
||||
<Icon name="checkbox-checked" />Radicle CLI is setup
|
||||
<div class="global-flex">
|
||||
<Icon name="thumb-up" />Radicle CLI is installed, you're good to
|
||||
go.
|
||||
</div>
|
||||
<button
|
||||
class="txt-small"
|
||||
onclick={async () => {
|
||||
if ($activeRouteStore.resource !== "home") {
|
||||
await push({
|
||||
resource: "home",
|
||||
activeTab: "all",
|
||||
});
|
||||
}
|
||||
await sleep(1);
|
||||
const addRepoButton = document.getElementById(
|
||||
addRepoPopoverToggleId,
|
||||
);
|
||||
addRepoButton?.click();
|
||||
}}>
|
||||
Try adding a repo!
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="txt-small">
|
||||
<div class="global-flex" style:padding-bottom="0.5rem">
|
||||
<Icon name="checkbox-unchecked" />Make sure to install Radicle CLI
|
||||
<div class="global-flex" style:padding-bottom="1rem">
|
||||
<Icon name="warning" />Radicle CLI is not installed
|
||||
</div>
|
||||
<div style:padding-bottom="1rem">
|
||||
To interact with repositories on the Radicle network, you’ll need
|
||||
to install Radicle node along with its accompanying CLI tools. The
|
||||
node runs in the background, enabling seamless pushing and pulling
|
||||
of changes, while the CLI tools let you manage the node and
|
||||
provide interoperability between Git and Radicle.
|
||||
</div>
|
||||
<div style:padding-bottom="0.5rem">
|
||||
To be able to interact with repos on the Radicle network you'll
|
||||
need to install a node on your computer. This node will identify
|
||||
itself on the network with your keys to push and pull changes.
|
||||
</div>
|
||||
<div style:padding-bottom="0.5rem">
|
||||
To install the node and other Radicle CLI tooling, simply run the
|
||||
command below from your shell:
|
||||
To install Radicle node and CLI tooling, run this in your shell:
|
||||
</div>
|
||||
<Command
|
||||
styleWidth="fit-content"
|
||||
@@ -109,9 +143,6 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="guide-header txt-medium txt-semibold">Guide</div>
|
||||
|
||||
<RepoGuide />
|
||||
</Border>
|
||||
{/snippet}
|
||||
</Popover>
|
||||
@@ -9,11 +9,13 @@
|
||||
|
||||
import { checkRadicleCLI } from "@app/lib/checkRadicleCLI.svelte";
|
||||
import { dynamicInterval } from "@app/lib/interval";
|
||||
import { setFocused } from "@app/components/Popover.svelte";
|
||||
import { sleep } from "@app/lib/sleep";
|
||||
|
||||
import Icon from "@app/components/Icon.svelte";
|
||||
import InboxPopover from "@app/components/InboxPopover.svelte";
|
||||
import InfoButton from "@app/components/InfoButton.svelte";
|
||||
import InboxButton from "@app/components/InboxButton.svelte";
|
||||
import GuideButton, {
|
||||
guidePopoverToggleId,
|
||||
} from "@app/components/GuideButton.svelte";
|
||||
import NakedButton from "@app/components/NakedButton.svelte";
|
||||
import NodeStatusButton from "@app/components/NodeStatusButton.svelte";
|
||||
|
||||
@@ -41,7 +43,9 @@
|
||||
}
|
||||
|
||||
if (firstLaunchStorage.value === true) {
|
||||
setFocused("popover-guide");
|
||||
const guidePopoverButton = document.getElementById(guidePopoverToggleId);
|
||||
await sleep(1);
|
||||
guidePopoverButton?.click();
|
||||
firstLaunchStorage.value = false;
|
||||
}
|
||||
});
|
||||
@@ -108,9 +112,9 @@
|
||||
</div>
|
||||
|
||||
<div class="global-flex">
|
||||
<InfoButton {config} />
|
||||
<GuideButton {config} />
|
||||
<NodeStatusButton />
|
||||
<InboxPopover {notificationCount} />
|
||||
<InboxButton {notificationCount} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
| "branch"
|
||||
| "broom"
|
||||
| "broom-double"
|
||||
| "bulb"
|
||||
| "checkbox-checked"
|
||||
| "checkbox-unchecked"
|
||||
| "checkmark"
|
||||
@@ -44,6 +45,7 @@
|
||||
| "file"
|
||||
| "filter"
|
||||
| "home"
|
||||
| "hourglass"
|
||||
| "inbox"
|
||||
| "info"
|
||||
| "issue"
|
||||
@@ -258,6 +260,24 @@
|
||||
<path d="M9 3H10V9H9V3Z" />
|
||||
<path d="M10 3H11V9H10V3Z" />
|
||||
<path d="M9 2H11V3H9V2Z" />
|
||||
{:else if name === "bulb"}
|
||||
<path d="M9 13.5H7V14.5H9V13.5Z" />
|
||||
<path d="M5 7.50003V9.50003H4L4 7.50003H5Z" />
|
||||
<path d="M4 4.50003L4 7.50003H3L3 4.50003H4Z" />
|
||||
<path d="M12 4.50003V7.50003H13V4.50003H12Z" />
|
||||
<path d="M11 7.50003V9.50003L12 9.50003V7.50003H11Z" />
|
||||
<path d="M5 4.50003L4 4.50003L4 3.50003L5 3.50003L5 4.50003Z" />
|
||||
<path d="M11 4.50003H12V3.50003H11V4.50003Z" />
|
||||
<path d="M6 9.50003V10.5H5L5 9.50003H6Z" />
|
||||
<path d="M6 2.50003L10 2.50003V1.50003L6 1.50003L6 2.50003Z" />
|
||||
<path d="M10 9.50003L11 9.50003V10.5H10V9.50003Z" />
|
||||
<path d="M9 10.5H10V13.5H9L9 10.5Z" />
|
||||
<path d="M6 10.5H7V13.5H6L6 10.5Z" />
|
||||
<path d="M7 10.5H10L10 11.5H7V10.5Z" />
|
||||
<path d="M10 2.50003H12V3.50003L10 3.50003L10 2.50003Z" />
|
||||
<path d="M4 2.50003L6 2.50003V3.50003L4 3.50003L4 2.50003Z" />
|
||||
<path d="M7 7.50003H9V9.50003H7V7.50003Z" />
|
||||
<path d="M7 12.5H8V13.5H7L7 12.5Z" />
|
||||
{:else if name === "checkmark"}
|
||||
<path d="M7 11V12H6V11H7Z" />
|
||||
<path d="M8 10V11L7 11L7 10H8Z" />
|
||||
@@ -680,6 +700,29 @@
|
||||
<path d="M6 10.5H7V13.5H6V10.5Z" />
|
||||
<path d="M7 9.50003H9V10.5L7 10.5L7 9.50003Z" />
|
||||
<path d="M9 10.5L10 10.5V13.5H9L9 10.5Z" />
|
||||
{:else if name === "hourglass"}
|
||||
<path d="M13 14H3V13H13V14Z" />
|
||||
<path d="M3 13H2V12H3V13Z" />
|
||||
<path d="M14 13H13V12H14V13Z" />
|
||||
<path d="M4 12H3V11H4V12Z" />
|
||||
<path d="M11 12H5V11H6V10H10V11H11V12Z" />
|
||||
<path d="M13 12H12V11H13V12Z" />
|
||||
<path d="M5 11H4V10H5V11Z" />
|
||||
<path d="M12 11H11V10H12V11Z" />
|
||||
<path d="M6 10H5V9H6V10Z" />
|
||||
<path d="M11 10H10V9H11V10Z" />
|
||||
<path d="M7 9H6V7H7V9Z" />
|
||||
<path d="M10 9H9V7H10V9Z" />
|
||||
<path d="M6 7H5V6H6V7Z" />
|
||||
<path d="M10 6H9V7H7V6H6V5H10V6Z" />
|
||||
<path d="M11 7H10V6H11V7Z" />
|
||||
<path d="M5 6H4V5H5V6Z" />
|
||||
<path d="M12 6H11V5H12V6Z" />
|
||||
<path d="M4 5H3V4H4V5Z" />
|
||||
<path d="M13 5H12V4H13V5Z" />
|
||||
<path d="M3 4H2V3H3V4Z" />
|
||||
<path d="M14 4H13V3H14V4Z" />
|
||||
<path d="M13 3H3V2H13V3Z" />
|
||||
{:else if name === "inbox"}
|
||||
<path d="M2 3H3V13H2V3Z" />
|
||||
<path d="M13 3H14V13H13V3Z" />
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
import Border from "./Border.svelte";
|
||||
import Icon from "./Icon.svelte";
|
||||
import Inbox from "@app/views/home/Inbox.svelte";
|
||||
import Inbox from "./Inbox.svelte";
|
||||
import OutlineButton from "./OutlineButton.svelte";
|
||||
import Popover from "./Popover.svelte";
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
interface Props {
|
||||
id?: string;
|
||||
children: Snippet;
|
||||
title?: string;
|
||||
disabled?: boolean;
|
||||
@@ -14,6 +15,7 @@
|
||||
}
|
||||
|
||||
const {
|
||||
id,
|
||||
children,
|
||||
title,
|
||||
disabled,
|
||||
@@ -240,6 +242,7 @@
|
||||
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
{id}
|
||||
class="container"
|
||||
class:disabled
|
||||
class:active
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { nodeRunning } from "@app/lib/events";
|
||||
|
||||
import Border from "@app/components/Border.svelte";
|
||||
import Command from "@app/components/Command.svelte";
|
||||
import Icon from "@app/components/Icon.svelte";
|
||||
import NakedButton from "@app/components/NakedButton.svelte";
|
||||
import Popover from "@app/components/Popover.svelte";
|
||||
@@ -32,13 +33,20 @@
|
||||
styleMinWidth="20rem"
|
||||
styleAlignItems="flex-start"
|
||||
styleFlexDirection="column">
|
||||
<div class="txt-small txt-missing">
|
||||
<div class="txt-small" style:line-height="1.625rem">
|
||||
{#if $nodeRunning}
|
||||
Your node is up and running, your changes will be synced
|
||||
automatically.
|
||||
{:else}
|
||||
Your node is not running, changes you make are safe but won't be
|
||||
announced.
|
||||
|
||||
<div style:margin-top="1rem">
|
||||
You can start your node with:
|
||||
<div style:margin-top="0.5rem">
|
||||
<Command styleWidth="fit-content" command="rad node start" />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Border>
|
||||
|
||||
@@ -6,17 +6,6 @@
|
||||
export function closeFocused() {
|
||||
focused = undefined;
|
||||
}
|
||||
|
||||
export function setFocused(id: string) {
|
||||
const thisComponent = document.querySelector(`[data-popover-id="${id}"]`);
|
||||
if (thisComponent) {
|
||||
if (focused?.element === thisComponent) {
|
||||
closeFocused();
|
||||
} else {
|
||||
focused = { element: thisComponent as HTMLDivElement, id };
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -25,7 +14,6 @@
|
||||
interface Props {
|
||||
toggle: Snippet<[() => void]>;
|
||||
popover: Snippet;
|
||||
popoverId?: string;
|
||||
popoverContainerMinWidth?: string;
|
||||
popoverPadding?: string;
|
||||
popoverPositionBottom?: string;
|
||||
@@ -39,7 +27,6 @@
|
||||
let {
|
||||
toggle,
|
||||
popover,
|
||||
popoverId,
|
||||
popoverContainerMinWidth,
|
||||
popoverPadding,
|
||||
popoverPositionBottom,
|
||||
@@ -50,7 +37,7 @@
|
||||
}: Props = $props();
|
||||
/* eslint-enable prefer-const */
|
||||
|
||||
const id = popoverId ?? crypto.randomUUID();
|
||||
const id = crypto.randomUUID();
|
||||
let thisComponent: HTMLDivElement | undefined = $state();
|
||||
|
||||
function clickOutside(ev: MouseEvent | TouchEvent) {
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
<script lang="ts">
|
||||
import { formatRepositoryId } from "@app/lib/utils";
|
||||
|
||||
import { invoke } from "@app/lib/invoke";
|
||||
import { nodeRunning } from "@app/lib/events";
|
||||
import { announce } from "@app/components/AnnounceSwitch.svelte";
|
||||
|
||||
import Border from "./Border.svelte";
|
||||
import Id from "./Id.svelte";
|
||||
import Icon from "./Icon.svelte";
|
||||
import NakedButton from "./NakedButton.svelte";
|
||||
|
||||
interface Props {
|
||||
reload: () => Promise<void>;
|
||||
rid: string;
|
||||
}
|
||||
|
||||
const { reload, rid }: Props = $props();
|
||||
|
||||
async function unseed() {
|
||||
try {
|
||||
await invoke<null>("unseed", {
|
||||
rid: rid,
|
||||
opts: { announce: $nodeRunning && $announce },
|
||||
});
|
||||
await reload();
|
||||
} catch (error) {
|
||||
console.error("Seeding failed", error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.unseed {
|
||||
display: none;
|
||||
color: var(--color-fill-gray);
|
||||
height: 1.375rem;
|
||||
}
|
||||
.container:hover .unseed {
|
||||
display: flex;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container">
|
||||
<Border
|
||||
variant="float"
|
||||
styleWidth="100%"
|
||||
styleHeight="8.375rem"
|
||||
styleAlignItems="flex-start"
|
||||
styleFlexDirection="column"
|
||||
styleGap="0"
|
||||
stylePadding="0.5rem 0.75rem"
|
||||
styleOverflow="hidden">
|
||||
<div class="header txt-small">
|
||||
<div class="global-flex txt-overflow">
|
||||
<div
|
||||
class="global-counter"
|
||||
style:background-color="var(--color-fill-ghost)">
|
||||
</div>
|
||||
<span class="global-flex" style:height="1.375rem">
|
||||
<div
|
||||
style:height="1rem"
|
||||
style:width="7rem"
|
||||
style:background-color="var(--color-fill-ghost)">
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<div class="global-flex">
|
||||
<div class="global-flex unseed">
|
||||
<NakedButton
|
||||
stylePadding="0 0.25rem"
|
||||
variant="ghost"
|
||||
onclick={unseed}>
|
||||
<Icon name="broom" />
|
||||
Remove
|
||||
</NakedButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="global-flex" style:height="1.375rem" style:margin-top="0.25rem">
|
||||
<div
|
||||
style:height="0.875rem"
|
||||
style:width="13rem"
|
||||
style:background-color="var(--color-fill-ghost)">
|
||||
</div>
|
||||
</div>
|
||||
<Id
|
||||
ariaLabel="repo-id"
|
||||
clipboard={rid}
|
||||
shorten={false}
|
||||
variant="oid"
|
||||
id={formatRepositoryId(rid)} />
|
||||
|
||||
<div
|
||||
class="global-flex footer txt-small"
|
||||
style:margin-top="auto"
|
||||
style:width="100%">
|
||||
<span
|
||||
title={$nodeRunning
|
||||
? "This may take a while depending on your network connectivity and repo size."
|
||||
: "Your node is offline. Start your node to fetch this repo."}
|
||||
class="global-flex"
|
||||
style:color="var(--color-fill-gray)"
|
||||
style:margin-left="auto">
|
||||
<Icon name="hourglass" />
|
||||
Queued for fetching…
|
||||
</span>
|
||||
</div>
|
||||
</Border>
|
||||
</div>
|
||||
@@ -1,67 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { z } from "zod";
|
||||
|
||||
import useLocalStorage from "@app/lib/useLocalStorage.svelte";
|
||||
|
||||
import clone from "@app/components/RepoGuide/clone.md?raw";
|
||||
import publish from "@app/components/RepoGuide/publish.md?raw";
|
||||
|
||||
import Border from "@app/components/Border.svelte";
|
||||
import Markdown from "@app/components/Markdown.svelte";
|
||||
import Tab from "@app/components/Tab.svelte";
|
||||
|
||||
const tab = useLocalStorage(
|
||||
"repoGuideTab",
|
||||
z.union([z.literal("clone"), z.literal("publish")]),
|
||||
"publish",
|
||||
!window.localStorage,
|
||||
);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
overflow: scroll;
|
||||
}
|
||||
.tab {
|
||||
height: 1.5rem;
|
||||
color: var(--color-foreground-contrast);
|
||||
}
|
||||
</style>
|
||||
|
||||
{#snippet tabSnippet(name: typeof tab.value, content: string)}
|
||||
<Tab
|
||||
active={tab.value === name}
|
||||
onclick={() => {
|
||||
tab.value = name;
|
||||
}}>
|
||||
<span class="tab">{content}</span>
|
||||
</Tab>
|
||||
{/snippet}
|
||||
|
||||
<Border
|
||||
stylePosition="relative"
|
||||
variant="ghost"
|
||||
flatBottom
|
||||
styleDisplay="flex"
|
||||
styleWidth="100%"
|
||||
styleGap="1rem"
|
||||
stylePadding="0 1rem">
|
||||
{@render tabSnippet("clone", "Clone a repo from the network")}
|
||||
{@render tabSnippet("publish", "Publish existing repo")}
|
||||
</Border>
|
||||
|
||||
<Border
|
||||
variant="ghost"
|
||||
flatTop
|
||||
stylePadding="1rem"
|
||||
styleDisplay="block"
|
||||
styleFlexDirection="column"
|
||||
styleAlignItems="flex-start">
|
||||
<div class="container txt-small">
|
||||
{#if tab.value === "clone"}
|
||||
<Markdown content={clone} />
|
||||
{:else if tab.value === "publish"}
|
||||
<Markdown content={publish} />
|
||||
{/if}
|
||||
</div>
|
||||
</Border>
|
||||
@@ -1,21 +0,0 @@
|
||||
#### 1. Find a repo on the Radicle network
|
||||
|
||||
You can search for Radicle repos by name or description at [search.radicle.xyz](https://search.radicle.xyz).
|
||||
|
||||
To clone a repo, you’ll need its Repository Identifier (RID) — a unique string that begins with `rad:`.
|
||||
|
||||
#### 2. Start your node
|
||||
|
||||
If you node is Offline, you should start it by running:
|
||||
|
||||
```sh
|
||||
rad node start
|
||||
```
|
||||
|
||||
#### 3. Clone the repo
|
||||
|
||||
To clone a repo, use the `rad clone` command followed by the RID of the repo you want to clone.
|
||||
|
||||
```sh
|
||||
rad clone <RID>
|
||||
```
|
||||
@@ -1,15 +0,0 @@
|
||||
#### Publish existing repo on Radicle
|
||||
|
||||
Navigate to your existing Git repo and publish it to Radicle by following the setup prompts:
|
||||
|
||||
- **Repository Name:** Enter a name for your repo.
|
||||
- **Description:** Provide a brief summary of what your repo does.
|
||||
- **Default Branch:** Typically **main** or **master**.
|
||||
- **Visibility:** Choose **public** to share with others or **private** to not publish it to the network yet.
|
||||
|
||||
```sh
|
||||
cd path/to/your/repo
|
||||
rad init
|
||||
```
|
||||
|
||||
That's it! Your repo is now on the Radicle network. 🚀
|
||||
@@ -63,7 +63,7 @@
|
||||
style:padding="0 0.375rem"
|
||||
style:background-color="var(--color-fill-ghost)"
|
||||
style:gap="0.25rem">
|
||||
<Icon name="seedling" />
|
||||
<Icon name="seedling-filled" />
|
||||
{repo.seeding}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
name?: string;
|
||||
onDismiss?: () => void;
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
onSubmit?: () => void;
|
||||
oninput?: FormEventHandler<HTMLInputElement>;
|
||||
placeholder?: string;
|
||||
@@ -33,6 +34,7 @@
|
||||
name,
|
||||
onDismiss,
|
||||
onFocus,
|
||||
onBlur,
|
||||
onSubmit,
|
||||
oninput,
|
||||
placeholder,
|
||||
@@ -110,6 +112,9 @@
|
||||
focussed = true;
|
||||
}}
|
||||
onblur={() => {
|
||||
if (onBlur) {
|
||||
onBlur();
|
||||
}
|
||||
focussed = false;
|
||||
}}
|
||||
bind:this={inputElement}
|
||||
|
||||
+126
-54
@@ -11,40 +11,57 @@
|
||||
import * as router from "@app/lib/router";
|
||||
import { didFromPublicKey, modifierKey } from "@app/lib/utils";
|
||||
import { dynamicInterval } from "@app/lib/interval";
|
||||
import { guidePopoverToggleId } from "@app/components/GuideButton.svelte";
|
||||
import { invoke } from "@app/lib/invoke";
|
||||
import { setFocused } from "@app/components/Popover.svelte";
|
||||
import { sleep } from "@app/lib/sleep";
|
||||
|
||||
import AddRepoButton from "@app/components/AddRepoButton.svelte";
|
||||
import Border from "@app/components/Border.svelte";
|
||||
import HomeSidebar from "@app/components/HomeSidebar.svelte";
|
||||
import Icon from "@app/components/Icon.svelte";
|
||||
import Layout from "@app/views/repo/Layout.svelte";
|
||||
import NakedButton from "@app/components/NakedButton.svelte";
|
||||
import NodeBreadcrumb from "@app/components/NodeBreadcrumb.svelte";
|
||||
import OutlineButton from "@app/components/OutlineButton.svelte";
|
||||
import RepoCard from "@app/components/RepoCard.svelte";
|
||||
import RepoCardPlaceholder from "@app/components/RepoCardPlaceholder.svelte";
|
||||
import TextInput from "@app/components/TextInput.svelte";
|
||||
|
||||
interface Props {
|
||||
activeTab: HomeReposTab;
|
||||
config: Config;
|
||||
notificationCount: number;
|
||||
repoCount: RepoCount;
|
||||
repos: RepoInfo[];
|
||||
notificationCount: number;
|
||||
seededNotReplicated: string[];
|
||||
}
|
||||
|
||||
/* eslint-disable prefer-const */
|
||||
let { config, repos, repoCount, activeTab, notificationCount }: Props =
|
||||
let {
|
||||
activeTab,
|
||||
config,
|
||||
notificationCount,
|
||||
repoCount,
|
||||
repos,
|
||||
seededNotReplicated,
|
||||
}: Props =
|
||||
/* eslint-enable prefer-const */
|
||||
$props();
|
||||
|
||||
let lock = false;
|
||||
const startup = $state<{ error?: ErrorWrapper }>({ error: undefined });
|
||||
let showFilters: boolean = $state(false);
|
||||
let searchInput = $state("");
|
||||
|
||||
async function checkRepos() {
|
||||
let lock = false;
|
||||
|
||||
async function reloadRepoList() {
|
||||
try {
|
||||
if (lock) {
|
||||
return;
|
||||
}
|
||||
if (repoCount.total > 0) {
|
||||
if (seededNotReplicated.length === 0 && repoCount.total > 0) {
|
||||
return;
|
||||
}
|
||||
if (searchInput !== "") {
|
||||
return;
|
||||
}
|
||||
lock = true;
|
||||
@@ -58,24 +75,27 @@
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
dynamicInterval("repos", checkRepos, 5_000);
|
||||
dynamicInterval("repos", reloadRepoList, 5_000);
|
||||
});
|
||||
|
||||
async function reload() {
|
||||
[repos, repoCount, config] = await Promise.all([
|
||||
[repos, repoCount, config, seededNotReplicated] = await Promise.all([
|
||||
invoke<RepoInfo[]>("list_repos", { show: activeTab ?? "all" }),
|
||||
invoke<RepoCount>("repo_count"),
|
||||
invoke<Config>("config"),
|
||||
invoke<string[]>("seeded_not_replicated"),
|
||||
]);
|
||||
}
|
||||
|
||||
let searchInput = $state("");
|
||||
|
||||
const searchableRepos = $derived(
|
||||
repos
|
||||
.flatMap(r => {
|
||||
if (r.payloads["xyz.radicle.project"]) {
|
||||
return { repo: r, name: r.payloads["xyz.radicle.project"].data.name };
|
||||
return {
|
||||
repo: r,
|
||||
name: r.payloads["xyz.radicle.project"].data.name,
|
||||
description: r.payloads["xyz.radicle.project"].data.description,
|
||||
};
|
||||
}
|
||||
})
|
||||
.filter((item): item is NonNullable<typeof item> => item !== undefined),
|
||||
@@ -83,7 +103,7 @@
|
||||
|
||||
const searchResults = $derived(
|
||||
fuzzysort.go(searchInput, searchableRepos, {
|
||||
keys: ["name", "repo.rid"],
|
||||
keys: ["name", "description", "repo.rid"],
|
||||
threshold: 0.5,
|
||||
all: true,
|
||||
}),
|
||||
@@ -104,10 +124,19 @@
|
||||
font-size: var(--font-size-medium);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-right: 1.5rem;
|
||||
padding-right: 0.325rem;
|
||||
align-items: center;
|
||||
min-height: 2.5rem;
|
||||
}
|
||||
button {
|
||||
text-decoration: underline;
|
||||
border: 0;
|
||||
color: var(--color-foreground-dim);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<Layout
|
||||
@@ -124,38 +153,77 @@
|
||||
{/snippet}
|
||||
<div class="container">
|
||||
<div class="global-flex" style:margin-bottom="1rem">
|
||||
<div class="header">Repositories</div>
|
||||
{#if repos.length > 0}
|
||||
<div class="global-flex" style:margin-left="auto">
|
||||
<TextInput
|
||||
onSubmit={async () => {
|
||||
if (searchResults.length === 1) {
|
||||
await router.push({
|
||||
resource: "repo.home",
|
||||
rid: searchResults[0].obj.repo.rid,
|
||||
});
|
||||
}
|
||||
}}
|
||||
onDismiss={() => {
|
||||
searchInput = "";
|
||||
}}
|
||||
placeholder={`Fuzzy filter repositories ${modifierKey()} + f`}
|
||||
keyShortcuts="ctrl+f"
|
||||
bind:value={searchInput}>
|
||||
{#snippet left()}
|
||||
<div
|
||||
style:color="var(--color-foreground-dim)"
|
||||
style:padding-left="0.5rem">
|
||||
<Icon name="filter" />
|
||||
</div>
|
||||
{/snippet}
|
||||
</TextInput>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="global-flex">
|
||||
<div class="header">Repositories</div>
|
||||
{#if repos.length > 0}
|
||||
{#if !showFilters}
|
||||
<NakedButton
|
||||
styleHeight="2.5rem"
|
||||
keyShortcuts="ctrl+f"
|
||||
variant="ghost"
|
||||
active={showFilters}
|
||||
onclick={() => {
|
||||
if (showFilters) {
|
||||
showFilters = false;
|
||||
searchInput = "";
|
||||
} else {
|
||||
showFilters = true;
|
||||
}
|
||||
}}>
|
||||
<Icon name="filter" />
|
||||
</NakedButton>
|
||||
{/if}
|
||||
{#if showFilters}
|
||||
<TextInput
|
||||
autofocus
|
||||
onSubmit={async () => {
|
||||
if (searchResults.length === 1) {
|
||||
await router.push({
|
||||
resource: "repo.home",
|
||||
rid: searchResults[0].obj.repo.rid,
|
||||
});
|
||||
}
|
||||
}}
|
||||
onDismiss={() => {
|
||||
searchInput = "";
|
||||
showFilters = false;
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (searchInput.trim() === "") {
|
||||
showFilters = false;
|
||||
}
|
||||
}}
|
||||
placeholder={`Fuzzy filter repositories ${modifierKey()} + f`}
|
||||
keyShortcuts="ctrl+f"
|
||||
bind:value={searchInput}>
|
||||
{#snippet left()}
|
||||
<div style:padding-left="0.5rem">
|
||||
<Icon name="filter" />
|
||||
</div>
|
||||
{/snippet}
|
||||
</TextInput>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="global-flex" style:margin-left="auto">
|
||||
<AddRepoButton
|
||||
{repos}
|
||||
{reload}
|
||||
{seededNotReplicated}
|
||||
onOpen={() => {
|
||||
searchInput = "";
|
||||
showFilters = false;
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
{#if repoCount.total > 0}
|
||||
{#if searchResults.length > 0}
|
||||
{#if repoCount.total > 0 || seededNotReplicated.length > 0}
|
||||
{#if searchResults.length > 0 || seededNotReplicated.length > 0}
|
||||
<div class="repo-grid">
|
||||
{#if !showFilters}
|
||||
{#each seededNotReplicated as rid}
|
||||
<RepoCardPlaceholder {rid} {reload} />
|
||||
{/each}
|
||||
{/if}
|
||||
{#each searchResults as result}
|
||||
<RepoCard
|
||||
focussed={searchResults.length === 1 && searchInput !== ""}
|
||||
@@ -190,17 +258,21 @@
|
||||
</Border>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="txt-missing txt-small" style:margin-bottom="1.5rem">
|
||||
You don't have any repositories in your Radicle storage yet. To get
|
||||
started, check out the guide below.
|
||||
<div class="txt-missing txt-small">
|
||||
You don't have any repositories in your Radicle storage yet.
|
||||
</div>
|
||||
<div style="display: flex; gap: 1rem;">
|
||||
<OutlineButton
|
||||
popoverToggle="popover-guide"
|
||||
onclick={() => setFocused("popover-guide")}
|
||||
variant="ghost">
|
||||
<Icon name="info" />Guide
|
||||
</OutlineButton>
|
||||
<!-- prettier-ignore -->
|
||||
<div class="txt-missing txt-small">
|
||||
To get started, check out
|
||||
<button
|
||||
class="txt-small"
|
||||
onclick={async () => {
|
||||
const guidePopoverButton = document.getElementById(guidePopoverToggleId);
|
||||
await sleep(1);
|
||||
guidePopoverButton?.click();
|
||||
}}>
|
||||
the guide
|
||||
</button>.
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface LoadedHomeRoute {
|
||||
repos: RepoInfo[];
|
||||
config: Config;
|
||||
notificationCount: number;
|
||||
seededNotReplicated: string[];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -35,12 +36,14 @@ export async function loadHome(route: HomeRoute): Promise<LoadedHomeRoute> {
|
||||
}
|
||||
}
|
||||
|
||||
const [config, repoCount, repos, notificationCount] = await Promise.all([
|
||||
invoke<Config>("config"),
|
||||
invoke<RepoCount>("repo_count"),
|
||||
invoke<RepoInfo[]>("list_repos", { show }),
|
||||
invoke<number>("notification_count"),
|
||||
]);
|
||||
const [config, repoCount, repos, notificationCount, seededNotReplicated] =
|
||||
await Promise.all([
|
||||
invoke<Config>("config"),
|
||||
invoke<RepoCount>("repo_count"),
|
||||
invoke<RepoInfo[]>("list_repos", { show }),
|
||||
invoke<number>("notification_count"),
|
||||
invoke<string[]>("seeded_not_replicated"),
|
||||
]);
|
||||
return {
|
||||
resource: "home",
|
||||
params: {
|
||||
@@ -49,6 +52,7 @@ export async function loadHome(route: HomeRoute): Promise<LoadedHomeRoute> {
|
||||
repos,
|
||||
config,
|
||||
notificationCount,
|
||||
seededNotReplicated,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user