Compare commits
6 Commits
f21984e7de
...
6ae20f9171
Author | SHA1 | Date |
---|---|---|
Tomáš Mládek | 6ae20f9171 | |
Tomáš Mládek | 5771f32736 | |
Tomáš Mládek | b480cf2e64 | |
Tomáš Mládek | 07c76423ac | |
Tomáš Mládek | 8932341445 | |
Tomáš Mládek | 1f270d6dc7 |
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@upnd/upend",
|
||||
"version": "0.4.1",
|
||||
"version": "0.5.0",
|
||||
"description": "Client library to interact with the UpEnd system.",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
|
|
@ -121,6 +121,7 @@ export class UpEntry extends UpObject implements IEntry {
|
|||
attribute: string;
|
||||
value: IValue;
|
||||
provenance: string;
|
||||
user: string;
|
||||
timestamp: string;
|
||||
|
||||
constructor(address: string, entry: IEntry, listing: UpListing) {
|
||||
|
@ -130,6 +131,7 @@ export class UpEntry extends UpObject implements IEntry {
|
|||
this.attribute = entry.attribute;
|
||||
this.value = entry.value;
|
||||
this.provenance = entry.provenance;
|
||||
this.user = entry.user;
|
||||
this.timestamp = entry.timestamp;
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@ export interface IEntry {
|
|||
value: IValue;
|
||||
/** The origin or provenance of the data entry (e.g. SYSTEM or USER API...) */
|
||||
provenance: string;
|
||||
/** The user who created the data entry. */
|
||||
user: string;
|
||||
/** The timestamp when the data entry was created in RFC 3339 format. */
|
||||
timestamp: string;
|
||||
}
|
||||
|
|
|
@ -21,10 +21,13 @@
|
|||
let files: File[] = [];
|
||||
let URLs: string[] = [];
|
||||
let uploading = false;
|
||||
let abortController: AbortController | undefined;
|
||||
|
||||
let progress: Record<string, number> = {};
|
||||
let totalProgress: number | undefined;
|
||||
|
||||
let filesElement: HTMLDivElement;
|
||||
|
||||
$: visible = files.length + URLs.length > 0;
|
||||
|
||||
addEmitter.on('files', (ev) => {
|
||||
|
@ -40,9 +43,15 @@
|
|||
uploading = true;
|
||||
|
||||
try {
|
||||
abortController = new AbortController();
|
||||
const addresses: string[] = [];
|
||||
for (const file of files) {
|
||||
for (const [idx, file] of files.entries()) {
|
||||
filesElement
|
||||
?.querySelectorAll('.entry')
|
||||
[idx]?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
|
||||
const address = await api.putBlob(file, {
|
||||
abortController,
|
||||
onProgress: (p) => {
|
||||
progress[file.name] = (p.loaded / p.total) * 100;
|
||||
totalProgress = Object.values(progress).reduce((a, b) => a + b, 0) / files.length;
|
||||
|
@ -50,6 +59,10 @@
|
|||
timeout: -1
|
||||
});
|
||||
addresses.push(address);
|
||||
|
||||
if (!uploading) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (addresses.length == 1) {
|
||||
|
@ -67,11 +80,16 @@
|
|||
}
|
||||
|
||||
function reset() {
|
||||
if (!uploading) {
|
||||
files = [];
|
||||
URLs = [];
|
||||
progress = {};
|
||||
if (uploading) {
|
||||
const msg = $i18n.t('Are you sure you want to cancel the upload?');
|
||||
if (!confirm(msg)) return;
|
||||
}
|
||||
abortController?.abort();
|
||||
|
||||
files = [];
|
||||
URLs = [];
|
||||
progress = {};
|
||||
uploading = false;
|
||||
}
|
||||
|
||||
function onKeydown(event: KeyboardEvent) {
|
||||
|
@ -84,14 +102,22 @@
|
|||
reset();
|
||||
}
|
||||
}
|
||||
|
||||
function onBeforeUnload(ev: BeforeUnloadEvent) {
|
||||
if (files.length || uploading) {
|
||||
ev.preventDefault();
|
||||
ev.returnValue = true;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:beforeunload={onBeforeUnload} />
|
||||
<svelte:body on:keydown={onKeydown} />
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions -->
|
||||
<div class="addmodal-container" class:visible class:uploading on:click={reset}>
|
||||
<div class="addmodal" on:click|stopPropagation>
|
||||
<div class="files">
|
||||
<div class="files" bind:this={filesElement}>
|
||||
{#each files as file}
|
||||
<div class="entry">
|
||||
<div class="row">
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
export let address: string | undefined = undefined;
|
||||
export let index: number;
|
||||
export let only: boolean;
|
||||
export let background: string | undefined = undefined;
|
||||
export let forceDetail = false;
|
||||
let shifted = false;
|
||||
let key = Math.random();
|
||||
|
@ -64,35 +63,13 @@
|
|||
window.addEventListener('mouseup', onMouseUp);
|
||||
}
|
||||
|
||||
let resultBackground = background;
|
||||
let imageBackground: string | undefined = undefined;
|
||||
$: {
|
||||
if (background?.startsWith('url(')) {
|
||||
imageBackground = background;
|
||||
resultBackground = 'transparent';
|
||||
} else {
|
||||
resultBackground = background;
|
||||
imageBackground = undefined;
|
||||
}
|
||||
resultBackground ||= 'var(--background-lighter)';
|
||||
}
|
||||
|
||||
function reload() {
|
||||
key = Math.random();
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="browse-column"
|
||||
class:detail
|
||||
style="--background: {resultBackground}"
|
||||
class:image-background={Boolean(imageBackground)}
|
||||
on:mousemove={(ev) => (shifted = ev.shiftKey)}
|
||||
>
|
||||
{#if imageBackground}
|
||||
<div class="background" style="background-image: {imageBackground}" />
|
||||
{/if}
|
||||
<div class="browse-column" class:detail on:mousemove={(ev) => (shifted = ev.shiftKey)}>
|
||||
<div class="view" style="--width: {width}px">
|
||||
<header>
|
||||
{#if address}
|
||||
|
@ -165,7 +142,7 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
background: var(--background);
|
||||
background: var(--background-lighter);
|
||||
color: var(--foreground-lighter);
|
||||
border: 1px solid var(--foreground-lightest);
|
||||
border-radius: 0.5em;
|
||||
|
|
|
@ -27,7 +27,6 @@
|
|||
const dispatch = createEventDispatcher<{
|
||||
resolved: string[];
|
||||
close: void;
|
||||
background: string | undefined;
|
||||
}>();
|
||||
|
||||
export let address: string;
|
||||
|
@ -393,19 +392,6 @@
|
|||
);
|
||||
}
|
||||
});
|
||||
|
||||
$: {
|
||||
const cover = $entity?.attr['COVER']?.[0];
|
||||
if (!cover) {
|
||||
dispatch('background', undefined);
|
||||
} else {
|
||||
switch (cover.value.t) {
|
||||
case 'Address':
|
||||
dispatch('background', `url('${api.getRaw(cover.value.c)}')`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
@ -527,13 +513,13 @@
|
|||
<h2>{$i18n.t('Attributes')}</h2>
|
||||
<EntryList
|
||||
entries={$entity?.attributes || []}
|
||||
columns={detail ? 'timestamp, provenance, attribute, value' : 'attribute, value'}
|
||||
columns={detail ? 'timestamp, user, provenance, attribute, value' : 'attribute, value'}
|
||||
on:change={onChange}
|
||||
/>
|
||||
<h2>{$i18n.t('Backlinks')}</h2>
|
||||
<EntryList
|
||||
entries={$entity?.backlinks || []}
|
||||
columns={detail ? 'timestamp, provenance, entity, attribute' : 'entity, attribute'}
|
||||
columns={detail ? 'timestamp, user, provenance, entity, attribute' : 'entity, attribute'}
|
||||
on:change={onChange}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -152,6 +152,22 @@
|
|||
dispatch('change', { type: 'upsert', attribute: ATTR_LABEL, value: ev.detail });
|
||||
}
|
||||
}
|
||||
|
||||
let background: string | undefined;
|
||||
$: background = $entity?.get('COVER')?.toString();
|
||||
|
||||
let resultBackground = background;
|
||||
let imageBackground: string | undefined = undefined;
|
||||
$: {
|
||||
if (background) {
|
||||
imageBackground = `url(${api.getRaw(background)})`;
|
||||
resultBackground = 'transparent';
|
||||
} else {
|
||||
resultBackground = background;
|
||||
imageBackground = undefined;
|
||||
}
|
||||
resultBackground ||= 'var(--background-lighter)';
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
@ -160,6 +176,8 @@
|
|||
class:right-active={address == $addresses[$index + 1]}
|
||||
class:selected={select && $selected.includes(address)}
|
||||
class:plain
|
||||
style="--background: {resultBackground}"
|
||||
class:image-background={Boolean(imageBackground)}
|
||||
>
|
||||
<div
|
||||
class="address"
|
||||
|
@ -167,6 +185,10 @@
|
|||
class:banner
|
||||
class:show-type={$entityInfo?.t === 'Url' && !addressIds.length}
|
||||
>
|
||||
{#if imageBackground}
|
||||
<div class="image-gradient"></div>
|
||||
<div class="image-background" style="background-image: {imageBackground}"></div>
|
||||
{/if}
|
||||
<HashBadge {address} />
|
||||
<div class="label" class:resolving title={displayLabel}>
|
||||
<Editable
|
||||
|
@ -264,6 +286,7 @@
|
|||
}
|
||||
|
||||
.address {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
|
||||
|
@ -275,7 +298,7 @@
|
|||
font-family: var(--monospace-font);
|
||||
line-break: anywhere;
|
||||
|
||||
background: var(--background-lighter);
|
||||
background: var(--background);
|
||||
border: 0.1em solid var(--foreground-lighter);
|
||||
border-radius: 0.2em;
|
||||
|
||||
|
@ -336,6 +359,29 @@
|
|||
&.banner .secondary {
|
||||
display: unset;
|
||||
}
|
||||
|
||||
.image-gradient {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(90deg, rgba(0, 0, 0, 0.66) 16%, var(--background) 66%);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.image-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: -2;
|
||||
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
filter: brightness(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
|
|
|
@ -166,7 +166,12 @@
|
|||
}}
|
||||
/>
|
||||
<div class="icon">
|
||||
<IconButton name="trash" color="#dc322f" on:click={() => removeEntity(entity)} />
|
||||
<IconButton
|
||||
plain
|
||||
name="trash"
|
||||
color="#dc322f"
|
||||
on:click={() => removeEntity(entity)}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="object">
|
||||
|
@ -181,7 +186,12 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<IconButton name="trash" color="#dc322f" on:click={() => removeEntity(entity)} />
|
||||
<IconButton
|
||||
plain
|
||||
name="trash"
|
||||
color="#dc322f"
|
||||
on:click={() => removeEntity(entity)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
|
@ -262,7 +272,9 @@
|
|||
.item {
|
||||
display: flex;
|
||||
.object {
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
|
@ -298,6 +310,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 0.25em;
|
||||
}
|
||||
|
||||
.add {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
|
||||
const TIMESTAMP_COL = 'timestamp';
|
||||
const PROVENANCE_COL = 'provenance';
|
||||
const USER_COL = 'user';
|
||||
const ENTITY_COL = 'entity';
|
||||
const ATTR_COL = 'attribute';
|
||||
const VALUE_COL = 'value';
|
||||
|
@ -188,6 +189,7 @@
|
|||
const COLUMN_LABELS: { [key: string]: string } = {
|
||||
timestamp: $i18n.t('Added at'),
|
||||
provenance: $i18n.t('Provenance'),
|
||||
user: $i18n.t('User'),
|
||||
entity: $i18n.t('Entity'),
|
||||
attribute: $i18n.t('Attribute'),
|
||||
value: $i18n.t('Value')
|
||||
|
@ -243,6 +245,16 @@
|
|||
</div>
|
||||
{:else if column == PROVENANCE_COL}
|
||||
<div class="cell">{entry.provenance}</div>
|
||||
{:else if column == USER_COL}
|
||||
<div class="cell">
|
||||
{#if entry.user}
|
||||
{entry.user}
|
||||
{:else}
|
||||
<div class="unset">
|
||||
{$i18n.t('unset')}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if column == ENTITY_COL}
|
||||
<div class="cell entity mark-entity">
|
||||
<UpObject
|
||||
|
|
Loading…
Reference in New Issue