389 lines
9.3 KiB
Svelte
389 lines
9.3 KiB
Svelte
<script lang="ts">
|
|
import { createEventDispatcher, getContext } from "svelte";
|
|
|
|
import HashBadge from "./HashBadge.svelte";
|
|
import UpObjectLabel from "./UpObjectLabel.svelte";
|
|
import UpLink from "./UpLink.svelte";
|
|
import Icon from "../utils/Icon.svelte";
|
|
import { readable, type Readable, writable } from "svelte/store";
|
|
import { notify, UpNotification } from "../../notifications";
|
|
import IconButton from "../utils/IconButton.svelte";
|
|
import { vaultInfo } from "../../util/info";
|
|
import type { BrowseContext } from "../../util/browse";
|
|
import { Query, type UpObject } from "@upnd/upend";
|
|
import type { ADDRESS_TYPE, EntityInfo } from "@upnd/upend/types";
|
|
import { useEntity } from "../../lib/entity";
|
|
import { i18n } from "../../i18n";
|
|
import api from "../../lib/api";
|
|
import { ATTR_IN, ATTR_LABEL, HIER_ROOT_ADDR } from "@upnd/upend/constants";
|
|
import { selected } from "../EntitySelect.svelte";
|
|
import { Any } from "@upnd/upend/query";
|
|
|
|
const dispatch = createEventDispatcher();
|
|
|
|
export let address: string;
|
|
export let labels: string[] | undefined = undefined;
|
|
export let link = false;
|
|
export let banner = false;
|
|
export let resolve = !(labels || []).length || banner;
|
|
export let backpath = 0;
|
|
export let select = true;
|
|
export let plain = false;
|
|
|
|
let entity: Readable<UpObject> = readable(undefined);
|
|
let entityInfo: Readable<EntityInfo> = writable(undefined);
|
|
$: if (resolve) ({ entity, entityInfo } = useEntity(address));
|
|
$: if (!resolve)
|
|
entityInfo = readable(undefined, (set) => {
|
|
api.addressToComponents(address).then((info) => {
|
|
set(info);
|
|
});
|
|
});
|
|
|
|
let hasFile = false;
|
|
$: {
|
|
if ($entityInfo?.t == "Hash" && banner) {
|
|
fetch(api.getRaw(address), {
|
|
method: "HEAD",
|
|
}).then((response) => {
|
|
hasFile = response.ok;
|
|
});
|
|
}
|
|
}
|
|
|
|
// Identification
|
|
let inferredIds: string[] = [];
|
|
$: inferredIds = $entity?.identify() || [];
|
|
let addressIds: string[] = [];
|
|
$: resolving = inferredIds.concat(labels || []).length == 0 && !$entity;
|
|
|
|
$: fetchAddressLabels(address);
|
|
|
|
async function fetchAddressLabels(address: string) {
|
|
addressIds = [];
|
|
await Promise.all(
|
|
(["Hash", "Uuid", "Attribute", "Url"] as ADDRESS_TYPE[]).map(
|
|
async (t) => {
|
|
if ((await api.getAddress(t)) == address) {
|
|
addressIds.push(`∈ ${t}`);
|
|
}
|
|
},
|
|
),
|
|
);
|
|
addressIds = addressIds;
|
|
}
|
|
|
|
let displayLabel = address;
|
|
$: {
|
|
const allLabels = []
|
|
.concat(inferredIds)
|
|
.concat(addressIds)
|
|
.concat(labels || []);
|
|
displayLabel = Array.from(new Set(allLabels)).join(" | ");
|
|
|
|
if (!displayLabel && $entityInfo?.t === "Attribute") {
|
|
displayLabel = `${$entityInfo.c}`;
|
|
}
|
|
displayLabel = displayLabel || address;
|
|
}
|
|
|
|
$: dispatch("resolved", inferredIds);
|
|
|
|
// Resolved backpath
|
|
let resolvedBackpath: string[] = [];
|
|
$: if (backpath) resolveBackpath();
|
|
|
|
async function resolveBackpath() {
|
|
resolvedBackpath = [];
|
|
let levels = 0;
|
|
let current = address;
|
|
while (levels < backpath && current !== HIER_ROOT_ADDR) {
|
|
const parent = await api.query(
|
|
Query.matches(`@${current}`, ATTR_IN, Any),
|
|
);
|
|
if (parent.entries.length) {
|
|
current = parent.entries[0].value.c as string;
|
|
const label = await api.query(
|
|
Query.matches(`@${current}`, ATTR_LABEL, Any),
|
|
);
|
|
if (label.entries.length) {
|
|
resolvedBackpath = [
|
|
label.entries[0].value.c as string,
|
|
...resolvedBackpath,
|
|
];
|
|
}
|
|
}
|
|
levels++;
|
|
}
|
|
}
|
|
|
|
// Navigation highlights
|
|
const context = getContext("browse") as BrowseContext | undefined;
|
|
const index = context?.index || undefined;
|
|
const addresses = context?.addresses || readable([]);
|
|
|
|
// Native open
|
|
function nativeOpen() {
|
|
notify.emit(
|
|
"notification",
|
|
new UpNotification(
|
|
$i18n.t("Opening {{identity}} in a default native application...", {
|
|
identity: inferredIds[0] || address,
|
|
}),
|
|
),
|
|
);
|
|
api
|
|
.nativeOpen(address)
|
|
.then(async (response) => {
|
|
if (!response.ok) {
|
|
throw new Error(`${response.statusText} - ${await response.text()}`);
|
|
}
|
|
if (response.headers.has("warning")) {
|
|
const warningText = response.headers
|
|
.get("warning")
|
|
.split(" ")
|
|
.slice(2)
|
|
.join(" ");
|
|
notify.emit(
|
|
"notification",
|
|
new UpNotification(warningText, "warning"),
|
|
);
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
notify.emit(
|
|
"notification",
|
|
new UpNotification(
|
|
$i18n.t("Failed to open in native application! ({{err}})", { err }),
|
|
"error",
|
|
),
|
|
);
|
|
});
|
|
}
|
|
</script>
|
|
|
|
<div
|
|
class="upobject"
|
|
class:left-active={address == $addresses[$index - 1]}
|
|
class:right-active={address == $addresses[$index + 1]}
|
|
class:selected={select && $selected.includes(address)}
|
|
class:plain
|
|
>
|
|
<div
|
|
class="address"
|
|
class:identified={inferredIds.length || addressIds.length || labels?.length}
|
|
class:banner
|
|
class:show-type={$entityInfo?.t === "Url" && !addressIds.length}
|
|
>
|
|
<HashBadge {address} />
|
|
<div class="separator" />
|
|
<div class="label" class:resolving title={displayLabel}>
|
|
<div class="label-inner">
|
|
{#if banner && hasFile}
|
|
<UpObjectLabel label={displayLabel} backpath={resolvedBackpath} />
|
|
{:else if link}
|
|
<UpLink to={{ entity: address }}>
|
|
<UpObjectLabel label={displayLabel} backpath={resolvedBackpath} />
|
|
</UpLink>
|
|
{:else}
|
|
<UpObjectLabel label={displayLabel} backpath={resolvedBackpath} />
|
|
{/if}
|
|
</div>
|
|
{#if $entity?.get("KEY")}
|
|
<div class="key">{$entity.get("KEY")}</div>
|
|
{/if}
|
|
<div class="secondary">
|
|
<div class="type">
|
|
{$entityInfo?.t}
|
|
{#if $entityInfo?.t === "Url" || $entityInfo?.t === "Attribute"}
|
|
— {$entityInfo.c}
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{#if banner}
|
|
{#if $entityInfo?.t === "Attribute"}
|
|
<div class="icon">
|
|
<UpLink
|
|
to={{ surfaceAttribute: $entityInfo.c }}
|
|
title={$i18n.t("Open on surface")}
|
|
>
|
|
<Icon name="cross" />
|
|
</UpLink>
|
|
</div>
|
|
{/if}
|
|
{#if $entityInfo?.t == "Hash"}
|
|
<div
|
|
class="icon"
|
|
title={hasFile
|
|
? $i18n.t("Download as file")
|
|
: $i18n.t("File not present in vault")}
|
|
>
|
|
<a
|
|
class="link-button"
|
|
class:disabled={!hasFile}
|
|
href="{api.apiUrl}/raw/{address}"
|
|
download={inferredIds[0]}
|
|
>
|
|
<Icon name="download" />
|
|
</a>
|
|
</div>
|
|
{#if $vaultInfo?.desktop && hasFile}
|
|
<div class="icon">
|
|
<IconButton
|
|
name="window-alt"
|
|
on:click={nativeOpen}
|
|
title={$i18n.t("Open in default application...")}
|
|
/>
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<style lang="scss">
|
|
@use "../../styles/colors";
|
|
|
|
.upobject {
|
|
border-radius: 4px;
|
|
|
|
&.left-active {
|
|
background: linear-gradient(90deg, colors.$orange 0%, transparent 100%);
|
|
padding: 2px 0 2px 2px;
|
|
}
|
|
|
|
&.right-active {
|
|
background: linear-gradient(90deg, transparent 0%, colors.$orange 100%);
|
|
padding: 2px 2px 2px 0;
|
|
}
|
|
|
|
&.plain .address {
|
|
border: none;
|
|
background: none;
|
|
padding: 0;
|
|
}
|
|
}
|
|
|
|
.address {
|
|
display: flex;
|
|
align-items: center;
|
|
|
|
padding: 0.1em 0.25em;
|
|
|
|
font-family: var(--monospace-font);
|
|
line-break: anywhere;
|
|
|
|
background: var(--background-lighter);
|
|
border: 0.1em solid var(--foreground-lighter);
|
|
border-radius: 0.2em;
|
|
|
|
&.banner {
|
|
border: 0.12em solid var(--foreground);
|
|
padding: 0.5em 0.25em;
|
|
}
|
|
|
|
&.identified {
|
|
font-family: var(--default-font);
|
|
font-size: 0.95em;
|
|
line-break: auto;
|
|
}
|
|
|
|
.label {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: baseline;
|
|
}
|
|
|
|
.label-inner {
|
|
max-width: 100%;
|
|
margin-right: 0.25em;
|
|
}
|
|
|
|
&.banner .label {
|
|
flex-direction: column;
|
|
gap: 0.1em;
|
|
}
|
|
|
|
.secondary {
|
|
font-size: 0.66em;
|
|
display: none;
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.key {
|
|
font-family: var(--monospace-font);
|
|
color: colors.$yellow;
|
|
opacity: 0.8;
|
|
|
|
&:before {
|
|
content: "⌘";
|
|
margin-right: 0.1em;
|
|
}
|
|
}
|
|
|
|
&.banner .key {
|
|
font-size: 0.66em;
|
|
}
|
|
|
|
&:not(.banner) .key {
|
|
flex-grow: 1;
|
|
text-align: right;
|
|
}
|
|
|
|
&.show-type .secondary,
|
|
&.banner .secondary {
|
|
display: unset;
|
|
}
|
|
}
|
|
|
|
.label {
|
|
flex-grow: 1;
|
|
min-width: 0;
|
|
|
|
:global(a) {
|
|
text-decoration: none;
|
|
}
|
|
}
|
|
|
|
.separator {
|
|
width: 0.5em;
|
|
}
|
|
|
|
.icon {
|
|
margin: 0 0.1em;
|
|
}
|
|
|
|
.resolving {
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.link-button {
|
|
opacity: 0.66;
|
|
transition:
|
|
opacity 0.2s,
|
|
color 0.2s;
|
|
|
|
&:hover {
|
|
opacity: 1;
|
|
color: var(--active-color, var(--primary));
|
|
}
|
|
}
|
|
|
|
.upobject {
|
|
transition:
|
|
margin 0.2s ease,
|
|
box-shadow 0.2s ease;
|
|
}
|
|
|
|
.selected {
|
|
margin: 0.12rem;
|
|
box-shadow: 0 0 0.1rem 0.11rem colors.$red;
|
|
}
|
|
|
|
.disabled {
|
|
pointer-events: none;
|
|
opacity: 0.7;
|
|
}
|
|
</style>
|