371 lines
8.6 KiB
Svelte
371 lines
8.6 KiB
Svelte
<script lang="ts">
|
|
import EntryList from "../components/widgets/EntryList.svelte";
|
|
import EntityList from "../components/widgets/EntityList.svelte";
|
|
import type { Widget } from "../components/EntryView.svelte";
|
|
import { Link, useNavigate } from "svelte-navigator";
|
|
import { UpListing } from "@upnd/upend";
|
|
import EntryView from "../components/EntryView.svelte";
|
|
import UpObject from "../components/display/UpObject.svelte";
|
|
import UpObjectCard from "../components/display/UpObjectCard.svelte";
|
|
import Spinner from "../components/utils/Spinner.svelte";
|
|
import api from "../lib/api";
|
|
import { query } from "../lib/entity";
|
|
import { vaultInfo } from "../util/info";
|
|
import { updateTitle } from "../util/title";
|
|
import { i18n } from "../i18n";
|
|
import {
|
|
ATTR_ADDED,
|
|
ATTR_IN,
|
|
ATTR_LABEL,
|
|
HIER_ROOT_ADDR,
|
|
} from "@upnd/upend/constants";
|
|
const navigate = useNavigate();
|
|
|
|
const roots = (async () => {
|
|
const data = await api.fetchRoots();
|
|
const listing = new UpListing(data);
|
|
return Object.values(listing.objects)
|
|
.filter((obj) => Boolean(obj.attr[ATTR_LABEL]))
|
|
.map((obj) => [obj.address, obj.identify().join(" | ")])
|
|
.sort(([_, i1], [__, i2]) => i1.localeCompare(i2));
|
|
})();
|
|
|
|
const groups = (async () => {
|
|
const data = await api.query(`(matches ? "${ATTR_IN}" ?)`);
|
|
|
|
const addresses = data.entries
|
|
.filter((e) => e.value.t === "Address")
|
|
.map((e) => e.value.c) as string[];
|
|
|
|
const sortedAddresses = [...new Set(addresses)]
|
|
.map((address) => ({
|
|
address,
|
|
count: addresses.filter((a) => a === address).length,
|
|
}))
|
|
.sort((a, b) => b.count - a.count);
|
|
|
|
const shortlist = sortedAddresses
|
|
.slice(0, 25)
|
|
.filter(({ address }) => address !== HIER_ROOT_ADDR);
|
|
|
|
const addressesString = shortlist
|
|
.map(({ address }) => `@${address}`)
|
|
.join(" ");
|
|
const labels = (
|
|
await api.query(`(matches (in ${addressesString}) "${ATTR_LABEL}" ? )`)
|
|
).entries.filter((e) => e.value.t === "String");
|
|
|
|
const display = shortlist.map(({ address, count }) => ({
|
|
address,
|
|
labels: labels
|
|
.filter((e) => e.entity === address)
|
|
.map((e) => e.value.c)
|
|
.sort() as string[],
|
|
count,
|
|
}));
|
|
|
|
display
|
|
.sort((a, b) => (a.labels[0] || "").localeCompare(b.labels[0] || ""))
|
|
.sort((a, b) => b.count - a.count);
|
|
|
|
return { groups: display, total: sortedAddresses.length };
|
|
})();
|
|
|
|
const { result: recentQuery } = query(`(matches ? "LAST_VISITED" ?)`);
|
|
$: recent = ($recentQuery?.entries || [])
|
|
.filter((e) => e.value.t == "Number")
|
|
.sort((a, b) => (b.value.c as number) - (a.value.c as number))
|
|
.slice(0, 25);
|
|
|
|
const { result: frequentQuery } = query(`(matches ? "NUM_VISITED" ?)`);
|
|
$: frequent = ($frequentQuery?.entries || [])
|
|
.filter((e) => e.value.t == "Number")
|
|
.sort((a, b) => (b.value.c as number) - (a.value.c as number))
|
|
.slice(0, 25);
|
|
|
|
const { result: latestQuery } = query(`(matches ? "${ATTR_ADDED}" ?)`);
|
|
$: latest = ($latestQuery?.entries || [])
|
|
.filter((e) => e.value.t == "Number")
|
|
.sort((a, b) => (b.value.c as number) - (a.value.c as number))
|
|
.slice(0, 25);
|
|
|
|
const shortWidgets: Widget[] = [
|
|
{
|
|
name: "List",
|
|
icon: "list-ul",
|
|
components: ({ entries }) => [
|
|
{
|
|
component: EntryList,
|
|
props: {
|
|
columns: "value, entity",
|
|
columnWidths: ["6em"],
|
|
orderByValue: true,
|
|
header: false,
|
|
entries,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "EntityList",
|
|
icon: "image",
|
|
components: ({ entries }) => [
|
|
{
|
|
component: EntityList,
|
|
props: {
|
|
entities: entries.map((e) => e.entity),
|
|
sort: false,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const longWidgets: Widget[] = [
|
|
{
|
|
name: "List",
|
|
icon: "list-ul",
|
|
components: ({ entries }) => [
|
|
{
|
|
component: EntryList,
|
|
props: {
|
|
columns: "value, entity",
|
|
columnWidths: ["13em"],
|
|
orderByValue: true,
|
|
header: false,
|
|
entries,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "EntityList",
|
|
icon: "image",
|
|
components: ({ entries }) => [
|
|
{
|
|
component: EntityList,
|
|
props: {
|
|
entities: entries.map((e) => e.entity),
|
|
sort: false,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
fetch("/api/options")
|
|
.then((res) => res.json())
|
|
.then((options) => {
|
|
if (!options.tree_mode) {
|
|
navigate("/setup");
|
|
}
|
|
});
|
|
|
|
updateTitle("Home");
|
|
</script>
|
|
|
|
<div class="home">
|
|
<h1>
|
|
{$vaultInfo?.name || "UpEnd"}
|
|
</h1>
|
|
|
|
<section class="roots">
|
|
<h2>{$i18n.t("Roots")}</h2>
|
|
{#await roots}
|
|
<Spinner centered />
|
|
{:then data}
|
|
<ul>
|
|
{#each data as [address, _]}
|
|
<li class="root">
|
|
<UpObjectCard {address} />
|
|
</li>
|
|
{:else}
|
|
<li>No roots :(</li>
|
|
{/each}
|
|
</ul>
|
|
{/await}
|
|
</section>
|
|
|
|
<section class="groups">
|
|
<h2>{$i18n.t("Groups")}</h2>
|
|
{#await groups}
|
|
<Spinner centered />
|
|
{:then data}
|
|
<ul>
|
|
{#each data.groups as group}
|
|
<li class="group">
|
|
<UpObject link address={group.address} labels={group.labels} />
|
|
<div class="count">{group.count}</div>
|
|
</li>
|
|
{:else}
|
|
<li>No groups?</li>
|
|
{/each}
|
|
{#if data.groups && data.total > data.groups.length}
|
|
<li>+ {data.total - data.groups.length}...</li>
|
|
{/if}
|
|
</ul>
|
|
{/await}
|
|
</section>
|
|
|
|
<div class="frecent">
|
|
{#if frequent.length || $frequentQuery === undefined}
|
|
<section class="frequent">
|
|
<h2>{$i18n.t("Frequently visited")}</h2>
|
|
{#if $frequentQuery == undefined}
|
|
<Spinner centered />
|
|
{:else}
|
|
<EntryView
|
|
--current-background="var(--background)"
|
|
entries={frequent}
|
|
widgets={shortWidgets}
|
|
/>
|
|
{/if}
|
|
</section>
|
|
{/if}
|
|
{#if recent.length || $recentQuery === undefined}
|
|
<section class="recent">
|
|
<h2>{$i18n.t("Recently visited")}</h2>
|
|
{#if $recentQuery == undefined}
|
|
<Spinner centered />
|
|
{:else}
|
|
<EntryView
|
|
--current-background="var(--background)"
|
|
entries={recent}
|
|
widgets={longWidgets}
|
|
/>
|
|
{/if}
|
|
</section>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if latest.length || $latestQuery === undefined}
|
|
<section class="latest">
|
|
<h2>{$i18n.t("Most recently added")}</h2>
|
|
{#if $latestQuery == undefined}
|
|
<Spinner centered />
|
|
{:else}
|
|
<EntryView
|
|
--current-background="var(--background)"
|
|
entries={latest}
|
|
widgets={longWidgets}
|
|
/>
|
|
{/if}
|
|
</section>
|
|
{/if}
|
|
|
|
<div class="button store-button">
|
|
<Link to="/store">{$i18n.t("View store statistics")}</Link>
|
|
</div>
|
|
|
|
<footer>
|
|
<div>
|
|
<strong>UpEnd</strong> - {$i18n.t(
|
|
"a database for the complex, the changing, and the indeterminate",
|
|
)}
|
|
</div>
|
|
<div>
|
|
<!-- svelte-ignore security-anchor-rel-noreferrer -->
|
|
<a target="_blank" href="https://upend.dev" class="version">
|
|
{$vaultInfo?.version || "???"}
|
|
</a>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
|
|
<style lang="scss">
|
|
.home {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
h1,
|
|
h2 {
|
|
text-align: center;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 32pt;
|
|
font-variant: small-caps;
|
|
margin: 2rem 0 0 0;
|
|
}
|
|
|
|
h2 {
|
|
margin: 2rem 0;
|
|
}
|
|
|
|
.latest,
|
|
.frecent {
|
|
margin: 0 2rem;
|
|
}
|
|
|
|
.frecent {
|
|
display: flex;
|
|
gap: 2rem;
|
|
& > * {
|
|
flex-basis: 50%;
|
|
}
|
|
|
|
@media screen and (max-width: 600px) {
|
|
flex-direction: column;
|
|
}
|
|
}
|
|
|
|
.roots {
|
|
ul {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0 2rem;
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
justify-content: center;
|
|
gap: 2rem;
|
|
}
|
|
|
|
.root {
|
|
font-size: 24px;
|
|
}
|
|
}
|
|
|
|
.groups {
|
|
ul {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0 2rem;
|
|
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.5em;
|
|
}
|
|
|
|
.group {
|
|
display: flex;
|
|
}
|
|
|
|
.count {
|
|
display: inline-block;
|
|
font-size: 0.66em;
|
|
margin-left: 0.25em;
|
|
}
|
|
}
|
|
|
|
footer {
|
|
border-top: 1px solid var(--foreground);
|
|
text-align: center;
|
|
margin: 3em 3em 1em 3em;
|
|
& > * {
|
|
margin: 0.5em;
|
|
}
|
|
}
|
|
|
|
.store-button {
|
|
display: inline-block;
|
|
padding: 1em;
|
|
margin: auto;
|
|
}
|
|
|
|
.version {
|
|
text-decoration: none;
|
|
opacity: 0.66;
|
|
}
|
|
</style>
|