[ui] first hackable version
parent
e51f1b43d3
commit
2c476fcf49
|
@ -0,0 +1,21 @@
|
||||||
|
import type { Address, IEntry, ListingResult, OrderedListing } from "./types";
|
||||||
|
|
||||||
|
export function listingAsOrdered(listing: ListingResult): OrderedListing {
|
||||||
|
const entries = Object.entries(listing) as [Address, IEntry][];
|
||||||
|
return entries
|
||||||
|
.sort(([_, a], [__, b]) => String(a.value.c).localeCompare(b.value.c))
|
||||||
|
.sort(([_, a], [__, b]) => String(a.value.t).localeCompare(b.value.t))
|
||||||
|
.sort(([_, a], [__, b]) => a.attribute.localeCompare(b.attribute));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function asDict(attributes: OrderedListing): {
|
||||||
|
[key: string]: string;
|
||||||
|
} {
|
||||||
|
const result = {} as { [key: string]: string };
|
||||||
|
attributes
|
||||||
|
.map(([_, attribute]) => attribute)
|
||||||
|
.forEach((attribute) => {
|
||||||
|
result[attribute.attribute] = attribute.value.c;
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"name": "upend",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Client library to interact with the UpEnd system.",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "Tomáš Mládek <t@mldk.cz>",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
export type Address = string;
|
||||||
|
export type VALUE_TYPE = "Value" | "Address" | "Invalid";
|
||||||
|
|
||||||
|
export interface IEntry {
|
||||||
|
entity: Address;
|
||||||
|
attribute: string;
|
||||||
|
value: { t: VALUE_TYPE; c: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListingResult {
|
||||||
|
[key: string]: IEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OrderedListing = [Address, IEntry][];
|
||||||
|
|
||||||
|
export interface Job {
|
||||||
|
title: string;
|
||||||
|
progress: number;
|
||||||
|
state: "InProgress" | "Done" | "Failed";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFile {
|
||||||
|
hash: string;
|
||||||
|
path: string;
|
||||||
|
valid: boolean;
|
||||||
|
added: string;
|
||||||
|
size: number;
|
||||||
|
mtime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VaultInfo {
|
||||||
|
name: string | null;
|
||||||
|
location: string;
|
||||||
|
}
|
|
@ -1,4 +1,11 @@
|
||||||
/node_modules/
|
/node_modules/
|
||||||
/public/build/
|
/public/build/
|
||||||
|
/public/vendor/
|
||||||
|
|
||||||
.DS_Store
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/sdks
|
||||||
|
!.yarn/versions
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,3 @@
|
||||||
|
nodeLinker: node-modules
|
||||||
|
|
||||||
|
yarnPath: .yarn/releases/yarn-3.1.0.cjs
|
177
ui/lib/entity.ts
177
ui/lib/entity.ts
|
@ -1,177 +0,0 @@
|
||||||
import { IEntry, ListingResult } from "@/types/base";
|
|
||||||
import { fetcher } from "@/utils";
|
|
||||||
import useSWRV from "swrv";
|
|
||||||
import { computed, ComputedRef, Ref } from "vue";
|
|
||||||
|
|
||||||
export function useEntity(
|
|
||||||
address: string | (() => string),
|
|
||||||
condition?: () => Boolean
|
|
||||||
) {
|
|
||||||
const { data, error, mutate } = useSWRV<ListingResult, unknown>(
|
|
||||||
() =>
|
|
||||||
condition === undefined || condition()
|
|
||||||
? `/api/obj/${typeof address === "string" ? address : address()}`
|
|
||||||
: null,
|
|
||||||
fetcher,
|
|
||||||
{ revalidateOnFocus: false }
|
|
||||||
);
|
|
||||||
|
|
||||||
const entries = computed(() => {
|
|
||||||
if (data?.value) {
|
|
||||||
const entries = Object.entries(data.value) as [string, IEntry][];
|
|
||||||
return entries
|
|
||||||
.sort(([_, a], [__, b]) => String(a.value.c).localeCompare(b.value.c))
|
|
||||||
.sort(([_, a], [__, b]) => String(a.value.t).localeCompare(b.value.t))
|
|
||||||
.sort(([_, a], [__, b]) => a.attribute.localeCompare(b.attribute));
|
|
||||||
} else {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const attributes = computed(() => {
|
|
||||||
const addr = typeof address === "string" ? address : address();
|
|
||||||
return entries.value.filter(([_, e]) => e.entity === addr);
|
|
||||||
});
|
|
||||||
|
|
||||||
const backlinks = computed(() => {
|
|
||||||
const addr = typeof address === "string" ? address : address();
|
|
||||||
return entries.value.filter(([_, e]) => e.entity !== addr);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
entries,
|
|
||||||
attributes,
|
|
||||||
backlinks,
|
|
||||||
data,
|
|
||||||
error,
|
|
||||||
mutate
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function query(
|
|
||||||
query: string | (() => string),
|
|
||||||
condition?: () => Boolean
|
|
||||||
) {
|
|
||||||
const { data, error, mutate } = useSWRV<ListingResult, unknown>(
|
|
||||||
() =>
|
|
||||||
condition === undefined || condition()
|
|
||||||
? `/api/obj?query=${typeof query === "string" ? query : query()}`
|
|
||||||
: null,
|
|
||||||
fetcher,
|
|
||||||
{ revalidateOnFocus: false }
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = computed(() => {
|
|
||||||
if (data?.value) {
|
|
||||||
const entries = Object.entries(data.value) as [string, IEntry][];
|
|
||||||
return entries
|
|
||||||
.sort(([_, a], [__, b]) => String(a.value.c).localeCompare(b.value.c))
|
|
||||||
.sort(([_, a], [__, b]) => String(a.value.t).localeCompare(b.value.t))
|
|
||||||
.sort(([_, a], [__, b]) => a.attribute.localeCompare(b.attribute));
|
|
||||||
} else {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
result,
|
|
||||||
data,
|
|
||||||
error,
|
|
||||||
mutate
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EntityIdentification {
|
|
||||||
type: string;
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function identify(
|
|
||||||
attributes: ComputedRef<[string, IEntry][]>,
|
|
||||||
backlinks: ComputedRef<[string, IEntry][]>
|
|
||||||
): ComputedRef<EntityIdentification[]> {
|
|
||||||
// Get all entries where the object is linked
|
|
||||||
const hasEntries = computed(() => {
|
|
||||||
return backlinks.value
|
|
||||||
.filter(([_, entry]) => entry.attribute === "HAS")
|
|
||||||
.map(([addr, _]) => addr);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Out of those relations, retrieve their ALIAS attrs
|
|
||||||
const { data: hasListing } = query(() => {
|
|
||||||
return (
|
|
||||||
hasEntries.value &&
|
|
||||||
`(matches (in ${hasEntries.value
|
|
||||||
.map(e => `"${e}"`)
|
|
||||||
.join(" ")}) "ALIAS" ?)`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const aliasValues: ComputedRef<string[]> = computed(() => {
|
|
||||||
return Object.values(hasListing.value || {}).map(entry => {
|
|
||||||
return entry.value.c;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get all identities of the object
|
|
||||||
const isEntries = computed(() => {
|
|
||||||
return attributes.value
|
|
||||||
.filter(([_, entry]) => entry.attribute === "IS")
|
|
||||||
.map(([_, entry]) => entry.value.c);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Out of those, retrieve their TYPE_ID entries
|
|
||||||
const { data: typeIdListing } = query(() => {
|
|
||||||
return (
|
|
||||||
isEntries.value &&
|
|
||||||
`(matches (in ${isEntries.value
|
|
||||||
.map(e => `"${e}"`)
|
|
||||||
.join(" ")}) "TYPE_ID" ?)`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const typeIdAttributes: ComputedRef<[string, string][]> = computed(() => {
|
|
||||||
return Object.values(typeIdListing.value || {}).map(entry => {
|
|
||||||
return [entry.entity, entry.value.c];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Finally, filter own object's attributes according to TYPE_IDs
|
|
||||||
return computed(() => {
|
|
||||||
// For each identity/TYPE_ID pair
|
|
||||||
return typeIdAttributes.value
|
|
||||||
.map(([type, attrName]) => {
|
|
||||||
// And each associated TYPE_ID attribute...
|
|
||||||
// return own matchin attributes
|
|
||||||
return attributes.value
|
|
||||||
.filter(([_, e]) => e.attribute === attrName)
|
|
||||||
.map(([_, attr]) => {
|
|
||||||
return {
|
|
||||||
type,
|
|
||||||
value: attr.value.c
|
|
||||||
};
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.flat()
|
|
||||||
.concat(
|
|
||||||
aliasValues.value.map(value => {
|
|
||||||
return {
|
|
||||||
type: "ALIAS",
|
|
||||||
value
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function asDict(
|
|
||||||
attributes: [string, IEntry][]
|
|
||||||
): { [key: string]: string } {
|
|
||||||
const result = {} as { [key: string]: string };
|
|
||||||
attributes
|
|
||||||
.map(([_, attribute]) => attribute)
|
|
||||||
.forEach(attribute => {
|
|
||||||
result[attribute.attribute] = attribute.value.c;
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
import { ComponentOptions } from "vue";
|
|
||||||
|
|
||||||
export class UpType {
|
|
||||||
address: string;
|
|
||||||
name: string | null = null;
|
|
||||||
attributes: string[] = [];
|
|
||||||
|
|
||||||
constructor(address: string) {
|
|
||||||
this.address = address;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get icon(): string | undefined {
|
|
||||||
return this.name ? TYPE_ICONS[this.name] : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get widgetInfo(): Widget | undefined {
|
|
||||||
return this.name ? TYPE_WIDGETS[this.name] : undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Widget {
|
|
||||||
name: string;
|
|
||||||
icon?: string;
|
|
||||||
components: ComponentOptions[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const TYPE_ICONS: { [key: string]: string } = {
|
|
||||||
BLOB: "box",
|
|
||||||
HIER: "folder"
|
|
||||||
};
|
|
||||||
|
|
||||||
const TYPE_WIDGETS: { [key: string]: Widget } = {
|
|
||||||
KSX_TRACK_MOODS: {
|
|
||||||
name: "ksx-track-compass",
|
|
||||||
icon: "plus-square",
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
name: "Compass",
|
|
||||||
id: "compass_tint_energy",
|
|
||||||
props: {
|
|
||||||
xAttrName: "KSX_TINT",
|
|
||||||
yAttrName: "KSX_ENERGY",
|
|
||||||
xLabel: "Lightsoft // Heavydark",
|
|
||||||
yLabel: "Chill // Extreme"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Compass",
|
|
||||||
id: "compass_seriousness_materials",
|
|
||||||
props: {
|
|
||||||
xAttrName: "KSX_SERIOUSNESS",
|
|
||||||
yAttrName: "KSX_MATERIALS",
|
|
||||||
xLabel: "Dionysia // Apollonia",
|
|
||||||
yLabel: "Natural // Reinforced"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -11,10 +11,16 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-commonjs": "^17.0.0",
|
"@rollup/plugin-commonjs": "^17.0.0",
|
||||||
"@rollup/plugin-node-resolve": "^11.0.0",
|
"@rollup/plugin-node-resolve": "^11.0.0",
|
||||||
"@rollup/plugin-typescript": "^8.0.0",
|
"@rollup/plugin-replace": "^3.0.0",
|
||||||
|
"@rollup/plugin-typescript": "^8.3.0",
|
||||||
"@tsconfig/svelte": "^2.0.0",
|
"@tsconfig/svelte": "^2.0.0",
|
||||||
"rollup": "^2.3.4",
|
"@types/history": "^4.7.9",
|
||||||
|
"@types/lru-cache": "^5.1.1",
|
||||||
|
"postcss": "^8.3.11",
|
||||||
|
"rollup": "^2.59.0",
|
||||||
|
"rollup-plugin-copy": "^3.4.0",
|
||||||
"rollup-plugin-css-only": "^3.1.0",
|
"rollup-plugin-css-only": "^3.1.0",
|
||||||
|
"rollup-plugin-dev": "^2.0.0",
|
||||||
"rollup-plugin-livereload": "^2.0.0",
|
"rollup-plugin-livereload": "^2.0.0",
|
||||||
"rollup-plugin-svelte": "^7.0.0",
|
"rollup-plugin-svelte": "^7.0.0",
|
||||||
"rollup-plugin-terser": "^7.0.0",
|
"rollup-plugin-terser": "^7.0.0",
|
||||||
|
@ -26,6 +32,16 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@shoelace-style/shoelace": "^2.0.0-beta.58",
|
"@shoelace-style/shoelace": "^2.0.0-beta.58",
|
||||||
"sirv-cli": "^1.0.0"
|
"date-fns": "^2.25.0",
|
||||||
}
|
"filesize": "^8.0.6",
|
||||||
|
"history": "^5.1.0",
|
||||||
|
"lru-cache": "^6.0.0",
|
||||||
|
"normalize.css": "^8.0.1",
|
||||||
|
"sass": "^1.43.4",
|
||||||
|
"sirv-cli": "^1.0.0",
|
||||||
|
"sswr": "^1.3.1",
|
||||||
|
"svelte-navigator": "^3.1.5",
|
||||||
|
"upend": "file:../tools/upend_js"
|
||||||
|
},
|
||||||
|
"packageManager": "yarn@3.1.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,10 @@
|
||||||
<meta charset='utf-8'>
|
<meta charset='utf-8'>
|
||||||
<meta name='viewport' content='width=device-width,initial-scale=1'>
|
<meta name='viewport' content='width=device-width,initial-scale=1'>
|
||||||
|
|
||||||
<title>Svelte app</title>
|
<title>UpEnd</title>
|
||||||
|
|
||||||
<link rel='icon' type='image/png' href='/favicon.png'>
|
<link rel='icon' type='image/png' href='/favicon.png'>
|
||||||
<link rel='stylesheet' href='/global.css'>
|
<!-- <link rel='stylesheet' href='/global.css'> -->
|
||||||
<link rel='stylesheet' href='/build/bundle.css'>
|
<link rel='stylesheet' href='/build/bundle.css'>
|
||||||
|
|
||||||
<script defer src='/build/bundle.js'></script>
|
<script defer src='/build/bundle.js'></script>
|
||||||
|
|
|
@ -1,39 +1,18 @@
|
||||||
|
import replace from "@rollup/plugin-replace";
|
||||||
import svelte from "rollup-plugin-svelte";
|
import svelte from "rollup-plugin-svelte";
|
||||||
import commonjs from "@rollup/plugin-commonjs";
|
import commonjs from "@rollup/plugin-commonjs";
|
||||||
|
import path from "path";
|
||||||
|
import copy from "rollup-plugin-copy";
|
||||||
import resolve from "@rollup/plugin-node-resolve";
|
import resolve from "@rollup/plugin-node-resolve";
|
||||||
import livereload from "rollup-plugin-livereload";
|
import livereload from "rollup-plugin-livereload";
|
||||||
import { terser } from "rollup-plugin-terser";
|
import { terser } from "rollup-plugin-terser";
|
||||||
import sveltePreprocess from "svelte-preprocess";
|
import sveltePreprocess from "svelte-preprocess";
|
||||||
import typescript from "@rollup/plugin-typescript";
|
import typescript from "@rollup/plugin-typescript";
|
||||||
import css from "rollup-plugin-css-only";
|
import css from "rollup-plugin-css-only";
|
||||||
|
import dev from "rollup-plugin-dev";
|
||||||
|
|
||||||
const production = !process.env.ROLLUP_WATCH;
|
const production = !process.env.ROLLUP_WATCH;
|
||||||
|
|
||||||
function serve() {
|
|
||||||
let server;
|
|
||||||
|
|
||||||
function toExit() {
|
|
||||||
if (server) server.kill(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
writeBundle() {
|
|
||||||
if (server) return;
|
|
||||||
server = require("child_process").spawn(
|
|
||||||
"npm",
|
|
||||||
["run", "start", "--", "--dev"],
|
|
||||||
{
|
|
||||||
stdio: ["ignore", "inherit", "inherit"],
|
|
||||||
shell: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
process.on("SIGTERM", toExit);
|
|
||||||
process.on("exit", toExit);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
input: "src/main.ts",
|
input: "src/main.ts",
|
||||||
output: {
|
output: {
|
||||||
|
@ -43,12 +22,18 @@ export default {
|
||||||
file: "public/build/bundle.js",
|
file: "public/build/bundle.js",
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
// To fix `history`
|
||||||
|
replace({
|
||||||
|
"process.env.NODE_ENV": JSON.stringify("production"),
|
||||||
|
}),
|
||||||
|
|
||||||
svelte({
|
svelte({
|
||||||
preprocess: sveltePreprocess({ sourceMap: !production }),
|
preprocess: sveltePreprocess({ sourceMap: !production }),
|
||||||
compilerOptions: {
|
compilerOptions: {
|
||||||
// enable run-time checks when not in production
|
// enable run-time checks when not in production
|
||||||
dev: !production,
|
dev: !production,
|
||||||
},
|
},
|
||||||
|
exclude: [path.resolve(__dirname, "public/vendor")],
|
||||||
}),
|
}),
|
||||||
// we'll extract any component CSS out into
|
// we'll extract any component CSS out into
|
||||||
// a separate file - better for performance
|
// a separate file - better for performance
|
||||||
|
@ -70,20 +55,29 @@ export default {
|
||||||
}),
|
}),
|
||||||
|
|
||||||
copy({
|
copy({
|
||||||
|
copyOnce: true,
|
||||||
|
hook: "closeBundle",
|
||||||
targets: [
|
targets: [
|
||||||
{
|
{
|
||||||
src: path.resolve(
|
src: path.resolve(
|
||||||
__dirname,
|
__dirname,
|
||||||
"node_modules/@shoelace-style/shoelace/dist/assets"
|
"node_modules/@shoelace-style/shoelace/dist/assets"
|
||||||
),
|
),
|
||||||
dest: path.resolve(__dirname, "assets/shoelace"),
|
dest: path.resolve(__dirname, "public/vendor/shoelace"),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// In dev mode, call `npm run start` once
|
!production &&
|
||||||
// the bundle has been generated
|
dev({
|
||||||
!production && serve(),
|
dirs: ["public"],
|
||||||
|
proxy: [
|
||||||
|
{
|
||||||
|
from: "/api/",
|
||||||
|
to: "http://localhost:8093/api/",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
|
||||||
// Watch the `public` directory and refresh the
|
// Watch the `public` directory and refresh the
|
||||||
// browser on changes when not in production
|
// browser on changes when not in production
|
||||||
|
|
|
@ -1,30 +1,117 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let name: string;
|
import { Router, Route, createHistory } from "svelte-navigator";
|
||||||
|
import createHashSource from "./util/history";
|
||||||
|
import Header from "./layout/Header.svelte";
|
||||||
|
import Home from "./views/Home.svelte";
|
||||||
|
import { setBasePath } from "@shoelace-style/shoelace/dist/utilities/base-path.js";
|
||||||
|
import "@shoelace-style/shoelace/dist/components/input/input.js";
|
||||||
|
import "@shoelace-style/shoelace/dist/components/icon-button/icon-button.js";
|
||||||
|
import Browse from "./views/Browse.svelte";
|
||||||
|
|
||||||
|
setBasePath("/vendor/shoelace");
|
||||||
|
|
||||||
|
$: document.body.classList.toggle(
|
||||||
|
"sl-theme-dark",
|
||||||
|
window.matchMedia &&
|
||||||
|
window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
|
);
|
||||||
|
|
||||||
|
const history = createHistory(createHashSource());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main>
|
<Router {history} primary={false}>
|
||||||
<h1>Hello {name}!</h1>
|
<Header />
|
||||||
<p>Visit the <a href="https://svelte.dev/tutorial">Svelte tutorial</a> to learn how to build Svelte apps.</p>
|
<Route path="/"><Home /></Route>
|
||||||
</main>
|
<Route path="/browse/*addresses" let:params>
|
||||||
|
<Browse />
|
||||||
|
</Route>
|
||||||
|
</Router>
|
||||||
|
|
||||||
<style>
|
<style global lang="scss">
|
||||||
main {
|
@use "../node_modules/normalize.css/normalize.css";
|
||||||
text-align: center;
|
@use "../node_modules/@shoelace-style/shoelace/dist/themes/light.css";
|
||||||
padding: 1em;
|
@use "../node_modules/@shoelace-style/shoelace/dist/themes/dark.css";
|
||||||
max-width: 240px;
|
@import url("/assets/fonts/inter.css");
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
html {
|
||||||
color: #ff3e00;
|
--default-font: "Inter", sans-serif;
|
||||||
text-transform: uppercase;
|
--foreground: #2c3e50;
|
||||||
font-size: 4em;
|
--background: white;
|
||||||
font-weight: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
b {
|
||||||
main {
|
color: red;
|
||||||
max-width: none;
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</style>
|
@supports (font-variation-settings: normal) {
|
||||||
|
html {
|
||||||
|
--default-font: "Inter var", sans-serif;
|
||||||
|
font-feature-settings: "ss02" on;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
html {
|
||||||
|
--foreground: white;
|
||||||
|
--background: #2c3e50;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#app,
|
||||||
|
#root {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
font-family: var(--default-font);
|
||||||
|
color: var(--foreground);
|
||||||
|
background: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
color: var(--foreground);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
margin: 1rem 0;
|
||||||
|
|
||||||
|
--monospace-font: "Fira Code", "Consolas", "JetBrains Mono", "Inconsolata",
|
||||||
|
monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main,
|
||||||
|
#header {
|
||||||
|
margin: 0 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
background: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer > * {
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:visited {
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { identify, useEntity } from "../lib/entity";
|
||||||
|
|
||||||
|
import HashBadge from "./HashBadge.svelte";
|
||||||
|
|
||||||
|
import Marquee from "./Marquee.svelte";
|
||||||
|
import UpLink from "./UpLink.svelte";
|
||||||
|
|
||||||
|
export let address: string;
|
||||||
|
export let link = false;
|
||||||
|
export let isFile = false;
|
||||||
|
export let resolve = true;
|
||||||
|
|
||||||
|
// Identification
|
||||||
|
let inferredIds = [];
|
||||||
|
const { attributes, backlinks } = useEntity(address, () => resolve);
|
||||||
|
$: {
|
||||||
|
if (resolve) {
|
||||||
|
identify($attributes, $backlinks).then((inferredEntries) => {
|
||||||
|
inferredIds = inferredEntries.map((eid) => eid.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="address" class:identified={Boolean(inferredIds)}>
|
||||||
|
<HashBadge {address} />
|
||||||
|
<Marquee>
|
||||||
|
{#if isFile}
|
||||||
|
<UpLink to={{ entity: address }}>
|
||||||
|
{address}
|
||||||
|
</UpLink>
|
||||||
|
{:else if link}
|
||||||
|
<UpLink to={{ entity: address }}>
|
||||||
|
{inferredIds.join(" | ") || address}
|
||||||
|
</UpLink>
|
||||||
|
{:else}
|
||||||
|
{inferredIds.join(" | ") || address}
|
||||||
|
{/if}
|
||||||
|
</Marquee>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.address {
|
||||||
|
font-family: var(--monospace-font);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&,
|
||||||
|
& a {
|
||||||
|
line-break: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.identified {
|
||||||
|
font-family: var(--default-font);
|
||||||
|
font-size: 0.95em;
|
||||||
|
line-break: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hash-badge {
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,148 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher, SvelteComponent } from "svelte";
|
||||||
|
import type { IEntry } from "upend/types";
|
||||||
|
import UpLink from "./UpLink.svelte";
|
||||||
|
import type { Component, UpType, Widget } from "../lib/types";
|
||||||
|
import Table from "./widgets/Table.svelte";
|
||||||
|
const dispatcher = createEventDispatcher();
|
||||||
|
|
||||||
|
export let attributes: [string, IEntry][];
|
||||||
|
export let type: UpType | undefined = undefined;
|
||||||
|
export let address: String;
|
||||||
|
export let title: String | undefined = undefined;
|
||||||
|
export let editable = false;
|
||||||
|
export let reverse = false;
|
||||||
|
|
||||||
|
let currentWidget = "table";
|
||||||
|
|
||||||
|
let availableWidgets: Widget[] = [];
|
||||||
|
$: {
|
||||||
|
availableWidgets = [
|
||||||
|
{
|
||||||
|
name: "table",
|
||||||
|
icon: "table",
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
component: Table,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (type?.widgetInfo) {
|
||||||
|
availableWidgets = [type.widgetInfo, ...availableWidgets];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let components: Component[] = [];
|
||||||
|
$: {
|
||||||
|
components = availableWidgets.find(
|
||||||
|
(w) => w.name === currentWidget
|
||||||
|
)!.components;
|
||||||
|
}
|
||||||
|
|
||||||
|
function processChange() {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="attribute-view">
|
||||||
|
<header>
|
||||||
|
<h3>
|
||||||
|
{#if type}
|
||||||
|
<UpLink to={{ entity: type.address }}>
|
||||||
|
{#if type.icon}
|
||||||
|
<sl-icon name={type.icon} />
|
||||||
|
{/if}
|
||||||
|
{type.name || "???"}
|
||||||
|
</UpLink>
|
||||||
|
{:else}
|
||||||
|
{title || "???"}
|
||||||
|
{/if}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{#if availableWidgets.length > 1 || editable}
|
||||||
|
<div class="views">
|
||||||
|
{#each availableWidgets as widget (widget.name)}
|
||||||
|
<sl-icon-button
|
||||||
|
name={widget.icon || "question-diamond"}
|
||||||
|
class:active={widget.name === currentWidget}
|
||||||
|
on:click={() => (currentWidget = widget.name)}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{#each components as component}
|
||||||
|
<svelte:component
|
||||||
|
this={component.component}
|
||||||
|
{...component.props || {}}
|
||||||
|
{attributes}
|
||||||
|
{editable}
|
||||||
|
{reverse}
|
||||||
|
on:edit={processChange}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
section {
|
||||||
|
position: relative;
|
||||||
|
overflow: visible;
|
||||||
|
|
||||||
|
margin-top: 1.66em;
|
||||||
|
padding: 1ex 1em;
|
||||||
|
|
||||||
|
border: 1px solid var(--foreground);
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
header {
|
||||||
|
margin-bottom: 0.2em;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
position: absolute;
|
||||||
|
top: -0.66em;
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
background: var(--background);
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0 0.75ex;
|
||||||
|
|
||||||
|
sl-icon {
|
||||||
|
margin-bottom: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
left: 1ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.views {
|
||||||
|
right: 1ex;
|
||||||
|
|
||||||
|
font-size: 18px;
|
||||||
|
|
||||||
|
sl-icon-button {
|
||||||
|
&::part(base) {
|
||||||
|
padding: 0 calc(0.75ex / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
&::part(base) {
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,58 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
|
const BADGE_HEIGHT = 3;
|
||||||
|
export let address: string;
|
||||||
|
|
||||||
|
let canvas: HTMLCanvasElement | undefined;
|
||||||
|
let width = 0;
|
||||||
|
|
||||||
|
const bytes = [...address].map((c) => c.charCodeAt(0));
|
||||||
|
while (bytes.length % (3 * BADGE_HEIGHT) !== 0) {
|
||||||
|
bytes.push(bytes[bytes.length - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
width = Math.ceil(bytes.length / 3 / BADGE_HEIGHT);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const ctx = canvas?.getContext("2d");
|
||||||
|
if (!ctx) {
|
||||||
|
console.warn("Couldn't initialize canvas!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let idx = 0;
|
||||||
|
while (bytes.length > 0) {
|
||||||
|
const tmp = [];
|
||||||
|
while (bytes.length > 0 && tmp.length < 3) {
|
||||||
|
tmp.push(bytes.shift());
|
||||||
|
}
|
||||||
|
while (tmp.length < 3) {
|
||||||
|
tmp.push(tmp[tmp.length - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const r = (tmp[0]! / 128) * 255;
|
||||||
|
const g = (tmp[1]! / 128) * 255;
|
||||||
|
const b = (tmp[2]! / 128) * 255;
|
||||||
|
ctx.fillStyle = `rgb(${r},${g},${b})`;
|
||||||
|
ctx.fillRect(Math.floor(idx / BADGE_HEIGHT), idx % BADGE_HEIGHT, 1, 1);
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="hash-badge">
|
||||||
|
<canvas bind:this={canvas} {width} height="3" title={address} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.hash-badge {
|
||||||
|
display: inline-block;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hash-badge canvas {
|
||||||
|
height: 100%;
|
||||||
|
image-rendering: optimizeSpeed;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,159 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import AttributeView from "./AttributeView.svelte";
|
||||||
|
import { query, useEntity } from "../lib/entity";
|
||||||
|
import Address from "./Address.svelte";
|
||||||
|
import { UpType } from "../lib/types";
|
||||||
|
import type { IEntry } from "upend/types";
|
||||||
|
|
||||||
|
export let address: string;
|
||||||
|
export let editable = false;
|
||||||
|
|
||||||
|
const { error, revalidate, attributes, backlinks } = useEntity(address);
|
||||||
|
|
||||||
|
$: allTypeAddresses = $attributes
|
||||||
|
.map(([_, attr]) => attr)
|
||||||
|
.filter((attr) => attr.attribute == "IS")
|
||||||
|
.map((attr) => attr.value.c);
|
||||||
|
|
||||||
|
$: allTypeEntries = query(
|
||||||
|
() =>
|
||||||
|
`(matches (in ${allTypeAddresses
|
||||||
|
.map((addr) => `"${addr}"`)
|
||||||
|
.join(" ")}) ? ?)`
|
||||||
|
).result;
|
||||||
|
|
||||||
|
let allTypes: { [key: string]: UpType } = {};
|
||||||
|
$: {
|
||||||
|
allTypes = {};
|
||||||
|
$allTypeEntries.forEach(([_, entry]) => {
|
||||||
|
if (allTypes[entry.entity] === undefined) {
|
||||||
|
allTypes[entry.entity] = new UpType(entry.entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (entry.attribute) {
|
||||||
|
case "TYPE":
|
||||||
|
allTypes[entry.entity].name = entry.value.c;
|
||||||
|
break;
|
||||||
|
case "TYPE_HAS":
|
||||||
|
case "TYPE_REQUIRES":
|
||||||
|
case "TYPE_ID":
|
||||||
|
allTypes[entry.entity].attributes.push(entry.value.c);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
allTypes = allTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
let typedAttributes = {} as { [key: string]: [string, IEntry][] };
|
||||||
|
let untypedAttributes = [] as [string, IEntry][];
|
||||||
|
|
||||||
|
$: {
|
||||||
|
typedAttributes = {};
|
||||||
|
untypedAttributes = [];
|
||||||
|
|
||||||
|
$attributes.forEach(([entryAddr, entry]) => {
|
||||||
|
const entryTypes = Object.entries(allTypes).filter(([_, t]) =>
|
||||||
|
t.attributes.includes(entry.attribute)
|
||||||
|
);
|
||||||
|
if (entryTypes.length > 0) {
|
||||||
|
entryTypes.forEach(([addr, _]) => {
|
||||||
|
if (typedAttributes[addr] == undefined) {
|
||||||
|
typedAttributes[addr] = [];
|
||||||
|
}
|
||||||
|
typedAttributes[addr].push([entryAddr, entry]);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
untypedAttributes.push([entryAddr, entry]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
typedAttributes = typedAttributes;
|
||||||
|
untypedAttributes = untypedAttributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
$: filteredUntypedAttributes = untypedAttributes.filter(
|
||||||
|
([_, entry]) =>
|
||||||
|
entry.attribute !== "IS" ||
|
||||||
|
!Object.keys(typedAttributes).includes(entry.value.c)
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="inspect">
|
||||||
|
<h2>
|
||||||
|
<Address
|
||||||
|
{address}
|
||||||
|
isFile={$backlinks.some(([_, e]) => e.attribute === "FILE_IS")}
|
||||||
|
/>
|
||||||
|
</h2>
|
||||||
|
<blob-preview :address="address" />
|
||||||
|
{#if !$error}
|
||||||
|
<div>
|
||||||
|
{#each Object.entries(typedAttributes) as [typeAddr, attributes] (typeAddr)}
|
||||||
|
<AttributeView
|
||||||
|
{editable}
|
||||||
|
{address}
|
||||||
|
type={allTypes[typeAddr]}
|
||||||
|
{attributes}
|
||||||
|
on:edit={revalidate}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
{#if filteredUntypedAttributes.length > 0 || editable}
|
||||||
|
<AttributeView
|
||||||
|
title="Other attributes"
|
||||||
|
{editable}
|
||||||
|
{address}
|
||||||
|
attributes={untypedAttributes}
|
||||||
|
on:change={revalidate}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{#if $backlinks.length > 0}
|
||||||
|
<AttributeView
|
||||||
|
title={`Referred to (${$backlinks.length})`}
|
||||||
|
{address}
|
||||||
|
attributes={$backlinks}
|
||||||
|
reverse
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="error">
|
||||||
|
{JSON.stringify($error)}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.hr {
|
||||||
|
position: relative;
|
||||||
|
margin: 2rem 0 1rem 0;
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 4px double var(--foreground);
|
||||||
|
height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hr-label {
|
||||||
|
position: absolute;
|
||||||
|
top: -1ex;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 18px;
|
||||||
|
|
||||||
|
color: var(--foreground);
|
||||||
|
background: var(--background);
|
||||||
|
padding: 0 4px;
|
||||||
|
|
||||||
|
sl-icon {
|
||||||
|
margin-bottom: -4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,67 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
|
export let speed = 30;
|
||||||
|
|
||||||
|
let root: HTMLDivElement | undefined;
|
||||||
|
let inner: HTMLDivElement | undefined;
|
||||||
|
|
||||||
|
let overflowed = false;
|
||||||
|
let shiftWidth = "unset";
|
||||||
|
let animLength = "unset";
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
|
if (!root) return;
|
||||||
|
overflowed = root.scrollWidth > root.clientWidth;
|
||||||
|
shiftWidth = `-${inner.clientWidth - root.clientWidth}px`;
|
||||||
|
animLength = `${inner.clientWidth / speed}s`;
|
||||||
|
});
|
||||||
|
resizeObserver.observe(inner);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="marquee"
|
||||||
|
class:overflowed
|
||||||
|
style={`--shift-width: ${shiftWidth}; --anim-length: ${animLength}`}
|
||||||
|
bind:this={root}
|
||||||
|
>
|
||||||
|
<div class="inner" bind:this={inner}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.marquee {
|
||||||
|
height: 1.1em;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner {
|
||||||
|
white-space: nowrap;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.overflowed .inner {
|
||||||
|
animation: marquee var(--anim-length) ease-in-out infinite;
|
||||||
|
--padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes marquee {
|
||||||
|
0% {
|
||||||
|
transform: translateX(var(--padding));
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: translateX(calc(var(--shift-width) - var(--padding)));
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: translateX(var(--padding));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,23 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Link, useLocation } from "svelte-navigator";
|
||||||
|
import type { Address, VALUE_TYPE } from "upend/types";
|
||||||
|
export let to: IPointer;
|
||||||
|
|
||||||
|
interface IPointer {
|
||||||
|
entity?: Address;
|
||||||
|
attribute?: string;
|
||||||
|
value?: { t: VALUE_TYPE; c: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
let routerTo = "#";
|
||||||
|
|
||||||
|
if ($location.pathname.startsWith("/browse") && to.entity) {
|
||||||
|
routerTo = `${$location.pathname},${to.entity}`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Link to={routerTo}>
|
||||||
|
<slot />
|
||||||
|
</Link>
|
|
@ -0,0 +1,225 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import filesize from "filesize";
|
||||||
|
import { format, fromUnixTime } from "date-fns";
|
||||||
|
import type { Readable } from "svelte/store";
|
||||||
|
import type { OrderedListing } from "upend/types";
|
||||||
|
import Marquee from "../Marquee.svelte";
|
||||||
|
import Address from "../Address.svelte";
|
||||||
|
|
||||||
|
export let attributes: OrderedListing;
|
||||||
|
export let editable = false;
|
||||||
|
export let reverse = false;
|
||||||
|
|
||||||
|
let newEntryAttribute = "'";
|
||||||
|
let newEntryValue = "";
|
||||||
|
|
||||||
|
let currentDisplay = 999;
|
||||||
|
const MAX_DISPLAY = 50;
|
||||||
|
|
||||||
|
async function addEntry() {
|
||||||
|
// this.$emit("edit", {
|
||||||
|
// type: "create",
|
||||||
|
// attribute: this.newEntryAttribute,
|
||||||
|
// value: this.newEntryValue,
|
||||||
|
// } as AttributeChange);
|
||||||
|
// this.newEntryAttribute = "";
|
||||||
|
// this.newEntryValue = "";
|
||||||
|
}
|
||||||
|
async function removeEntry(addr: string) {
|
||||||
|
// if (confirm("Are you sure you want to remove the attribute?")) {
|
||||||
|
// this.$emit("edit", { type: "delete", addr } as AttributeChange);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
async function updateEntry(addr: string, attribute: string, value: string) {
|
||||||
|
// this.$emit("edit", {
|
||||||
|
// type: "update",
|
||||||
|
// addr,
|
||||||
|
// value
|
||||||
|
// } as AttributeChange);
|
||||||
|
// this.$emit("edit", {
|
||||||
|
// type: "delete",
|
||||||
|
// addr,
|
||||||
|
// } as AttributeChange);
|
||||||
|
// this.$emit("edit", {
|
||||||
|
// type: "create",
|
||||||
|
// attribute,
|
||||||
|
// value,
|
||||||
|
// } as AttributeChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
let resolve = [];
|
||||||
|
|
||||||
|
const ATTRIBUTE_LABELS: { [key: string]: string } = {
|
||||||
|
FILE_MIME: "MIME type",
|
||||||
|
FILE_MTIME: "Last modified",
|
||||||
|
FILE_SIZE: "File size",
|
||||||
|
};
|
||||||
|
|
||||||
|
const VALUE_FORMATTERS: { [key: string]: (val: string) => string } = {
|
||||||
|
FILE_MTIME: (val) => format(fromUnixTime(parseInt(val, 10)), "PPpp"),
|
||||||
|
FILE_SIZE: (val) => filesize(parseInt(val, 10), { base: 2 }),
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatAttribute(attribute: string) {
|
||||||
|
return ATTRIBUTE_LABELS[attribute];
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatValue(value: string, attribute: string): string | undefined {
|
||||||
|
const handler = VALUE_FORMATTERS[attribute];
|
||||||
|
if (handler) {
|
||||||
|
return handler(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="table">
|
||||||
|
<table class:reverse>
|
||||||
|
<colgroup>
|
||||||
|
{#if editable}
|
||||||
|
<col class="attr-action-col" />
|
||||||
|
{/if}
|
||||||
|
<col class="attr-col" />
|
||||||
|
<col />
|
||||||
|
</colgroup>
|
||||||
|
{#if !reverse}
|
||||||
|
<tr>
|
||||||
|
{#if editable}
|
||||||
|
<th />
|
||||||
|
{/if}
|
||||||
|
<th>Attribute</th>
|
||||||
|
<th>Value</th>
|
||||||
|
</tr>
|
||||||
|
{#each attributes as [id, entry] (id)}
|
||||||
|
<tr v-for="[id, entry] in limitedAttributes">
|
||||||
|
{#if editable}
|
||||||
|
<td class="attr-action">
|
||||||
|
<sl-icon-button name="x-circle" on:click={removeEntry(id)} />
|
||||||
|
</td>
|
||||||
|
{/if}
|
||||||
|
<td class:formatted={Boolean(formatAttribute(entry.attribute))}>
|
||||||
|
<Marquee>
|
||||||
|
{formatAttribute(entry.attribute) || entry.attribute}
|
||||||
|
</Marquee>
|
||||||
|
</td>
|
||||||
|
<td class="value">
|
||||||
|
<text-input
|
||||||
|
{editable}
|
||||||
|
value={entry.value.c}
|
||||||
|
on:edit={(val) => updateEntry(id, entry.attribute, val)}
|
||||||
|
>
|
||||||
|
{#if entry.value.t === "Address"}
|
||||||
|
<Address
|
||||||
|
link
|
||||||
|
address={entry.value.c}
|
||||||
|
resolve={Boolean(resolve[id]) || true}
|
||||||
|
data-id={id}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class:formatted={Boolean(
|
||||||
|
formatValue(entry.value.c, entry.attribute)
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Marquee>
|
||||||
|
{formatValue(entry.value.c, entry.attribute) ||
|
||||||
|
entry.value.c}
|
||||||
|
</Marquee>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</text-input>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
{#if attributes.length > currentDisplay}
|
||||||
|
<tr>
|
||||||
|
<td colspan={editable ? 3 : 2}>
|
||||||
|
<sl-button
|
||||||
|
class="more-button"
|
||||||
|
on:click={(currentDisplay += MAX_DISPLAY)}
|
||||||
|
>
|
||||||
|
+ {attributes.length - currentDisplay} more...
|
||||||
|
</sl-button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/if}
|
||||||
|
{#if editable}
|
||||||
|
<tr>
|
||||||
|
<td class="attr-action">
|
||||||
|
<sl-icon-button name="plus-circle" on:click={addEntry()} />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<sl-input v-sl-model:newEntryAttribute size="small" />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<sl-input v-sl-model:newEntryValue size="small" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<tr>
|
||||||
|
<th>Entities</th>
|
||||||
|
<th>Attribute name</th>
|
||||||
|
</tr>
|
||||||
|
{#each attributes as [id, entry] (id)}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<Address link address={entry.entity} />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Marquee>
|
||||||
|
{entry.attribute}
|
||||||
|
</Marquee>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
table-layout: fixed;
|
||||||
|
|
||||||
|
th {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
font-family: var(--monospace-font);
|
||||||
|
padding-right: 1em;
|
||||||
|
line-height: 1em;
|
||||||
|
line-break: anywhere;
|
||||||
|
|
||||||
|
&.attr-action {
|
||||||
|
max-width: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formatted {
|
||||||
|
font-family: var(--default-font);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.attr-action-col {
|
||||||
|
width: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attr-col {
|
||||||
|
width: 33%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.reverse .attr-col {
|
||||||
|
width: 70%;
|
||||||
|
}
|
||||||
|
|
||||||
|
sl-icon-button {
|
||||||
|
&::part(base) {
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,67 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Link } from "svelte-navigator";
|
||||||
|
export let searchQuery = "";
|
||||||
|
$: console.log(searchQuery);
|
||||||
|
// this.$router.replace({ name: "search", query: { q: this.searchQuery } });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="header">
|
||||||
|
<h1>
|
||||||
|
<Link to="/">
|
||||||
|
<img class="logo" src="/assets/upend.svg" alt="UpEnd logo" />
|
||||||
|
UpEnd
|
||||||
|
</Link>
|
||||||
|
</h1>
|
||||||
|
<sl-input
|
||||||
|
placeholder="Search"
|
||||||
|
value={searchQuery}
|
||||||
|
on:sl-input={(ev) => (searchQuery = ev.target.value)}
|
||||||
|
>
|
||||||
|
<sl-icon name="search" slot="prefix" />
|
||||||
|
</sl-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-bottom: 1px solid var(--foreground);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
|
||||||
|
background: var(--background);
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 16pt;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
:global(a) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
color: var(--foreground);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: inline-block;
|
||||||
|
height: 1.5em;
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
filter: invert(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sl-input {
|
||||||
|
margin-left: 1em;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,138 @@
|
||||||
|
// import { useSWR } from "sswr";
|
||||||
|
import { useSWR } from "../util/fetch";
|
||||||
|
import { derived, Readable, readable, writable } from "svelte/store";
|
||||||
|
import type { IEntry, ListingResult, OrderedListing } from "upend/types";
|
||||||
|
import { listingAsOrdered } from "upend";
|
||||||
|
import LRU from "lru-cache";
|
||||||
|
|
||||||
|
export function useEntity(
|
||||||
|
address: string | (() => string),
|
||||||
|
condition?: () => Boolean
|
||||||
|
) {
|
||||||
|
const { data, error, revalidate } = useSWR<ListingResult, unknown>(() =>
|
||||||
|
condition === undefined || condition()
|
||||||
|
? `/api/obj/${typeof address === "string" ? address : address()}`
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
const entries = derived(data, ($values) =>
|
||||||
|
$values ? listingAsOrdered($values) : []
|
||||||
|
);
|
||||||
|
const attributes = derived(entries, ($entries) => {
|
||||||
|
const addr = typeof address === "string" ? address : address();
|
||||||
|
return $entries.filter(([_, e]) => e.entity === addr);
|
||||||
|
});
|
||||||
|
const backlinks = derived(entries, ($entries) => {
|
||||||
|
const addr = typeof address === "string" ? address : address();
|
||||||
|
return $entries.filter(([_, e]) => e.entity !== addr);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
entries,
|
||||||
|
attributes,
|
||||||
|
backlinks,
|
||||||
|
data,
|
||||||
|
error,
|
||||||
|
revalidate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function query(query: () => string) {
|
||||||
|
let queryString = typeof query === "string" ? query : query();
|
||||||
|
console.debug(`Querying: ${queryString}`);
|
||||||
|
const { data, error, revalidate } = useSWR<ListingResult, unknown>(
|
||||||
|
() => `/api/obj?query=${query()}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = derived(data, ($values) => {
|
||||||
|
return $values ? listingAsOrdered($values) : [];
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
result,
|
||||||
|
data,
|
||||||
|
error,
|
||||||
|
revalidate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryOnceLRU = new LRU<string, OrderedListing>(128);
|
||||||
|
|
||||||
|
export async function queryOnce(query: string): Promise<OrderedListing> {
|
||||||
|
const cacheResult = queryOnceLRU.get(query);
|
||||||
|
if (!cacheResult) {
|
||||||
|
console.debug(`Querying: ${query}`);
|
||||||
|
const response = await fetch(`/api/obj?query=${query}`);
|
||||||
|
return listingAsOrdered(await response.json());
|
||||||
|
} else {
|
||||||
|
console.debug(`Returning cached: ${query}`);
|
||||||
|
return cacheResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EntityIdentification {
|
||||||
|
type: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function identify(
|
||||||
|
attributes: OrderedListing,
|
||||||
|
backlinks: OrderedListing
|
||||||
|
): Promise<EntityIdentification[]> {
|
||||||
|
// Get all entries where the object is linked
|
||||||
|
const hasEntries = backlinks
|
||||||
|
.filter(([_, entry]) => entry.attribute === "HAS")
|
||||||
|
.map(([addr, _]) => addr);
|
||||||
|
|
||||||
|
// Out of those relations, retrieve their ALIAS attrs
|
||||||
|
const hasAliases = hasEntries.length
|
||||||
|
? await queryOnce(
|
||||||
|
`(matches (in ${hasEntries.map((e) => `"${e}"`).join(" ")}) "ALIAS" ?)`
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const aliasValues = hasAliases.map(([_, entry]) => {
|
||||||
|
return entry.value.c;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get all identities of the object
|
||||||
|
const isEntries = attributes
|
||||||
|
.filter(([_, entry]) => entry.attribute === "IS")
|
||||||
|
.map(([_, entry]) => entry.value.c);
|
||||||
|
|
||||||
|
// Out of those, retrieve their TYPE_ID entries
|
||||||
|
const typeIdListing = isEntries.length
|
||||||
|
? await queryOnce(
|
||||||
|
`(matches (in ${isEntries.map((e) => `"${e}"`).join(" ")}) "TYPE_ID" ?)`
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const typeIdAttributes = typeIdListing.map(([_, entry]) => {
|
||||||
|
return [entry.entity, entry.value.c];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Finally, filter own object's attributes according to TYPE_IDs
|
||||||
|
// For each identity/TYPE_ID pair
|
||||||
|
return typeIdAttributes
|
||||||
|
.map(([type, attrName]) => {
|
||||||
|
// And each associated TYPE_ID attribute...
|
||||||
|
// return own matchin attributes
|
||||||
|
return attributes
|
||||||
|
.filter(([_, e]) => e.attribute === attrName)
|
||||||
|
.map(([_, attr]) => {
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
value: attr.value.c,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.flat()
|
||||||
|
.concat(
|
||||||
|
aliasValues.map((value) => {
|
||||||
|
return {
|
||||||
|
type: "ALIAS",
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
import type { SvelteComponent, SvelteComponentTyped } from "svelte";
|
||||||
|
|
||||||
|
export class UpType {
|
||||||
|
address: string;
|
||||||
|
name: string | null = null;
|
||||||
|
attributes: string[] = [];
|
||||||
|
|
||||||
|
constructor(address: string) {
|
||||||
|
this.address = address;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get icon(): string | undefined {
|
||||||
|
return this.name ? TYPE_ICONS[this.name] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get widgetInfo(): Widget | undefined {
|
||||||
|
return this.name ? TYPE_WIDGETS[this.name] : undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Component {
|
||||||
|
component: any; // TODO
|
||||||
|
props?: { [key: string]: unknown };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Widget {
|
||||||
|
name: string;
|
||||||
|
icon?: string;
|
||||||
|
components: Array<Component>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TYPE_ICONS: { [key: string]: string } = {
|
||||||
|
BLOB: "box",
|
||||||
|
HIER: "folder",
|
||||||
|
};
|
||||||
|
|
||||||
|
const TYPE_WIDGETS: { [key: string]: Widget } = {
|
||||||
|
KSX_TRACK_MOODS: {
|
||||||
|
name: "ksx-track-compass",
|
||||||
|
icon: "plus-square",
|
||||||
|
components: [
|
||||||
|
// {
|
||||||
|
// name: "Compass",
|
||||||
|
// id: "compass_tint_energy",
|
||||||
|
// props: {
|
||||||
|
// xAttrName: "KSX_TINT",
|
||||||
|
// yAttrName: "KSX_ENERGY",
|
||||||
|
// xLabel: "Lightsoft // Heavydark",
|
||||||
|
// yLabel: "Chill // Extreme",
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: "Compass",
|
||||||
|
// id: "compass_seriousness_materials",
|
||||||
|
// props: {
|
||||||
|
// xAttrName: "KSX_SERIOUSNESS",
|
||||||
|
// yAttrName: "KSX_MATERIALS",
|
||||||
|
// xLabel: "Dionysia // Apollonia",
|
||||||
|
// yLabel: "Natural // Reinforced",
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type AttributeChange =
|
||||||
|
| AttributeCreate
|
||||||
|
| AttributeUpdate
|
||||||
|
| AttributeDelete;
|
||||||
|
|
||||||
|
export interface AttributeCreate {
|
||||||
|
type: "create";
|
||||||
|
attribute: string;
|
||||||
|
value: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AttributeUpdate {
|
||||||
|
type: "update";
|
||||||
|
addr: string;
|
||||||
|
value: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AttributeDelete {
|
||||||
|
type: "delete";
|
||||||
|
addr: string;
|
||||||
|
}
|
|
@ -2,9 +2,6 @@ import App from './App.svelte';
|
||||||
|
|
||||||
const app = new App({
|
const app = new App({
|
||||||
target: document.body,
|
target: document.body,
|
||||||
props: {
|
|
||||||
name: 'world'
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default app;
|
export default app;
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
|
// stale shim until https://github.com/ConsoleTVs/sswr/issues/24 is resolved
|
||||||
|
export type SWRKey = string;
|
||||||
|
export function useSWR<D = unknown, E = Error>(
|
||||||
|
key: SWRKey | undefined | (() => SWRKey | undefined)
|
||||||
|
) {
|
||||||
|
const data = writable<D | undefined>();
|
||||||
|
const error = writable<D | undefined>();
|
||||||
|
|
||||||
|
function doFetch() {
|
||||||
|
let keyString = typeof key === "string" ? key : key();
|
||||||
|
fetch(keyString)
|
||||||
|
.then(async (response) => {
|
||||||
|
data.set(await response.json());
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error.set(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
doFetch();
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
error,
|
||||||
|
revalidate: doFetch,
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { createHashHistory } from "history";
|
||||||
|
import type { HistorySource } from "svelte-navigator";
|
||||||
|
|
||||||
|
export default function(): HistorySource {
|
||||||
|
const history = createHashHistory({window});
|
||||||
|
let listeners = [];
|
||||||
|
|
||||||
|
history.listen(location => {
|
||||||
|
if (history.action === "POP") {
|
||||||
|
listeners.forEach(listener => listener(location));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
get location() {
|
||||||
|
return history.location as any;
|
||||||
|
},
|
||||||
|
addEventListener(name, handler) {
|
||||||
|
if (name !== "popstate") return;
|
||||||
|
listeners.push(handler);
|
||||||
|
},
|
||||||
|
removeEventListener(name, handler) {
|
||||||
|
if (name !== "popstate") return;
|
||||||
|
listeners = listeners.filter(fn => fn !== handler);
|
||||||
|
},
|
||||||
|
history: {
|
||||||
|
get state() {
|
||||||
|
return history.location.state;
|
||||||
|
},
|
||||||
|
pushState(state, title, uri) {
|
||||||
|
history.push(uri, state);
|
||||||
|
},
|
||||||
|
replaceState(state, title, uri) {
|
||||||
|
history.replace(uri, state);
|
||||||
|
},
|
||||||
|
go(to) {
|
||||||
|
history.go(to);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,94 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { useNavigate, useParams } from "svelte-navigator";
|
||||||
|
import Inspect from "../components/Inspect.svelte";
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const params = useParams();
|
||||||
|
|
||||||
|
let root: HTMLDivElement;
|
||||||
|
let addresses: string[] = $params.addresses.split(",");
|
||||||
|
let editable: boolean[] = [];
|
||||||
|
|
||||||
|
function visit(idx: number) {
|
||||||
|
addresses = [addresses[idx]];
|
||||||
|
}
|
||||||
|
|
||||||
|
function close(idx: number) {
|
||||||
|
addresses.splice(idx, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
navigate(`/browse/${addresses.join(",")}`);
|
||||||
|
|
||||||
|
root?.scrollTo({
|
||||||
|
left: root.scrollWidth,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="browser" bind:this={root}>
|
||||||
|
{#each addresses as address, idx (address)}
|
||||||
|
<div class="view" data-address={address}>
|
||||||
|
<header>
|
||||||
|
<sl-icon-button
|
||||||
|
class="edit-button"
|
||||||
|
name="pencil"
|
||||||
|
click={(editable[idx] = !editable[idx])}
|
||||||
|
/>
|
||||||
|
<sl-icon-button
|
||||||
|
class="this-button"
|
||||||
|
name="bookmark"
|
||||||
|
on:click={visit(idx)}
|
||||||
|
:disabled="addresses.length === 1"
|
||||||
|
/>
|
||||||
|
<sl-icon-button
|
||||||
|
class="close-button"
|
||||||
|
name="x-circle"
|
||||||
|
on:click={close(idx)}
|
||||||
|
disabled={addresses.length === 1}
|
||||||
|
/>
|
||||||
|
</header>
|
||||||
|
<Inspect {address} editable={editable[idx] || false} />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.browser {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
margin-left: -2rem;
|
||||||
|
margin-right: -2rem;
|
||||||
|
padding: 0 2rem;
|
||||||
|
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view {
|
||||||
|
min-width: 30em;
|
||||||
|
max-width: 30em;
|
||||||
|
|
||||||
|
border-left: 1px solid var(--foreground);
|
||||||
|
border-right: 1px solid var(--foreground);
|
||||||
|
|
||||||
|
margin: 1rem 0;
|
||||||
|
padding: 0 1rem;
|
||||||
|
|
||||||
|
header {
|
||||||
|
position: relative;
|
||||||
|
margin: 0;
|
||||||
|
min-height: 1em;
|
||||||
|
|
||||||
|
.this-button {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
translate: transformX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,62 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Link } from "svelte-navigator";
|
||||||
|
import type { IFile, VaultInfo } from "upend/types";
|
||||||
|
|
||||||
|
let infoData: VaultInfo | undefined;
|
||||||
|
let latestFiles: IFile[] = [];
|
||||||
|
|
||||||
|
fetch("/api/info").then(async (response) => {
|
||||||
|
infoData = await response.json();
|
||||||
|
});
|
||||||
|
|
||||||
|
fetch("/api/files/latest").then(async (response) => {
|
||||||
|
latestFiles = await response.json();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="home">
|
||||||
|
<h1>
|
||||||
|
Welcome to <em> "{infoData?.name || "UpEnd"}" </em>
|
||||||
|
</h1>
|
||||||
|
{#if latestFiles}
|
||||||
|
<section class="latest">
|
||||||
|
<h2>Most recently added files</h2>
|
||||||
|
<ul>
|
||||||
|
{#each latestFiles as file}
|
||||||
|
<li>
|
||||||
|
<div class="file-added">{file.added}</div>
|
||||||
|
<Link to="/browse/{file.hash}">
|
||||||
|
<div class="file-path">{file.path}</div>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.latest {
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
margin: 0.1em 0.25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-added {
|
||||||
|
opacity: 0.77;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,6 +1,5 @@
|
||||||
{
|
{
|
||||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||||
|
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*", "node_modules/upend/*"],
|
||||||
"exclude": ["node_modules/*", "__sapper__/*", "public/*"]
|
}
|
||||||
}
|
|
4341
ui/yarn.lock
4341
ui/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue