mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-16 03:40:37 +08:00
253 lines
6.5 KiB
Svelte
253 lines
6.5 KiB
Svelte
<script lang="ts">
|
|
import { navigate } from 'svelte-routing';
|
|
import type { Config } from '@app/config';
|
|
import * as proj from '@app/project';
|
|
import Loading from '@app/Loading.svelte';
|
|
import Address from '@app/Address.svelte';
|
|
import { Org } from '@app/base/orgs/Org';
|
|
import * as utils from '@app/utils';
|
|
|
|
import Tree from './Tree.svelte';
|
|
import Blob from './Blob.svelte';
|
|
import Readme from './Readme.svelte';
|
|
|
|
enum Status {
|
|
Idle,
|
|
Loading,
|
|
Loaded,
|
|
}
|
|
|
|
type State =
|
|
{ status: Status.Idle }
|
|
| { status: Status.Loading; path: string }
|
|
| { status: Status.Loaded; path: string; blob: proj.Blob };
|
|
|
|
export let urn: string;
|
|
export let commit: string;
|
|
export let config: Config;
|
|
export let path: string;
|
|
export let org = "";
|
|
|
|
// When the component is loaded the first time, the blob is yet to be loaded.
|
|
let state: State = { status: Status.Idle };
|
|
|
|
const loadBlob = async (path: string): Promise<proj.Blob> => {
|
|
if (state.status == Status.Loaded && state.path === path) {
|
|
return state.blob;
|
|
}
|
|
|
|
const isMarkdownPath = utils.isMarkdownPath(path);
|
|
const promise = path === "/"
|
|
? proj.getReadme(urn, commit, config)
|
|
: proj.getBlob(urn, commit, path, { highlight: !isMarkdownPath }, config);
|
|
|
|
state = { status: Status.Loading, path };
|
|
state = { status: Status.Loaded, path, blob: await promise };
|
|
|
|
return state.blob;
|
|
};
|
|
|
|
const onSelect = async ({ detail: path }: { detail: string }) => {
|
|
// Ensure we don't spend any time in a "loading" state. This means
|
|
// the loading spinner won't be shown, and instead the blob will be
|
|
// displayed once loaded.
|
|
const blob = await loadBlob(path);
|
|
getBlob = new Promise(resolve => resolve(blob));
|
|
|
|
navigate(proj.path({ urn, org, commit, path }));
|
|
};
|
|
|
|
const fetchTree = async (path: string) => {
|
|
return proj.getTree(urn, commit, path, config);
|
|
};
|
|
|
|
// This is reactive to respond to path changes that don't originate from this
|
|
// component, eg. when using the browser's "back" button.
|
|
$: getBlob = loadBlob(path);
|
|
$: getAnchor = org ? Org.getAnchor(org, urn, config) : null;
|
|
$: loadingPath = state.status == Status.Loading ? state.path : null;
|
|
</script>
|
|
|
|
<style>
|
|
main > header {
|
|
font-size: 0.75rem;
|
|
padding: 0 2rem 0 8rem;
|
|
margin-bottom: 2rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: left;
|
|
|
|
}
|
|
main > header > * {
|
|
margin-right: 0.75rem;
|
|
border-radius: 0.25rem;
|
|
min-width: max-content;
|
|
}
|
|
|
|
.commit {
|
|
display: inline-block;
|
|
font-family: var(--font-family-monospace);
|
|
color: var(--color-secondary);
|
|
background-color: var(--color-secondary-background);
|
|
padding: 0.5rem 0.75rem;
|
|
}
|
|
|
|
.anchor {
|
|
display: flex;
|
|
}
|
|
.anchor-widget {
|
|
display: flex;
|
|
padding: 0.5rem 0.75rem;
|
|
border-radius: 0.25rem;
|
|
color: var(--color-tertiary);
|
|
background-color: var(--color-tertiary-background);
|
|
}
|
|
.anchor-widget.not-anchored {
|
|
color: var(--color-foreground-faded);
|
|
background-color: var(--color-foreground-background);
|
|
}
|
|
.anchor-label {
|
|
font-family: var(--font-family-monospace);
|
|
margin-right: 0.5rem;
|
|
}
|
|
.anchor-label:last-child {
|
|
margin-right: 0;
|
|
}
|
|
|
|
.stat {
|
|
font-family: var(--font-family-monospace);
|
|
padding: 0.5rem 0.75rem;
|
|
background: var(--color-foreground-background);
|
|
}
|
|
|
|
.center-content {
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.container {
|
|
display: flex;
|
|
width: inherit;
|
|
margin-bottom: 4rem;
|
|
padding: 0 2rem 0 8rem;
|
|
}
|
|
|
|
.column-left {
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding-right: 1rem;
|
|
}
|
|
|
|
.column-right {
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding-left: 1rem;
|
|
min-width: var(--content-min-width);
|
|
width: 100%;
|
|
}
|
|
|
|
.source-tree {
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
.file-not-found {
|
|
text-align: center;
|
|
border-radius: 0.25rem;
|
|
padding-bottom: 2rem;
|
|
}
|
|
.file-not-found header {
|
|
padding: 1rem 0;
|
|
font-weight: bold;
|
|
}
|
|
.file-not-found .icon {
|
|
font-size: 1.5rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
@media (max-width: 800px) {
|
|
main > header, .container {
|
|
padding-left: 2rem;
|
|
}
|
|
.stat {
|
|
display: none;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<main>
|
|
{#await proj.getTree(urn, commit, "/", config)}
|
|
<!-- Loading -->
|
|
{:then tree}
|
|
<header>
|
|
<div class="commit">
|
|
commit {commit}
|
|
</div>
|
|
<div class="stat">
|
|
<strong>{tree.stats.commits}</strong> commit(s)
|
|
</div>
|
|
<div class="stat">
|
|
<strong>{tree.stats.contributors}</strong> contributor(s)
|
|
</div>
|
|
<div class="anchor">
|
|
{#if org}
|
|
{#await getAnchor}
|
|
<Loading small margins />
|
|
{:then anchor}
|
|
{#if anchor === commit}
|
|
<span class="anchor-widget">
|
|
<span class="anchor-label">anchored</span>
|
|
<Address address={org} compact resolve noBadge {config} />
|
|
</span>
|
|
{:else}
|
|
<span class="anchor-widget not-anchored">
|
|
<span class="anchor-label">not anchored 🔓</span>
|
|
</span>
|
|
{/if}
|
|
{/await}
|
|
{/if}
|
|
</div>
|
|
</header>
|
|
<div class="container center-content">
|
|
{#if tree.entries.length}
|
|
<div class="column-left">
|
|
<div class="source-tree">
|
|
<Tree {tree} {path} {fetchTree} {loadingPath} on:select={onSelect} />
|
|
</div>
|
|
</div>
|
|
<div class="column-right">
|
|
{#await getBlob}
|
|
<Loading small center />
|
|
{:then blob}
|
|
{#if utils.isMarkdownPath(blob.path)}
|
|
<Readme content={blob.content} />
|
|
{:else}
|
|
<Blob {blob} />
|
|
{/if}
|
|
{:catch}
|
|
<div class="error error-message file-not-found">
|
|
<header>
|
|
<div class="icon">🍂</div>
|
|
{#if path != "/"}
|
|
<div><code>{path}</code></div>
|
|
{/if}
|
|
</header>
|
|
{#if path == "/"}
|
|
The README could not be loaded.
|
|
{:else}
|
|
This path could not be loaded.
|
|
{/if}
|
|
</div>
|
|
{/await}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{:catch err}
|
|
<div class="container center-content">
|
|
<div class="error error-message text-small">
|
|
<!-- TODO: Differentiate between (1) commit doesn't exist and (2) failed
|
|
to fetch - this needs a change to the backend. -->
|
|
API request to <code class="text-small">{err.url}</code> failed
|
|
</div>
|
|
</div>
|
|
{/await}
|
|
</main>
|