mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-12 09:10:34 +08:00
Add breadcrumbs
This commit is contained in:
@@ -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" />
|
||||
@@ -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="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>
|
||||
<slot name="icon-left" />
|
||||
</div>
|
||||
|
||||
<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 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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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" }}>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user