Radish alpha
r
Radicle desktop app
Radicle
Git (anonymous pull)
Log in to clone via SSH
Add breadcrumbs
Rūdolfs Ošiņš committed 1 year ago
commit 5725cec33f6772420391236ef67eaf2be293f2bd
parent 33e0b52e23098b4adc18aa71e742b86cbc049151
9 files changed +319 -46
added src/components/Avatar.svelte
@@ -0,0 +1,45 @@
+
<script lang="ts">
+
  import { createIcon } from "@app/lib/blockies";
+

+
  export let nodeId: string;
+

+
  function createContainer(source: string) {
+
    const seed = source.toLowerCase();
+
    const avatar = createIcon({
+
      seed,
+
      size: 8,
+
      scale: 16,
+
    });
+
    return avatar.toDataURL();
+
  }
+
</script>
+

+
<style>
+
  .avatar {
+
    display: block;
+
    width: inherit;
+
    object-fit: cover;
+
    background-size: cover;
+
    background-repeat: no-repeat;
+
    width: 1rem;
+
    height: 1rem;
+
    clip-path: polygon(
+
      0 2px,
+
      2px 2px,
+
      2px 0,
+
      calc(100% - 2px) 0,
+
      calc(100% - 2px) 2px,
+
      100% 2px,
+
      100% calc(100% - 2px),
+
      calc(100% - 2px) calc(100% - 2px),
+
      calc(100% - 2px) calc(100% - 2px),
+
      calc(100% - 2px) 100%,
+
      2px 100%,
+
      2px calc(100% - 2px),
+
      0 calc(100% - 2px)
+
    );
+
    background-color: red;
+
  }
+
</style>
+

+
<img title={nodeId} src={createContainer(nodeId)} class="avatar" alt="avatar" />
modified src/components/Header.svelte
@@ -11,7 +11,7 @@
  .header {
    padding: 0 0.5rem;
    gap: 0.25rem;
-
    height: 3rem;
+
    height: 5rem;
  }
  .wrapper {
    width: 100%;
@@ -27,7 +27,7 @@
    top: 0;
    left: 0.5rem;
    right: 0.5rem;
-
    height: 3rem;
+
    height: 5rem;
    z-index: -1;

    background-color: var(--color-background-float);
@@ -53,44 +53,61 @@
</style>

<div class="header global-flex">
-
  <div class="wrapper global-flex">
-
    <div class="wrapper-left global-flex" style:gap="0">
-
      <div class="global-flex" style:gap="0">
-
        <NakedButton
-
          variant="ghost"
-
          onclick={() => {
-
            window.history.back();
-
          }}>
-
          <Icon name="arrow-left" />
-
        </NakedButton>
-
        <NakedButton
-
          variant="ghost"
-
          onclick={() => {
-
            window.history.forward();
-
          }}>
-
          <Icon name="arrow-right" />
-
        </NakedButton>
+
  <div
+
    class="global-flex"
+
    style:flex-direction="column"
+
    style:width="100%"
+
    style:align-items="flex-start">
+
    <div class="wrapper global-flex">
+
      <div class="wrapper-left global-flex" style:gap="0">
+
        <div class="global-flex" style:gap="0">
+
          <NakedButton
+
            variant="ghost"
+
            onclick={() => {
+
              window.history.back();
+
            }}>
+
            <Icon name="arrow-left" />
+
          </NakedButton>
+
          <NakedButton
+
            variant="ghost"
+
            onclick={() => {
+
              window.history.forward();
+
            }}>
+
            <Icon name="arrow-right" />
+
          </NakedButton>
+
        </div>
+
        <slot name="icon-left" />
      </div>
-
      <slot name="icon-left" />
-
    </div>

-
    <slot name="center" />
+
      <slot name="center" />

-
    <div class="global-flex" style:gap="0.5rem">
-
      <OutlineButton variant="ghost">
-
        <Icon name="offline" />
-
        Offline
-
      </OutlineButton>
-
      <Popover popoverPositionRight="0" popoverPositionTop="3rem">
-
        <NakedButton variant="ghost" slot="toggle" let:toggle onclick={toggle}>
-
          <Icon name="more-vertical" />
-
        </NakedButton>
-
        <Border variant="ghost" slot="popover" stylePadding="0.5rem 1rem">
-
          <div style="display: flex; gap: 2rem; align-items: center;">
-
            Theme <ThemeSwitch />
-
          </div>
-
        </Border>
-
      </Popover>
+
      <div class="global-flex" style:gap="0.5rem">
+
        <OutlineButton variant="ghost">
+
          <Icon name="offline" />
+
          Offline
+
        </OutlineButton>
+
        <Popover popoverPositionRight="0" popoverPositionTop="3rem">
+
          <NakedButton
+
            variant="ghost"
+
            slot="toggle"
+
            let:toggle
+
            onclick={toggle}>
+
            <Icon name="more-vertical" />
+
          </NakedButton>
+
          <Border variant="ghost" slot="popover" stylePadding="0.5rem 1rem">
+
            <div style="display: flex; gap: 2rem; align-items: center;">
+
              Theme <ThemeSwitch />
+
            </div>
+
          </Border>
+
        </Popover>
+
      </div>
+
    </div>
+
    <div
+
      class="global-flex txt-tiny txt-semibold"
+
      style:gap="0.5rem"
+
      style:margin-left="1rem"
+
      style:min-height="1.5rem">
+
      <slot name="breadcrumbs" />
    </div>
  </div>
  <div class="bottom-pixel-corners"></div>
added src/components/NodeId.svelte
@@ -0,0 +1,40 @@
+
<script lang="ts">
+
  import { truncateId } from "@app/lib/utils";
+

+
  import Avatar from "./Avatar.svelte";
+

+
  export let nodeId: string;
+
  export let alias: string | undefined = undefined;
+
  export let styleFontSize: string | undefined = "var(--font-size-small)";
+
  export let styleFontFamily: string | undefined =
+
    "var(--font-family-monospace)";
+
</script>
+

+
<style>
+
  .avatar-alias {
+
    display: flex;
+
    align-items: center;
+
    gap: 0.375rem;
+
    height: 1rem;
+
    font-weight: var(--font-weight-semibold);
+
  }
+
  .no-alias {
+
    color: var(--color-foreground-dim);
+
  }
+
</style>
+

+
<div
+
  class="avatar-alias"
+
  style:font-size={styleFontSize}
+
  style:font-family={styleFontFamily}>
+
  <Avatar {nodeId} />
+
  {#if alias}
+
    <span class="txt-overflow">
+
      {alias}
+
    </span>
+
  {:else}
+
    <span class="no-alias">
+
      {truncateId(nodeId)}
+
    </span>
+
  {/if}
+
</div>
added src/lib/blockies.ts
@@ -0,0 +1,125 @@
+
// Copyright (c) 2019, Ethereum Name Service
+

+
// The random number is a js implementation of the Xorshift PRNG
+
const randseed = new Array(4); // Xorshift: [x, y, z, w] 32 bit values
+

+
function seedrand(seed: string) {
+
  for (let i = 0; i < randseed.length; i++) {
+
    randseed[i] = 0;
+
  }
+
  for (let i = 0; i < seed.length; i++) {
+
    randseed[i % 4] =
+
      (randseed[i % 4] << 5) - randseed[i % 4] + seed.charCodeAt(i);
+
  }
+
}
+

+
function rand(): number {
+
  // Based on Java's String.hashCode(), expanded to 4 32bit values.
+
  const t = randseed[0] ^ (randseed[0] << 11);
+

+
  randseed[0] = randseed[1];
+
  randseed[1] = randseed[2];
+
  randseed[2] = randseed[3];
+
  randseed[3] = randseed[3] ^ (randseed[3] >> 19) ^ t ^ (t >> 8);
+

+
  return (randseed[3] >>> 0) / ((1 << 31) >>> 0);
+
}
+

+
function createColor(): string {
+
  // Saturation is the whole color spectrum.
+
  const h = Math.floor(rand() * 360);
+
  // Saturation goes from 40 to 100, it avoids greyish colors.
+
  const s = rand() * 60 + 40 + "%";
+
  // Lightness can be anything from 0 to 100, but probabilities are a bell curve around 50%.
+
  const l = (rand() + rand() + rand() + rand()) * 25 + "%";
+

+
  return `hsl(${h}, ${s}, ${l})`;
+
}
+

+
function createImageData(size: number): number[] {
+
  const width = size;
+
  const height = size;
+

+
  const dataWidth = Math.ceil(width / 2);
+
  const mirrorWidth = width - dataWidth;
+

+
  const data = [];
+
  for (let y = 0; y < height; y++) {
+
    let row = [];
+
    for (let x = 0; x < dataWidth; x++) {
+
      // this makes foreground and background color to have a 43% (1/2.3) probability
+
      // spot color has 13% chance
+
      row[x] = Math.floor(rand() * 2.3);
+
    }
+
    const r = row.slice(0, mirrorWidth);
+
    r.reverse();
+
    row = row.concat(r);
+

+
    for (let i = 0; i < row.length; i++) {
+
      data.push(row[i]);
+
    }
+
  }
+

+
  return data;
+
}
+

+
function createCanvas(
+
  imageData: number[],
+
  color: string,
+
  scale: number,
+
  bgcolor: string,
+
  spotcolor: string,
+
): HTMLCanvasElement {
+
  const c = document.createElement("canvas");
+
  const width = Math.sqrt(imageData.length);
+
  c.width = c.height = width * scale;
+

+
  const cc = c.getContext("2d");
+

+
  if (!cc) throw new Error("Can't get 2D context");
+

+
  cc.fillStyle = bgcolor;
+
  cc.fillRect(0, 0, c.width, c.height);
+
  cc.fillStyle = color;
+

+
  for (let i = 0; i < imageData.length; i++) {
+
    const row = Math.floor(i / width);
+
    const col = i % width;
+
    // if data is 2, choose spot color, if 1 choose foreground
+
    cc.fillStyle = imageData[i] === 1 ? color : spotcolor;
+

+
    // if data is 0, leave the background
+
    if (imageData[i]) {
+
      cc.fillRect(col * scale, row * scale, scale, scale);
+
    }
+
  }
+

+
  return c;
+
}
+

+
export interface Options {
+
  seed: string;
+
  size: number;
+
  scale: number;
+
  color?: string;
+
  bgcolor?: string;
+
  spotcolor?: string;
+
}
+

+
export function createIcon(opts: Options): HTMLCanvasElement {
+
  opts = opts || {};
+
  const size = opts.size || 8;
+
  const scale = opts.scale || 4;
+
  const seed =
+
    opts.seed || Math.floor(Math.random() * Math.pow(10, 16)).toString(16);
+

+
  seedrand(seed);
+

+
  const color = opts.color || createColor();
+
  const bgcolor = opts.bgcolor || createColor();
+
  const spotcolor = opts.spotcolor || createColor();
+
  const imageData = createImageData(size);
+
  const canvas = createCanvas(imageData, color, scale, bgcolor, spotcolor);
+

+
  return canvas;
+
}
modified src/views/Home.svelte
@@ -7,6 +7,7 @@
  import CopyableId from "@app/components/CopyableId.svelte";
  import Header from "@app/components/Header.svelte";
  import RepoCard from "@app/components/RepoCard.svelte";
+
  import NodeId from "@app/components/NodeId.svelte";

  export let repos: RepoInfo[];
  export let config: Config;
@@ -68,6 +69,13 @@
      <svelte:fragment slot="center">
        <CopyableId id={`did:key:${config.publicKey}`} />
      </svelte:fragment>
+
      <svelte:fragment slot="breadcrumbs">
+
        <NodeId
+
          nodeId={config.publicKey}
+
          alias={config.alias}
+
          styleFontFamily="var(--font-family-sans-serif)"
+
          styleFontSize="var(--font-size-tiny)" />
+
      </svelte:fragment>
    </Header>
  </div>
  <div style:padding="1rem">
modified src/views/repo/Issues.svelte
@@ -1,14 +1,32 @@
<script lang="ts">
+
  import type { Config } from "@bindings/Config";
  import type { Issue } from "@bindings/Issue";
  import type { RepoInfo } from "@bindings/RepoInfo";

+
  import Icon from "@app/components/Icon.svelte";
  import Layout from "./Layout.svelte";
+
  import NodeId from "@app/components/NodeId.svelte";

  export let repo: RepoInfo;
  export let issues: Issue[];
+
  export let config: Config;
+

+
  $: project = repo.payloads["xyz.radicle.project"]!;
</script>

<Layout {repo}>
+
  <svelte:fragment slot="breadcrumbs">
+
    <NodeId
+
      nodeId={config.publicKey}
+
      alias={config.alias}
+
      styleFontFamily="var(--font-family-sans-serif)"
+
      styleFontSize="var(--font-size-tiny)" />
+
    <Icon name="chevron-right" />
+
    {project.data.name}
+
    <Icon name="chevron-right" />
+
    Issues
+
  </svelte:fragment>
+

  <pre>
    <!-- prettier-ignore -->
    {#each issues as issue}
modified src/views/repo/Layout.svelte
@@ -8,8 +8,6 @@
  import NakedButton from "@app/components/NakedButton.svelte";

  export let repo: RepoInfo;
-

-
  $: project = repo.payloads["xyz.radicle.project"]!;
</script>

<style>
@@ -30,9 +28,11 @@
      <svelte:fragment slot="center">
        <CopyableId id={repo.rid} />
      </svelte:fragment>
+
      <svelte:fragment slot="breadcrumbs">
+
        <slot name="breadcrumbs" />
+
      </svelte:fragment>
    </Header>
  </div>
-
  <div>{project.data.name}</div>

  Issues
  <Link route={{ resource: "repo.issues", rid: repo.rid, status: "open" }}>
modified src/views/repo/Patches.svelte
@@ -1,14 +1,31 @@
<script lang="ts">
+
  import type { Config } from "@bindings/Config";
  import type { Patch } from "@bindings/Patch";
  import type { RepoInfo } from "@bindings/RepoInfo";

+
  import Icon from "@app/components/Icon.svelte";
  import Layout from "./Layout.svelte";
+
  import NodeId from "@app/components/NodeId.svelte";

  export let repo: RepoInfo;
  export let patches: Patch[];
+
  export let config: Config;
+

+
  $: project = repo.payloads["xyz.radicle.project"]!;
</script>

<Layout {repo}>
+
  <svelte:fragment slot="breadcrumbs">
+
    <NodeId
+
      nodeId={config.publicKey}
+
      alias={config.alias}
+
      styleFontFamily="var(--font-family-sans-serif)"
+
      styleFontSize="var(--font-size-tiny)" />
+
    <Icon name="chevron-right" />
+
    {project.data.name}
+
    <Icon name="chevron-right" />
+
    Patches
+
  </svelte:fragment>
  <pre>
    <!-- prettier-ignore -->
    {#each patches as patch}
modified src/views/repo/router.ts
@@ -1,6 +1,7 @@
-
import type { RepoInfo } from "@bindings/RepoInfo";
-
import type { Patch } from "@bindings/Patch";
+
import type { Config } from "@bindings/Config";
import type { Issue } from "@bindings/Issue";
+
import type { Patch } from "@bindings/Patch";
+
import type { RepoInfo } from "@bindings/RepoInfo";

import { invoke } from "@tauri-apps/api/core";
import { unreachable } from "@app/lib/utils";
@@ -13,7 +14,7 @@ export interface RepoIssuesRoute {

export interface LoadedRepoIssuesRoute {
  resource: "repo.issues";
-
  params: { repo: RepoInfo; issues: Issue[] };
+
  params: { repo: RepoInfo; config: Config; issues: Issue[] };
}

export interface RepoPatchesRoute {
@@ -24,7 +25,7 @@ export interface RepoPatchesRoute {

export interface LoadedRepoPatchesRoute {
  resource: "repo.patches";
-
  params: { repo: RepoInfo; patches: Patch[] };
+
  params: { repo: RepoInfo; config: Config; patches: Patch[] };
}

export type RepoRoute = RepoIssuesRoute | RepoPatchesRoute;
@@ -34,24 +35,26 @@ export async function loadPatches(route: RepoRoute): Promise<LoadedRepoRoute> {
  const repo: RepoInfo = await invoke("repo_by_id", {
    rid: route.rid,
  });
+
  const config: Config = await invoke("config");
  const patches: Patch[] = await invoke("list_patches", {
    rid: route.rid,
    status: route.status,
  });

-
  return { resource: "repo.patches", params: { repo, patches } };
+
  return { resource: "repo.patches", params: { repo, config, patches } };
}

export async function loadIssues(route: RepoRoute): Promise<LoadedRepoRoute> {
  const repo: RepoInfo = await invoke("repo_by_id", {
    rid: route.rid,
  });
+
  const config: Config = await invoke("config");
  const issues: Issue[] = await invoke("list_issues", {
    rid: route.rid,
    status: route.status,
  });

-
  return { resource: "repo.issues", params: { repo, issues } };
+
  return { resource: "repo.issues", params: { repo, config, issues } };
}

export function repoRouteToPath(route: RepoRoute): string {