refactor(webui): switch to SvelteKit | great lint fixing

develop
Tomáš Mládek 2024-01-22 20:33:12 +01:00
parent 0353e43dcf
commit e52560ae07
40 changed files with 1531 additions and 1262 deletions

View File

@ -29,13 +29,14 @@ module.exports = {
}
],
rules: {
"svelte/valid-compile": ["error", { "ignoreWarnings": false }],
"svelte/valid-compile": ["error", { "ignoreWarnings": true }],
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": ["warn", {
argsIgnorePattern: "^_",
varsIgnorePattern: "^_"
}],
"no-console": ["error", {
allow: ["debug", "warn", "error"]
}]
}],
},
};

View File

@ -33,6 +33,14 @@
"dependencies": {
"@ibm/plex": "^6.3.0",
"@recogito/annotorious": "^2.7.11",
"@types/d3": "^7.4.3",
"@types/debug": "^4.1.12",
"@types/dompurify": "^3.0.5",
"@types/lodash": "^4.14",
"@types/marked": "^4.3.2",
"@types/node": "^18.19.8",
"@types/three": "^0.160.0",
"@types/wavesurfer.js": "^6.0.12",
"@upnd/upend": "file:../tools/upend_js",
"@upnd/wasm-web": "file:../tools/upend_wasm/pkg-web",
"boxicons": "^2.1.4",
@ -54,9 +62,7 @@
"sswr": "^1.11.0",
"svelte-i18next": "^1.2.2",
"three": "^0.147.0",
"wavesurfer.js": "^6.6.4",
"vite-plugin-static-copy": "^0.13.1",
"@types/node": "^18.19.8",
"@types/lodash": "^4.14"
"wavesurfer.js": "^6.6.4"
}
}

View File

@ -11,12 +11,30 @@ dependencies:
'@recogito/annotorious':
specifier: ^2.7.11
version: 2.7.12(react-dom@16.14.0)(react@16.14.0)
'@types/d3':
specifier: ^7.4.3
version: 7.4.3
'@types/debug':
specifier: ^4.1.12
version: 4.1.12
'@types/dompurify':
specifier: ^3.0.5
version: 3.0.5
'@types/lodash':
specifier: ^4.14
version: 4.14.202
'@types/marked':
specifier: ^4.3.2
version: 4.3.2
'@types/node':
specifier: ^18.19.8
version: 18.19.8
'@types/three':
specifier: ^0.160.0
version: 0.160.0
'@types/wavesurfer.js':
specifier: ^6.0.12
version: 6.0.12
'@upnd/upend':
specifier: file:../tools/upend_js
version: file:../tools/upend_js
@ -783,6 +801,201 @@ packages:
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
dev: true
/@types/d3-array@3.2.1:
resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
dev: false
/@types/d3-axis@3.0.6:
resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==}
dependencies:
'@types/d3-selection': 3.0.10
dev: false
/@types/d3-brush@3.0.6:
resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==}
dependencies:
'@types/d3-selection': 3.0.10
dev: false
/@types/d3-chord@3.0.6:
resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==}
dev: false
/@types/d3-color@3.1.3:
resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
dev: false
/@types/d3-contour@3.0.6:
resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==}
dependencies:
'@types/d3-array': 3.2.1
'@types/geojson': 7946.0.13
dev: false
/@types/d3-delaunay@6.0.4:
resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==}
dev: false
/@types/d3-dispatch@3.0.6:
resolution: {integrity: sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==}
dev: false
/@types/d3-drag@3.0.7:
resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==}
dependencies:
'@types/d3-selection': 3.0.10
dev: false
/@types/d3-dsv@3.0.7:
resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==}
dev: false
/@types/d3-ease@3.0.2:
resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
dev: false
/@types/d3-fetch@3.0.7:
resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==}
dependencies:
'@types/d3-dsv': 3.0.7
dev: false
/@types/d3-force@3.0.9:
resolution: {integrity: sha512-IKtvyFdb4Q0LWna6ymywQsEYjK/94SGhPrMfEr1TIc5OBeziTi+1jcCvttts8e0UWZIxpasjnQk9MNk/3iS+kA==}
dev: false
/@types/d3-format@3.0.4:
resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==}
dev: false
/@types/d3-geo@3.1.0:
resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==}
dependencies:
'@types/geojson': 7946.0.13
dev: false
/@types/d3-hierarchy@3.1.6:
resolution: {integrity: sha512-qlmD/8aMk5xGorUvTUWHCiumvgaUXYldYjNVOWtYoTYY/L+WwIEAmJxUmTgr9LoGNG0PPAOmqMDJVDPc7DOpPw==}
dev: false
/@types/d3-interpolate@3.0.4:
resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
dependencies:
'@types/d3-color': 3.1.3
dev: false
/@types/d3-path@3.0.2:
resolution: {integrity: sha512-WAIEVlOCdd/NKRYTsqCpOMHQHemKBEINf8YXMYOtXH0GA7SY0dqMB78P3Uhgfy+4X+/Mlw2wDtlETkN6kQUCMA==}
dev: false
/@types/d3-polygon@3.0.2:
resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==}
dev: false
/@types/d3-quadtree@3.0.6:
resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==}
dev: false
/@types/d3-random@3.0.3:
resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==}
dev: false
/@types/d3-scale-chromatic@3.0.3:
resolution: {integrity: sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==}
dev: false
/@types/d3-scale@4.0.8:
resolution: {integrity: sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==}
dependencies:
'@types/d3-time': 3.0.3
dev: false
/@types/d3-selection@3.0.10:
resolution: {integrity: sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg==}
dev: false
/@types/d3-shape@3.1.6:
resolution: {integrity: sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==}
dependencies:
'@types/d3-path': 3.0.2
dev: false
/@types/d3-time-format@4.0.3:
resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==}
dev: false
/@types/d3-time@3.0.3:
resolution: {integrity: sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==}
dev: false
/@types/d3-timer@3.0.2:
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
dev: false
/@types/d3-transition@3.0.8:
resolution: {integrity: sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ==}
dependencies:
'@types/d3-selection': 3.0.10
dev: false
/@types/d3-zoom@3.0.8:
resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==}
dependencies:
'@types/d3-interpolate': 3.0.4
'@types/d3-selection': 3.0.10
dev: false
/@types/d3@7.4.3:
resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==}
dependencies:
'@types/d3-array': 3.2.1
'@types/d3-axis': 3.0.6
'@types/d3-brush': 3.0.6
'@types/d3-chord': 3.0.6
'@types/d3-color': 3.1.3
'@types/d3-contour': 3.0.6
'@types/d3-delaunay': 6.0.4
'@types/d3-dispatch': 3.0.6
'@types/d3-drag': 3.0.7
'@types/d3-dsv': 3.0.7
'@types/d3-ease': 3.0.2
'@types/d3-fetch': 3.0.7
'@types/d3-force': 3.0.9
'@types/d3-format': 3.0.4
'@types/d3-geo': 3.1.0
'@types/d3-hierarchy': 3.1.6
'@types/d3-interpolate': 3.0.4
'@types/d3-path': 3.0.2
'@types/d3-polygon': 3.0.2
'@types/d3-quadtree': 3.0.6
'@types/d3-random': 3.0.3
'@types/d3-scale': 4.0.8
'@types/d3-scale-chromatic': 3.0.3
'@types/d3-selection': 3.0.10
'@types/d3-shape': 3.1.6
'@types/d3-time': 3.0.3
'@types/d3-time-format': 4.0.3
'@types/d3-timer': 3.0.2
'@types/d3-transition': 3.0.8
'@types/d3-zoom': 3.0.8
dev: false
/@types/debounce@1.2.4:
resolution: {integrity: sha512-jBqiORIzKDOToaF63Fm//haOCHuwQuLa2202RK4MozpA6lh93eCBc+/8+wZn5OzjJt3ySdc+74SXWXB55Ewtyw==}
dev: false
/@types/debug@4.1.12:
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
dependencies:
'@types/ms': 0.7.34
dev: false
/@types/dompurify@3.0.5:
resolution: {integrity: sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==}
dependencies:
'@types/trusted-types': 2.0.7
dev: false
/@types/eslint@8.56.0:
resolution: {integrity: sha512-FlsN0p4FhuYRjIxpbdXovvHQhtlG05O1GG/RNWvdAxTboR438IOTwmrY/vLA+Xfgg06BTkP045M3vpFwTMv1dg==}
dependencies:
@ -793,6 +1006,10 @@ packages:
/@types/estree@1.0.5:
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
/@types/geojson@7946.0.13:
resolution: {integrity: sha512-bmrNrgKMOhM3WsafmbGmC+6dsF2Z308vLFsQ3a/bT8X8Sv5clVYpPars/UPq+sAaJP+5OoLAYgwbkS5QEJdLUQ==}
dev: false
/@types/json-schema@7.0.13:
resolution: {integrity: sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==}
dev: true
@ -801,6 +1018,14 @@ packages:
resolution: {integrity: sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==}
dev: false
/@types/marked@4.3.2:
resolution: {integrity: sha512-a79Yc3TOk6dGdituy8hmTTJXjOkZ7zsFYV10L337ttq/rec8lRMDBpV7fL3uLx6TgbFCa5DU/h8FmIBQPSbU0w==}
dev: false
/@types/ms@0.7.34:
resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==}
dev: false
/@types/node@18.19.8:
resolution: {integrity: sha512-g1pZtPhsvGVTwmeVoexWZLTQaOvXwoSq//pTL0DHeNzUDrFnir4fgETdhjhIxjVnN+hKOuh98+E1eMLnUXstFg==}
dependencies:
@ -818,6 +1043,33 @@ packages:
resolution: {integrity: sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg==}
dev: true
/@types/stats.js@0.17.3:
resolution: {integrity: sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ==}
dev: false
/@types/three@0.160.0:
resolution: {integrity: sha512-jWlbUBovicUKaOYxzgkLlhkiEQJkhCVvg4W2IYD2trqD2om3VK4DGLpHH5zQHNr7RweZK/5re/4IVhbhvxbV9w==}
dependencies:
'@types/stats.js': 0.17.3
'@types/webxr': 0.5.10
fflate: 0.6.10
meshoptimizer: 0.18.1
dev: false
/@types/trusted-types@2.0.7:
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
dev: false
/@types/wavesurfer.js@6.0.12:
resolution: {integrity: sha512-oM9hYlPIVms4uwwoaGs9d0qp7Xk7IjSGkdwgmhUymVUIIilRfjtSQvoOgv4dpKiW0UozWRSyXfQqTobi0qWyCw==}
dependencies:
'@types/debounce': 1.2.4
dev: false
/@types/webxr@0.5.10:
resolution: {integrity: sha512-n3u5sqXQJhf1CS68mw3Wf16FQ4cRPNBBwdYLFzq3UddiADOim1Pn3Y6PBdDilz1vOJF3ybLxJ8ZEDlLIzrOQZg==}
dev: false
/@typescript-eslint/eslint-plugin@6.19.0(@typescript-eslint/parser@6.19.0)(eslint@8.56.0)(typescript@5.3.3):
resolution: {integrity: sha512-DUCUkQNklCQYnrBSSikjVChdc84/vMPDQSgJTHBZ64G9bA9w0Crc0rd2diujKbTdp6w2J47qkeHQLoi0rpLCdg==}
engines: {node: ^16.0.0 || >=18.0.0}
@ -1925,6 +2177,10 @@ packages:
dependencies:
reusify: 1.0.4
/fflate@0.6.10:
resolution: {integrity: sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==}
dev: false
/file-entry-cache@6.0.1:
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
engines: {node: ^10.12.0 || >=12.0.0}
@ -2532,6 +2788,10 @@ packages:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
/meshoptimizer@0.18.1:
resolution: {integrity: sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==}
dev: false
/micromatch@4.0.5:
resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==}
engines: {node: '>=8.6'}

View File

@ -9,6 +9,7 @@
import { ATTR_IN } from '@upnd/upend/constants';
import { createEventDispatcher } from 'svelte';
import { Any } from '@upnd/upend/query';
import type { Widget } from '$lib/components/EntryView.svelte';
const dispatch = createEventDispatcher();
export let spec: string;
@ -26,7 +27,7 @@
dispatch('close');
}
const combinedWidgets = [
const combinedWidgets: Widget[] = [
{
name: 'List',
icon: 'list-check',
@ -55,7 +56,7 @@
}
];
let resultEntities = [];
let resultEntities: string[] = [];
async function updateResultEntities(
includedGroups: string[],
requiredGroups: string[],
@ -109,21 +110,21 @@
<div class="controls">
<EntitySetEditor
entities={includedGroups}
header={$i18n.t('Include')}
header={$i18n.t('Include') || ''}
confirmRemoveMessage={null}
on:add={(ev) => (includedGroups = [...includedGroups, ev.detail])}
on:remove={(ev) => (includedGroups = includedGroups.filter((e) => e !== ev.detail))}
/>
<EntitySetEditor
entities={requiredGroups}
header={$i18n.t('Require')}
header={$i18n.t('Require') || ''}
confirmRemoveMessage={null}
on:add={(ev) => (requiredGroups = [...requiredGroups, ev.detail])}
on:remove={(ev) => (requiredGroups = requiredGroups.filter((e) => e !== ev.detail))}
/>
<EntitySetEditor
entities={excludedGroups}
header={$i18n.t('Exclude')}
header={$i18n.t('Exclude') || ''}
confirmRemoveMessage={null}
on:add={(ev) => (excludedGroups = [...excludedGroups, ev.detail])}
on:remove={(ev) => (excludedGroups = excludedGroups.filter((e) => e !== ev.detail))}
@ -131,7 +132,7 @@
</div>
<div class="entities">
<EntryView
title={$i18n.t('Matching entities')}
title={$i18n.t('Matching entities') || ''}
entities={resultEntities}
widgets={combinedWidgets}
/>

View File

@ -1,87 +1,88 @@
<script lang="ts">
import { addEmitter } from "./AddModal.svelte";
import Icon from "./utils/Icon.svelte";
import { addEmitter } from './AddModal.svelte';
import Icon from './utils/Icon.svelte';
let dragging = false;
let dragging = false;
function onDrop(ev: DragEvent) {
if (ev.dataTransfer.files.length > 0) {
addEmitter.emit("files", Array.from(ev.dataTransfer.files));
} // TODO: else check for URLs
dragging = false;
}
function onDrop(ev: DragEvent) {
if (ev.dataTransfer?.files.length) {
addEmitter.emit('files', Array.from(ev.dataTransfer?.files || []));
} // TODO: else check for URLs
dragging = false;
}
function onDragEnter() {
// noop
}
function onDragEnter() {
// noop
}
function onDragOver(ev: DragEvent) {
if (Array.from(ev.dataTransfer.items).some((it) => it.kind === "file")) {
dragging = true;
}
}
function onDragOver(ev: DragEvent) {
if (Array.from(ev.dataTransfer?.items || []).some((it) => it.kind === 'file')) {
dragging = true;
}
}
function onDragLeave() {
dragging = false;
}
function onDragLeave() {
dragging = false;
}
function onPaste(ev: ClipboardEvent) {
if (ev.clipboardData.files.length > 0) {
addEmitter.emit("files", Array.from(ev.clipboardData.files));
} // TODO: else check for URLs
}
function onPaste(ev: ClipboardEvent) {
if (ev.clipboardData?.files.length) {
addEmitter.emit('files', Array.from(ev.clipboardData?.files || []));
} // TODO: else check for URLs
}
</script>
<svelte:body
on:dragenter|preventDefault={onDragEnter}
on:dragover|preventDefault={onDragOver}
on:dragleave|preventDefault={onDragLeave}
on:drop|preventDefault={onDrop}
on:paste={onPaste} />
on:dragenter|preventDefault={onDragEnter}
on:dragover|preventDefault={onDragOver}
on:dragleave|preventDefault={onDragLeave}
on:drop|preventDefault={onDrop}
on:paste={onPaste}
/>
<div class="dropindicator" class:dragging>
<div class="content">
<div class="icon">
<Icon name="current-location" />
</div>
<p>Drop an URL, an image or a file here!</p>
</div>
<div class="content">
<div class="icon">
<Icon name="current-location" />
</div>
<p>Drop an URL, an image or a file here!</p>
</div>
</div>
<style>
.dropindicator {
position: absolute;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.75);
.dropindicator {
position: absolute;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.75);
display: none;
}
display: none;
}
.dragging {
display: unset;
}
.dragging {
display: unset;
}
.content {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
.content {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.9);
background: rgba(0, 0, 0, 0.9);
color: var(--foreground);
border: solid 0.25em var(--foreground);
border-radius: 0.5em;
padding: 1.5em;
color: var(--foreground);
border: solid 0.25em var(--foreground);
border-radius: 0.5em;
padding: 1.5em;
text-align: center;
font-size: 32px;
}
text-align: center;
font-size: 32px;
}
.icon {
font-size: 128px;
}
.icon {
font-size: 128px;
}
</style>

View File

@ -1,167 +1,161 @@
<script lang="ts" context="module">
import { writable } from "svelte/store";
import { writable } from 'svelte/store';
export const selected = writable<string[]>([]);
export const selected = writable<string[]>([]);
</script>
<script lang="ts">
import { onMount } from "svelte";
import { i18n } from "../i18n";
import { onMount } from 'svelte';
import { i18n } from '../i18n';
let canvas: HTMLCanvasElement;
let canvas: HTMLCanvasElement;
onMount(() => {
const ctx = canvas.getContext("2d");
onMount(() => {
const ctx = canvas.getContext('2d');
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
window.addEventListener("resize", resizeCanvas);
resizeCanvas();
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
let selecting = false;
let selectAllArea: DOMRect | undefined = undefined;
let selectAllAddresses: string[] | undefined = undefined;
let addressesToRemove = new Set();
document.addEventListener("mousedown", (ev) => {
if (ev.ctrlKey || ev.metaKey) {
ev.preventDefault();
let selecting = false;
let selectAllArea: DOMRect | undefined = undefined;
let selectAllAddresses: string[] = [];
let addressesToRemove = new Set();
document.addEventListener('mousedown', (ev) => {
if (!ctx) return;
selecting = true;
addressesToRemove = new Set();
if (ev.ctrlKey || ev.metaKey) {
ev.preventDefault();
const el = document.elementFromPoint(
ev.clientX,
ev.clientY,
) as HTMLElement;
selecting = true;
addressesToRemove = new Set();
const multiElement = el.closest("[data-address-multi]") as
| HTMLElement
| undefined;
const el = document.elementFromPoint(ev.clientX, ev.clientY) as HTMLElement;
if (multiElement) {
const banner = multiElement.querySelector("h2");
const multiElement = el.closest('[data-address-multi]') as HTMLElement | undefined;
if (banner) {
const rect = banner.getBoundingClientRect();
selectAllArea = rect;
selectAllAddresses = multiElement.dataset.addressMulti.split(",");
if (multiElement) {
const banner = multiElement.querySelector('h2');
ctx.rect(rect.left, rect.top, rect.width, rect.height);
ctx.fillStyle = "#dc322f33";
ctx.fill();
if (banner) {
const rect = banner.getBoundingClientRect();
selectAllArea = rect;
selectAllAddresses = multiElement.dataset.addressMulti?.split(',') || [];
ctx.fillStyle = "#dc322f77";
ctx.font = `bold ${rect.height / 2}px Inter`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
const fix = ctx.measureText("M").actualBoundingBoxDescent / 2;
ctx.fillText(
$i18n.t("Select All"),
rect.left + rect.width / 2,
rect.top + rect.height / 2 + fix,
);
}
}
ctx.rect(rect.left, rect.top, rect.width, rect.height);
ctx.fillStyle = '#dc322f33';
ctx.fill();
ctx.strokeStyle = "#dc322f77";
ctx.lineWidth = 7;
ctx.beginPath();
ctx.moveTo(ev.clientX, ev.clientY);
}
});
ctx.fillStyle = '#dc322f77';
ctx.font = `bold ${rect.height / 2}px Inter`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const fix = ctx.measureText('M').actualBoundingBoxDescent / 2;
ctx.fillText(
$i18n.t('Select All'),
rect.left + rect.width / 2,
rect.top + rect.height / 2 + fix
);
}
}
document.addEventListener("mousemove", (ev) => {
if (selecting) {
ev.preventDefault();
ctx.strokeStyle = '#dc322f77';
ctx.lineWidth = 7;
ctx.beginPath();
ctx.moveTo(ev.clientX, ev.clientY);
}
});
if (selectAllArea) {
if (
ev.clientX > selectAllArea.left &&
ev.clientX < selectAllArea.right &&
ev.clientY > selectAllArea.top &&
ev.clientY < selectAllArea.bottom
) {
selected.update((selected) => {
return [
...selected,
...selectAllAddresses.filter((a) => {
return !selected.includes(a);
}),
];
});
stop();
}
}
document.addEventListener('mousemove', (ev) => {
if (!ctx) return;
const el = document.elementFromPoint(
ev.clientX,
ev.clientY,
) as HTMLElement;
if (selecting) {
ev.preventDefault();
const addressElement = el.closest("[data-address]") as
| HTMLElement
| undefined;
if (addressElement) {
const address = addressElement.dataset.address;
const selectMode = addressElement.dataset.selectMode;
if (selectMode === "add" || selectMode === undefined) {
selected.update((selected) => {
if (!selected.includes(address)) {
return [...selected, address];
} else {
return selected;
}
});
} else if (selectMode === "remove") {
addressesToRemove.add(address);
}
}
if (selectAllArea) {
if (
ev.clientX > selectAllArea.left &&
ev.clientX < selectAllArea.right &&
ev.clientY > selectAllArea.top &&
ev.clientY < selectAllArea.bottom
) {
selected.update((selected) => {
return [
...selected,
...selectAllAddresses.filter((a) => {
return !selected.includes(a);
})
];
});
stop();
}
}
ctx.lineTo(ev.clientX, ev.clientY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(ev.clientX, ev.clientY);
}
});
const el = document.elementFromPoint(ev.clientX, ev.clientY) as HTMLElement;
document.addEventListener("mouseup", () => {
stop();
});
const addressElement = el.closest('[data-address]') as HTMLElement | undefined;
if (addressElement) {
const address = addressElement.dataset.address;
const selectMode = addressElement.dataset.selectMode;
if (selectMode === 'add' || selectMode === undefined) {
selected.update((selected) => {
if (address && !selected.includes(address)) {
return [...selected, address];
} else {
return selected;
}
});
} else if (selectMode === 'remove') {
addressesToRemove.add(address);
}
}
function stop() {
selectAllArea = undefined;
selectAllAddresses = undefined;
selecting = false;
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (const address of addressesToRemove) {
selected.update((selected) => {
return selected.filter((a) => a !== address);
});
}
}
});
ctx.lineTo(ev.clientX, ev.clientY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(ev.clientX, ev.clientY);
}
});
document.addEventListener('mouseup', () => {
stop();
});
function stop() {
selectAllArea = undefined;
selectAllAddresses = [];
selecting = false;
ctx?.clearRect(0, 0, canvas.width, canvas.height);
for (const address of addressesToRemove) {
selected.update((selected) => {
return selected.filter((a) => a !== address);
});
}
}
});
</script>
<div class="selectIndicator">
<canvas bind:this={canvas}></canvas>
<canvas bind:this={canvas}></canvas>
</div>
<style lang="scss">
.selectIndicator {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
overflow: hidden;
.selectIndicator {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
overflow: hidden;
pointer-events: none;
pointer-events: none;
canvas {
width: 100%;
height: 100%;
}
}
canvas {
width: 100%;
height: 100%;
}
}
</style>

View File

@ -1,117 +1,111 @@
<script lang="ts">
import UpObjectDisplay from "./display/UpObject.svelte";
import Selector, { type SelectorValue } from "./utils/Selector.svelte";
import IconButton from "./utils/IconButton.svelte";
import { i18n } from "../i18n";
import LabelBorder from "./utils/LabelBorder.svelte";
import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher();
import UpObjectDisplay from './display/UpObject.svelte';
import Selector, { type SelectorValue } from './utils/Selector.svelte';
import IconButton from './utils/IconButton.svelte';
import { i18n } from '../i18n';
import LabelBorder from './utils/LabelBorder.svelte';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
export let entities: string[];
export let hide = false;
export let entities: string[];
export let hide = false;
export let header = "";
export let confirmRemoveMessage: string | null = $i18n.t(
"Are you sure you want to remove this?",
);
export let emptyMessage = $i18n.t("Nothing to show.");
export let header = '';
export let confirmRemoveMessage: string | null = $i18n.t('Are you sure you want to remove this?');
export let emptyMessage = $i18n.t('Nothing to show.');
let adding = false;
let selector: Selector;
let adding = false;
let selector: Selector;
$: if (adding && selector) selector.focus();
$: if (adding && selector) selector.focus();
async function add(ev: CustomEvent<SelectorValue>) {
if (ev.detail.t !== "Address") {
return;
}
dispatch("add", ev.detail.c);
}
async function add(ev: CustomEvent<SelectorValue>) {
if (ev.detail.t !== 'Address') {
return;
}
dispatch('add', ev.detail.c);
}
async function remove(address: string) {
if (!confirmRemoveMessage || confirm(confirmRemoveMessage)) {
dispatch("remove", address);
}
}
async function remove(address: string) {
if (!confirmRemoveMessage || confirm(confirmRemoveMessage)) {
dispatch('remove', address);
}
}
</script>
<LabelBorder {hide}>
<span slot="header">{header}</span>
<span slot="header">{header}</span>
{#if adding}
<div class="selector">
<Selector
bind:this={selector}
types={["Address", "NewAddress"]}
on:input={add}
on:focus={(ev) => {
if (!ev.detail) adding = false;
}}
placeholder={$i18n.t("Choose an entity...")}
/>
</div>
{/if}
{#if adding}
<div class="selector">
<Selector
bind:this={selector}
types={['Address', 'NewAddress']}
on:input={add}
on:focus={(ev) => {
if (!ev.detail) adding = false;
}}
placeholder={$i18n.t('Choose an entity...') || ''}
/>
</div>
{/if}
<div class="body">
<div class="group-list">
{#each entities as entity}
<div
class="group"
on:mouseenter={() => dispatch("highlighted", entity)}
on:mouseleave={() => dispatch("highlighted", undefined)}
>
<UpObjectDisplay address={entity} link />
<IconButton subdued name="x-circle" on:click={() => remove(entity)} />
</div>
{:else}
<div class="no-groups">
{emptyMessage}
</div>
{/each}
</div>
{#if !adding}
<div class="add-button">
<IconButton
outline
small
name="folder-plus"
on:click={() => (adding = true)}
/>
</div>
{/if}
</div>
<div class="body">
<div class="group-list">
{#each entities as entity}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="group"
on:mouseenter={() => dispatch('highlighted', entity)}
on:mouseleave={() => dispatch('highlighted', undefined)}
>
<UpObjectDisplay address={entity} link />
<IconButton subdued name="x-circle" on:click={() => remove(entity)} />
</div>
{:else}
<div class="no-groups">
{emptyMessage}
</div>
{/each}
</div>
{#if !adding}
<div class="add-button">
<IconButton outline small name="folder-plus" on:click={() => (adding = true)} />
</div>
{/if}
</div>
</LabelBorder>
<style lang="scss">
.group-list {
display: flex;
flex-wrap: wrap;
gap: 0.25rem 0.2rem;
align-items: center;
}
.group-list {
display: flex;
flex-wrap: wrap;
gap: 0.25rem 0.2rem;
align-items: center;
}
.group {
display: inline-flex;
align-items: center;
}
.group {
display: inline-flex;
align-items: center;
}
.body {
display: flex;
align-items: start;
.body {
display: flex;
align-items: start;
.group-list {
flex-grow: 1;
}
.group-list {
flex-grow: 1;
}
padding-bottom: 0.2rem;
}
padding-bottom: 0.2rem;
}
.selector {
width: 100%;
margin-bottom: 0.5rem;
}
.selector {
width: 100%;
margin-bottom: 0.5rem;
}
.no-groups {
opacity: 0.66;
}
.no-groups {
opacity: 0.66;
}
</style>

View File

@ -1,152 +1,150 @@
<script lang="ts" context="module">
export interface WidgetComponent {
component: ComponentType;
props: { [key: string]: unknown };
}
import type { ComponentType } from 'svelte';
export interface WidgetComponent {
component: ComponentType;
props: { [key: string]: unknown };
}
export interface Widget {
name: string;
icon?: string;
components: (input: {
entries: UpEntry[];
entities: string[];
group?: string;
address?: string;
}) => Array<WidgetComponent>;
}
export interface Widget {
name: string;
icon?: string;
components: (input: {
entries: UpEntry[];
entities: string[];
group?: string;
address?: string;
}) => Array<WidgetComponent>;
}
</script>
<script lang="ts">
import EntryList from "./widgets/EntryList.svelte";
import type { UpEntry } from "@upnd/upend";
import Icon from "./utils/Icon.svelte";
import IconButton from "./utils/IconButton.svelte";
import { createEventDispatcher, type ComponentType } from "svelte";
import UpObject from "./display/UpObject.svelte";
import LabelBorder from "./utils/LabelBorder.svelte";
const dispatch = createEventDispatcher();
import EntryList from './widgets/EntryList.svelte';
import type { UpEntry } from '@upnd/upend';
import Icon from './utils/Icon.svelte';
import IconButton from './utils/IconButton.svelte';
import { createEventDispatcher } from 'svelte';
import UpObject from './display/UpObject.svelte';
import LabelBorder from './utils/LabelBorder.svelte';
const dispatch = createEventDispatcher();
export let entries: UpEntry[] = [];
export let entities: string[] = [];
export let widgets: Widget[] | undefined = undefined;
export let initialWidget: string | undefined = undefined;
export let title: string | undefined = undefined;
export let group: string | undefined = undefined;
export let address: string | undefined = undefined;
export let icon: string | undefined = undefined;
export let highlighted = false;
export let entries: UpEntry[] = [];
export let entities: string[] = [];
export let widgets: Widget[] | undefined = undefined;
export let initialWidget: string | undefined = undefined;
export let title: string | undefined = undefined;
export let group: string | undefined = undefined;
export let address: string | undefined = undefined;
export let icon: string | undefined = undefined;
export let highlighted = false;
let currentWidget: string | undefined;
let currentWidget: string | undefined;
function switchWidget(widget: string) {
currentWidget = widget;
dispatch("widgetSwitched", currentWidget);
}
function switchWidget(widget: string) {
currentWidget = widget;
dispatch('widgetSwitched', currentWidget);
}
let availableWidgets: Widget[] = [];
$: {
availableWidgets = [];
let availableWidgets: Widget[] = [];
$: {
availableWidgets = [];
if (entries.length) {
availableWidgets = [
...availableWidgets,
{
name: "Entry List",
icon: "table",
components: ({ entries }) => [
{
component: EntryList,
props: { entries, columns: "entity, attribute, value" },
},
],
},
];
}
if (entries.length) {
availableWidgets = [
...availableWidgets,
{
name: 'Entry List',
icon: 'table',
components: ({ entries }) => [
{
component: EntryList,
props: { entries, columns: 'entity, attribute, value' }
}
]
}
];
}
if (widgets?.length) {
availableWidgets = [...widgets, ...availableWidgets];
}
if (widgets?.length) {
availableWidgets = [...widgets, ...availableWidgets];
}
if (availableWidgets.map((w) => w.name).includes(initialWidget)) {
currentWidget = initialWidget;
} else {
currentWidget = availableWidgets[0].name;
}
}
if (initialWidget && availableWidgets.map((w) => w.name).includes(initialWidget)) {
currentWidget = initialWidget;
} else {
currentWidget = availableWidgets[0].name;
}
}
let components: WidgetComponent[] = [];
$: {
components = availableWidgets
.find((w) => w.name === currentWidget)
.components({ entries, entities, group, address });
}
let components: WidgetComponent[] = [];
$: {
components =
availableWidgets
.find((w) => w.name === currentWidget)
?.components({ entries, entities, group, address }) || [];
}
</script>
<LabelBorder hide={entries.length === 0 && entities.length === 0}>
<svelte:fragment slot="header-full">
<h3 class:highlighted>
{#if group}
{#if icon}
<div class="icon">
<Icon name={icon} />
</div>
{/if}
<UpObject link address={group} labels={title ? [title] : undefined} />
{:else}
{#if icon}
<div class="icon">
<Icon name={icon} />
</div>
{/if}
{title || ""}
{/if}
</h3>
<svelte:fragment slot="header-full">
<h3 class:highlighted>
{#if group}
{#if icon}
<div class="icon">
<Icon name={icon} />
</div>
{/if}
<UpObject link address={group} labels={title ? [title] : undefined} />
{:else}
{#if icon}
<div class="icon">
<Icon name={icon} />
</div>
{/if}
{title || ''}
{/if}
</h3>
{#if currentWidget && availableWidgets.length > 1}
<div class="views">
{#each availableWidgets as widget (widget.name)}
<IconButton
name={widget.icon || "cube"}
title={widget.name}
active={widget.name === currentWidget}
--active-color="var(--foreground)"
on:click={() => switchWidget(widget.name)}
>
{widget.name}
</IconButton>
{/each}
</div>
{/if}
</svelte:fragment>
{#each components as component}
<svelte:component
this={component.component}
{...component.props || {}}
on:change
/>
{/each}
{#if currentWidget && availableWidgets.length > 1}
<div class="views">
{#each availableWidgets as widget (widget.name)}
<IconButton
name={widget.icon || 'cube'}
title={widget.name}
active={widget.name === currentWidget}
--active-color="var(--foreground)"
on:click={() => switchWidget(widget.name)}
>
{widget.name}
</IconButton>
{/each}
</div>
{/if}
</svelte:fragment>
{#each components as component}
<svelte:component this={component.component} {...component.props || {}} on:change />
{/each}
</LabelBorder>
<style lang="scss">
.icon {
display: inline-block;
font-size: 1.25em;
margin-top: -0.3em;
position: relative;
bottom: -2px;
}
.icon {
display: inline-block;
font-size: 1.25em;
margin-top: -0.3em;
position: relative;
bottom: -2px;
}
h3 {
margin: 0;
transition: text-shadow 0.2s;
h3 {
margin: 0;
transition: text-shadow 0.2s;
&.highlighted {
text-shadow: #cb4b16 0 0 0.5em;
}
}
&.highlighted {
text-shadow: #cb4b16 0 0 0.5em;
}
}
.views {
display: flex;
font-size: 16px;
}
.views {
display: flex;
font-size: 16px;
}
</style>

View File

@ -21,6 +21,7 @@
import LabelBorder from './utils/LabelBorder.svelte';
import { debug } from 'debug';
import { Any } from '@upnd/upend/query';
import { isDefined } from '$lib/util/werk';
const dbg = debug('kestrel:Inspect');
@ -38,7 +39,7 @@
$: allTypes = derived(
entityInfo,
($entityInfo, set) => {
getAllTypes($entityInfo).then((allTypes) => {
getAllTypes($entityInfo!).then((allTypes) => {
set(allTypes);
});
},
@ -54,7 +55,7 @@
.sort(([_, a], [__, b]) => a.attributes.length - b.attributes.length);
async function getAllTypes(entityInfo: EntityInfo) {
const allTypes = {};
const allTypes: Record<Address, { labels: string[]; attributes: string[] }> = {};
if (!entityInfo) {
return {};
@ -95,15 +96,15 @@
}
} catch (err) {
console.error(err);
return false;
return undefined;
}
})
)
).filter(Boolean);
).filter(isDefined);
})
);
const result = {};
const result: Record<Address, { labels: string[]; attributes: string[] }> = {};
Object.keys(allTypes).forEach((addr) => {
if (allTypes[addr].attributes.length > 0) {
result[addr] = allTypes[addr];
@ -182,10 +183,13 @@
async function fetchCorrectlyTagged() {
const attributes = (
await Promise.all($entity?.attr[`~${ATTR_OF}`].map((e) => api.addressToComponents(e.entity)))
await Promise.all(
($entity?.attr[`~${ATTR_OF}`] ?? []).map((e) => api.addressToComponents(e.entity))
)
)
.filter((ac) => ac.t == 'Attribute')
.map((ac) => ac.c);
.map((ac) => ac.c)
.filter(isDefined);
const attributeQuery = await api.query(
Query.matches(
@ -288,7 +292,7 @@
props: {
entries,
columns: 'attribute, value',
attributes: $allTypes[group]?.attributes || []
attributes: group ? $allTypes[group]?.attributes : [] || []
}
}
]
@ -341,7 +345,7 @@
];
$: entity.subscribe(async (object) => {
if (object && object.listing.entries.length) {
if (object && object.listing?.entries.length) {
dbg('Updating visit stats for %o', object);
await api.putEntityAttribute(
object.address,
@ -387,7 +391,7 @@
<div class="blob-viewer">
<BlobViewer {address} {detail} on:handled={(ev) => (blobHandled = ev.detail)} />
</div>
{#if !$error}
{#if !$error && $entity}
<InspectGroups
{entity}
on:highlighted={(ev) => (highlightedType = ev.detail)}
@ -412,7 +416,7 @@
{#if currentUntypedProperties.length > 0}
<EntryView
title={$i18n.t('Other Properties')}
title={$i18n.t('Other Properties') || ''}
widgets={attributeWidgets}
entries={currentUntypedProperties}
on:change={onChange}
@ -422,7 +426,7 @@
{#if currentUntypedLinks.length > 0}
<EntryView
title={$i18n.t('Links')}
title={$i18n.t('Links') || ''}
widgets={linkWidgets}
entries={currentUntypedLinks}
on:change={onChange}
@ -442,14 +446,14 @@
<EntryView
title={`${$i18n.t('Typed Members')} (${correctlyTagged.length})`}
widgets={taggedWidgets}
entries={tagged.filter((e) => correctlyTagged.includes(e.entity))}
entries={tagged.filter((e) => correctlyTagged?.includes(e.entity))}
on:change={onChange}
{address}
/>
<EntryView
title={`${$i18n.t('Untyped members')} (${incorrectlyTagged.length})`}
widgets={taggedWidgets}
entries={tagged.filter((e) => incorrectlyTagged.includes(e.entity))}
entries={tagged.filter((e) => incorrectlyTagged?.includes(e.entity))}
on:change={onChange}
{address}
/>
@ -482,13 +486,13 @@
<div class="entries">
<h2>{$i18n.t('Attributes')}</h2>
<EntryList
entries={$entity.attributes}
entries={$entity?.attributes || []}
columns={detail ? 'timestamp, provenance, attribute, value' : 'attribute, value'}
on:change={onChange}
/>
<h2>{$i18n.t('Backlinks')}</h2>
<EntryList
entries={$entity.backlinks}
entries={$entity?.backlinks || []}
columns={detail ? 'timestamp, provenance, entity, attribute' : 'entity, attribute'}
on:change={onChange}
/>
@ -497,7 +501,7 @@
<div class="footer">
<IconButton
name="detail"
title={$i18n.t('Show as entries')}
title={$i18n.t('Show as entries') || ''}
active={showAsEntries}
on:click={() => (showAsEntries = !showAsEntries)}
/>
@ -508,7 +512,7 @@
subdued
color="#dc322f"
on:click={deleteObject}
title={$i18n.t('Delete object')}
title={$i18n.t('Delete object') || ''}
/>
</div>
@ -575,10 +579,6 @@
justify-content: end;
}
.buttons {
display: flex;
}
.error {
color: red;
}

View File

@ -8,13 +8,15 @@
import { i18n } from '../i18n';
const dispatch = createEventDispatcher();
export let entity: Readable<UpObject>;
export let entity: Readable<UpObject | undefined>;
$: groups = Object.fromEntries(
($entity?.attr[ATTR_IN] || []).map((e) => [e.value.c as string, e.address])
);
async function addGroup(address: string) {
if (!$entity) return;
await api.putEntry([
{
entity: $entity.address,
@ -36,7 +38,7 @@
<EntitySetEditor
entities={Object.keys(groups)}
header={$i18n.t('Groups')}
header={$i18n.t('Groups') || ''}
hide={Object.keys(groups).length === 0}
on:add={(e) => addGroup(e.detail)}
on:remove={(e) => removeGroup(e.detail)}

View File

@ -11,7 +11,7 @@
import LabelBorder from './utils/LabelBorder.svelte';
const dispatch = createEventDispatcher();
export let entity: Readable<UpObject>;
export let entity: Readable<UpObject | undefined>;
let adding = false;
let typeSelector: Selector;
@ -21,7 +21,7 @@
$: typeEntries = $entity?.attr[`~${ATTR_OF}`] || [];
async function add(ev: CustomEvent<SelectorValue>) {
if (ev.detail.t !== 'Attribute') {
if (!$entity || ev.detail.t !== 'Attribute') {
return;
}
@ -37,11 +37,13 @@
}
async function remove(entry: UpEntry) {
if (!$entity) return;
let really = confirm(
$i18n.t('Really remove "{{attributeName}}" from "{{typeName}}"?', {
attributeName: (await api.addressToComponents(entry.entity)).c,
typeName: $entity.identify().join('/')
})
}) || ''
);
if (really) {
@ -60,7 +62,7 @@
bind:this={typeSelector}
types={['Attribute', 'NewAttribute']}
on:input={add}
placeholder={$i18n.t('Assign an attribute to this type...')}
placeholder={$i18n.t('Assign an attribute to this type...') || ''}
on:focus={(ev) => {
if (!ev.detail) adding = false;
}}

View File

@ -8,7 +8,7 @@
export let entities: string[];
let groups = [];
let groups: string[] = [];
let groupListing: UpListing | undefined = undefined;
async function updateGroups() {
const currentEntities = entities.concat();
@ -23,7 +23,7 @@
const commonGroups = new Set(
allGroups.values
.filter((v) => v.t == 'Address')
.map((v) => v.c)
.map((v) => v.c as string)
.filter((groupAddr) => {
return Object.values(allGroups.objects).every((obj) => {
return obj.attr[ATTR_IN].some((v) => v.value.c === groupAddr);
@ -54,11 +54,14 @@
async function removeGroup(address: string) {
await Promise.all(
entities.map((entity) =>
api.deleteEntry(
groupListing.objects[entity].attr[ATTR_IN].find((v) => v.value.c === address).address
)
)
entities.map((entity) => {
const group = groupListing?.objects[entity].attr[ATTR_IN].find(
(v) => v.value.c === address
);
if (group) {
return api.deleteEntry(group.address);
}
})
);
await updateGroups();
}
@ -66,7 +69,7 @@
<EntitySetEditor
entities={groups}
header={$i18n.t('Common groups')}
header={$i18n.t('Common groups') || ''}
on:add={(ev) => addGroup(ev.detail)}
on:remove={(ev) => removeGroup(ev.detail)}
/>

View File

@ -1,76 +1,77 @@
<script lang="ts">
import { i18n } from "../i18n";
import { selected } from "./EntitySelect.svelte";
import EntryView from "./EntryView.svelte";
import MultiGroupEditor from "./MultiGroupEditor.svelte";
import Icon from "./utils/Icon.svelte";
import EntityList from "./widgets/EntityList.svelte";
import { i18n } from '../i18n';
import { selected } from './EntitySelect.svelte';
import EntryView from './EntryView.svelte';
import MultiGroupEditor from './MultiGroupEditor.svelte';
import Icon from './utils/Icon.svelte';
import EntityList from './widgets/EntityList.svelte';
import type { Widget } from '$lib/components/EntryView.svelte';
const selectedWidgets = [
{
name: "List",
icon: "list-check",
components: ({ entities }) => [
{
component: EntityList,
props: {
entities,
thumbnails: false,
select: "remove",
},
},
],
},
{
name: "EntityList",
icon: "image",
components: ({ entities }) => [
{
component: EntityList,
props: {
entities,
thumbnails: true,
select: "remove",
},
},
],
},
];
const selectedWidgets: Widget[] = [
{
name: 'List',
icon: 'list-check',
components: ({ entities }) => [
{
component: EntityList,
props: {
entities,
thumbnails: false,
select: 'remove'
}
}
]
},
{
name: 'EntityList',
icon: 'image',
components: ({ entities }) => [
{
component: EntityList,
props: {
entities,
thumbnails: true,
select: 'remove'
}
}
]
}
];
</script>
<div class="view">
<h2>
<Icon plain name="select-multiple" />
{$i18n.t("Selected")}: {$selected.length}
</h2>
<div class="actions">
<MultiGroupEditor entities={$selected} />
</div>
<div class="entities">
<EntryView
title={$i18n.t("Selected entities")}
entities={$selected}
widgets={selectedWidgets}
/>
</div>
<h2>
<Icon plain name="select-multiple" />
{$i18n.t('Selected')}: {$selected.length}
</h2>
<div class="actions">
<MultiGroupEditor entities={$selected} />
</div>
<div class="entities">
<EntryView
title={$i18n.t('Selected entities') || ''}
entities={$selected}
widgets={selectedWidgets}
/>
</div>
</div>
<style lang="scss">
.view {
display: flex;
flex-direction: column;
height: 100%;
}
.view {
display: flex;
flex-direction: column;
height: 100%;
}
h2 {
text-align: center;
margin: 0;
margin-top: -0.66em;
}
h2 {
text-align: center;
margin: 0;
margin-top: -0.66em;
}
.entities {
flex-grow: 1;
overflow-y: auto;
height: 0;
}
.entities {
flex-grow: 1;
overflow-y: auto;
height: 0;
}
</style>

View File

@ -12,6 +12,7 @@
import debug from 'debug';
import { Query } from '@upnd/upend';
import { Any } from '@upnd/upend/query';
import { isDefined } from '$lib/util/werk';
const dbg = debug('kestrel:surface');
const dispatch = createEventDispatcher();
@ -78,13 +79,15 @@
}
let points: IPoint[] = [];
async function loadPoints() {
if (!x || !y) return;
points = [];
const result = await api.query(`(matches ? (in "${x}" "${y}") ?)`);
points = Object.entries(result.objects)
.map(([address, obj]) => {
let objX = parseInt(String(obj.get(x)));
let objY = parseInt(String(obj.get(y)));
let objX = parseInt(String(obj.get(x!)));
let objY = parseInt(String(obj.get(y!)));
if (objX && objY) {
return {
@ -94,7 +97,7 @@
};
}
})
.filter(Boolean);
.filter(isDefined);
tick().then(() => {
autofit();
@ -113,6 +116,10 @@
const d3 = await import('d3');
function init() {
if (!viewEl) {
dbg("Couldn't find view element");
return;
}
viewWidth = viewEl.clientWidth;
viewHeight = viewEl.clientHeight;
@ -164,11 +171,13 @@
}
autofit = () => {
zoom.translateTo(view, 0, viewHeight);
if (!zoom) return;
zoom.translateTo(view as any, 0, viewHeight);
if (points.length) {
zoom.scaleTo(
view,
view as any,
Math.min(
viewWidth / 2 / Math.max(...points.map((p) => Math.abs(p.x))) - 0.3,
viewHeight / 2 / Math.max(...points.map((p) => Math.abs(p.y))) - 0.3
@ -180,10 +189,10 @@
function updateStyles() {
svg
.selectAll('.tick line')
.attr('stroke-width', (d: number) => {
.attr('stroke-width', (d) => {
return d === 0 ? 2 : 1;
})
.attr('stroke', function (d: number) {
.attr('stroke', (d) => {
return d === 0 ? 'var(--foreground-lightest)' : 'var(--foreground-lighter)';
});
}
@ -204,7 +213,7 @@
});
d3.select(viewEl)
.call(zoom)
.call(zoom as any)
.on('dblclick.zoom', (_ev: MouseEvent) => {
selectorCoords = [currentX, currentY];
});
@ -217,7 +226,7 @@
const resizeObserver = new ResizeObserver(() => {
tick().then(() => init());
});
resizeObserver.observe(viewEl);
resizeObserver.observe(viewEl as any);
});
async function onSelectorInput(ev: CustomEvent<SelectorValue>) {
@ -225,13 +234,15 @@
if (value.t !== 'Address') return;
const address = value.c;
const [xValue, yValue] = selectorCoords;
const [xValue, yValue] = selectorCoords as any;
selectorCoords = null;
await Promise.all(
[
[x, xValue],
[y, yValue]
].map(([axis, value]: [string, number]) =>
(
[
[x, xValue],
[y, yValue]
] as any[]
).map(([axis, value]: [string, number]) =>
api.putEntityAttribute(address, axis, {
t: 'Number',
c: value

View File

@ -17,39 +17,40 @@
export let recurse = 3;
$: ({ entity, entityInfo } = useEntity(address));
$: types = $entity && getTypes($entity, $entityInfo);
$: types = $entity && $entityInfo && getTypes($entity, $entityInfo);
$: handled =
types &&
(!$entity ||
types.audio ||
types.video ||
types.image ||
types.text ||
types.model ||
types.web ||
types.fragment ||
(types.group && recurse > 0));
types?.audio ||
types?.video ||
types?.image ||
types?.text ||
types?.model ||
types?.web ||
types?.fragment ||
(types?.group && recurse > 0));
$: dispatch('handled', handled);
let loaded = null;
let loaded: string | boolean = false;
$: dispatch('loaded', Boolean(loaded));
let failedChildren: string[] = [];
let loadedChildren: string[] = [];
$: groupChildren = $entity?.backlinks
.filter((e) => e.attribute === ATTR_IN)
.map((e) => String(e.entity))
.filter(
(addr) =>
!failedChildren
.slice(0, $entity?.backlinks.filter((e) => e.attribute === ATTR_IN).length - 4)
.includes(addr)
)
.slice(0, 4);
$: groupChildren =
$entity?.backlinks
.filter((e) => e.attribute === ATTR_IN)
.map((e) => String(e.entity))
.filter(
(addr) =>
!failedChildren
.slice(0, $entity?.backlinks.filter((e) => e.attribute === ATTR_IN).length || 0 - 4)
.includes(addr)
)
.slice(0, 4) || [];
$: if (groupChildren)
$: if (groupChildren.length)
loaded = groupChildren.every(
(addr) => loadedChildren.includes(addr) || failedChildren.includes(addr)
);
@ -61,7 +62,7 @@
{#if !loaded}
<Spinner centered="absolute" />
{/if}
{#if types.group}
{#if types?.group}
<ul class="group">
{#each groupChildren as address (address)}
<li>
@ -80,28 +81,28 @@
</li>
{/each}
</ul>
{:else if types.model}
{:else if types?.model}
<ModelViewer
lookonly
src="{api.apiUrl}/raw/{address}"
on:loaded={() => (loaded = address)}
/>
{:else if types.web}
{:else if types?.web}
<img
alt="OpenGraph image for {$entityInfo?.t == 'Url' && $entityInfo?.c}"
use:concurrentImage={String($entity?.get('OG_IMAGE'))}
on:load={() => (loaded = address)}
on:error={() => (handled = false)}
/>
{:else if types.fragment}
{:else if types?.fragment}
<FragmentViewer {address} detail={false} on:loaded={() => (loaded = address)} />
{:else if types.audio}
{:else if types?.audio}
<AudioPreview
{address}
on:loaded={() => (loaded = address)}
on:error={() => (handled = false)}
/>
{:else if types.video}
{:else if types?.video}
<VideoViewer {address} detail={false} on:loaded={() => (loaded = address)} />
{:else}
<div class="image" class:loaded={loaded == address || !handled}>
@ -109,7 +110,7 @@
class:loaded={loaded == address}
alt="Thumbnail for {address}..."
use:concurrentImage={`${api.apiUrl}/${
types.mimeType?.includes('svg+xml') ? 'raw' : 'thumb'
types?.mimeType?.includes('svg+xml') ? 'raw' : 'thumb'
}/${address}?size=512&quality=75`}
on:load={() => (loaded = address)}
on:error={() => (handled = false)}
@ -168,8 +169,6 @@
}
.group {
padding: 0;
flex-grow: 1;
min-height: 0;
width: 100%;

View File

@ -1,61 +1,64 @@
<script lang="ts">
import { onMount } from "svelte";
import { onMount } from 'svelte';
const BADGE_HEIGHT = 3;
export let address: string;
const BADGE_HEIGHT = 3;
export let address: string;
let canvas: HTMLCanvasElement | undefined;
let width = 0;
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]);
}
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);
width = Math.ceil(bytes.length / 3 / BADGE_HEIGHT);
onMount(() => {
const ctx = canvas?.getContext("2d");
if (!ctx) {
console.warn("Couldn't initialize canvas!");
return;
}
onMount(() => {
const ctx = canvas?.getContext('2d');
if (!ctx) {
console.warn("Couldn't initialize canvas!");
return;
}
const hueRange = 120;
const hueCenter = 90 + bytes.length * 2.5;
const hueRange = 120;
const hueCenter = 90 + bytes.length * 2.5;
let idx = 0;
function draw() {
const tmp = [];
while (bytes.length > 0 && tmp.length < 3) {
tmp.push(bytes.shift());
}
while (tmp.length < 3) {
tmp.push(tmp[tmp.length - 1]);
}
let idx = 0;
function draw() {
if (!ctx) return;
const h = (tmp[0] / 128) * hueRange + hueCenter - hueRange / 2;
const s = (tmp[1] / 128) * 100;
const l = (tmp[2] / 128) * 100;
ctx.fillStyle = `hsl(${h},${s}%,${l}%)`;
ctx.fillRect(Math.floor(idx / BADGE_HEIGHT), idx % BADGE_HEIGHT, 1, 1);
idx++;
if (bytes.length > 0) {
requestAnimationFrame(draw);
}
}
const tmp = [];
while (bytes.length > 0 && tmp.length < 3) {
tmp.push(bytes.shift());
}
while (tmp.length < 3) {
tmp.push(tmp[tmp.length - 1]);
}
requestAnimationFrame(draw);
});
const h = (tmp[0]! / 128) * hueRange + hueCenter - hueRange / 2;
const s = (tmp[1]! / 128) * 100;
const l = (tmp[2]! / 128) * 100;
ctx.fillStyle = `hsl(${h},${s}%,${l}%)`;
ctx.fillRect(Math.floor(idx / BADGE_HEIGHT), idx % BADGE_HEIGHT, 1, 1);
idx++;
if (bytes.length > 0) {
requestAnimationFrame(draw);
}
}
requestAnimationFrame(draw);
});
</script>
<canvas bind:this={canvas} {width} height="3" title={address} />
<!--suppress CssOverwrittenProperties -->
<style>
canvas {
display: inline-block;
height: 1em;
image-rendering: optimizeSpeed;
image-rendering: pixelated;
}
canvas {
display: inline-block;
height: 1em;
image-rendering: optimizeSpeed;
image-rendering: pixelated;
}
</style>

View File

@ -1,50 +1,50 @@
<script lang="ts">
import type { Address } from "@upnd/upend/types";
import UpObject from "./UpObject.svelte";
import UpLink from "./UpLink.svelte";
import type { Address } from '@upnd/upend/types';
import UpObject from './UpObject.svelte';
import UpLink from './UpLink.svelte';
export let address: Address;
let popup = false;
export let address: Address;
let popup = false;
</script>
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<UpLink passthrough to={{ entity: address }}>
<div
class="surface-point"
on:mouseover={() => (popup = true)}
on:mouseleave={() => (popup = false)}
>
{#if popup}
<div class="popup-inner">
<UpObject {address} />
</div>
{/if}
</div>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="surface-point"
on:mouseover={() => (popup = true)}
on:mouseleave={() => (popup = false)}
>
{#if popup}
<div class="popup-inner">
<UpObject {address} />
</div>
{/if}
</div>
</UpLink>
<style lang="scss">
@use "../../styles/colors.scss";
@use '../../styles/colors.scss';
.surface-point {
display: relative;
.surface-point {
display: relative;
width: 0.75rem;
height: 0.75rem;
border-radius: 25%;
background: colors.$red;
box-shadow: 0 0 0 1px darken(colors.$red, 20%);
width: 0.75rem;
height: 0.75rem;
border-radius: 25%;
background: colors.$red;
box-shadow: 0 0 0 1px darken(colors.$red, 20%);
cursor: pointer;
&:hover {
background: lighten(colors.$red, 20%);
}
}
cursor: pointer;
&:hover {
background: lighten(colors.$red, 20%);
}
}
.popup-inner {
position: relative;
top: 1rem;
display: inline-block;
transform: translateX(-50%);
}
.popup-inner {
position: relative;
top: 1rem;
display: inline-block;
transform: translateX(-50%);
}
</style>

View File

@ -2,7 +2,7 @@
import { useEntity } from '$lib/entity';
import api from '$lib/api';
import { createEventDispatcher } from 'svelte';
import { formatDuration } from '../../../util/fragments/time';
import { formatDuration } from '$lib/util/fragments/time';
import { concurrentImage } from '../../imageQueue';
const dispatch = createEventDispatcher();
@ -10,7 +10,7 @@
$: ({ entity } = useEntity(address));
let loaded = null;
let loaded: string | null = null;
let handled = true;
$: dispatch('handled', handled);
$: dispatch('loaded', Boolean(loaded));
@ -70,6 +70,6 @@
font-size: var(--font-size);
font-weight: bold;
color: var(--foreground-lightest);
text-shadow: 0px 0px 0.2em var(--background-lighter);
text-shadow: 0 0 0.2em var(--background-lighter);
}
</style>

View File

@ -5,14 +5,14 @@
import type WaveSurfer from 'wavesurfer.js';
import type { Region, RegionParams } from 'wavesurfer.js/src/plugin/regions';
import api from '$lib/api';
import { TimeFragment } from '../../../util/fragments/time';
import { TimeFragment } from '$lib/util/fragments/time';
import Icon from '../../utils/Icon.svelte';
import Selector from '../../utils/Selector.svelte';
import UpObject from '../../display/UpObject.svelte';
import Spinner from '../../utils/Spinner.svelte';
import IconButton from '../../../components/utils/IconButton.svelte';
import LabelBorder from '../../../components/utils/LabelBorder.svelte';
import { i18n } from '../../../i18n';
import { i18n } from '$lib/i18n';
import { ATTR_LABEL } from '@upnd/upend/constants';
import debug from 'debug';
const dbg = debug('kestrel:AudioViewer');
@ -209,7 +209,7 @@
wavesurfer.on('region-removed', (region: UpRegion) => {
dbg('wavesurfer region-removed', region);
currentAnnotation = null;
currentAnnotation = undefined;
deleteAnnotation(region);
});
@ -252,7 +252,7 @@
confirm(
$i18n.t(
'File is large (>20 MiB) and UpEnd failed to load waveform from server. Generating the waveform locally may slow down your browser. Do you wish to proceed anyway?'
)
) || ''
)
) {
console.warn(`Failed to load peaks, falling back to client-side render...`);
@ -277,7 +277,7 @@
<header>
<IconButton
name="edit"
title={$i18n.t('Toggle Edit Mode')}
title={$i18n.t('Toggle Edit Mode') || ''}
on:click={() => (editable = !editable)}
active={editable}
>
@ -306,6 +306,7 @@
value={Math.round(currentAnnotation.start * 100) / 100}
disabled={!editable}
on:input={(ev) => {
if (!currentAnnotation) return;
currentAnnotation.update({
start: parseInt(ev.currentTarget.value)
});
@ -319,6 +320,7 @@
value={Math.round(currentAnnotation.end * 100) / 100}
disabled={!editable}
on:input={(ev) => {
if (!currentAnnotation) return;
currentAnnotation.update({
end: parseInt(ev.currentTarget.value)
});
@ -332,6 +334,7 @@
value={currentAnnotation.color || DEFAULT_ANNOTATION_COLOR}
disabled={!editable}
on:input={(ev) => {
if (!currentAnnotation) return;
currentAnnotation.update({ color: ev.currentTarget.value });
updateAnnotation(currentAnnotation);
}}
@ -340,7 +343,7 @@
</div>
{#if editable}
<div class="existControls">
<IconButton outline name="trash" on:click={() => currentAnnotation.remove()} />
<IconButton outline name="trash" on:click={() => currentAnnotation?.remove()} />
<!-- <div class="button">
<Icon name="check" />
</div> -->
@ -354,6 +357,7 @@
initial={currentAnnotation.data}
disabled={!editable}
on:input={(ev) => {
if (!currentAnnotation) return;
currentAnnotation.update({ data: ev.detail });
updateAnnotation(currentAnnotation);
}}

View File

@ -81,7 +81,8 @@
let a8sLinkAddress: string;
async function loaded() {
const { Annotorious } = await import('@recogito/annotorious');
// noinspection TypeScriptCheckImport
const { Annotorious } = (await import('@recogito/annotorious')) as any;
if (anno) {
anno.destroy();

View File

@ -1,86 +1,81 @@
<script lang="ts">
import { createEventDispatcher, onMount } from "svelte";
const dispatch = createEventDispatcher();
import { createEventDispatcher, onMount } from 'svelte';
const dispatch = createEventDispatcher();
export let src: string;
export let lookonly = false;
export let src: string;
export let lookonly = false;
let root: HTMLElement;
let root: HTMLElement;
onMount(async () => {
root.style.height = `${root.clientWidth}px`;
onMount(async () => {
root.style.height = `${root.clientWidth}px`;
const THREE = await import("three");
const THREE_OC = await import("three/examples/jsm/controls/OrbitControls");
const THREE_STL = await import("three/examples/jsm/loaders/STLLoader");
const THREE = await import('three');
const THREE_OC = await import('three/examples/jsm/controls/OrbitControls');
const THREE_STL = await import('three/examples/jsm/loaders/STLLoader');
const camera = new THREE.PerspectiveCamera(
70,
root.clientWidth / root.clientHeight,
);
const camera = new THREE.PerspectiveCamera(70, root.clientWidth / root.clientHeight);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(root.clientWidth, root.clientHeight);
root.appendChild(renderer.domElement);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(root.clientWidth, root.clientHeight);
root.appendChild(renderer.domElement);
const controls = new THREE_OC.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.1;
controls.enableZoom = true;
controls.autoRotate = true;
controls.autoRotateSpeed = 3;
const controls = new THREE_OC.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.1;
controls.enableZoom = true;
controls.autoRotate = true;
controls.autoRotateSpeed = 3;
const scene = new THREE.Scene();
scene.add(new THREE.HemisphereLight(0xffffff, 1.5));
const scene = new THREE.Scene();
scene.add(new THREE.HemisphereLight(0xffffff, 1.5));
const loader = new THREE_STL.STLLoader();
loader.load(src, (geometry) => {
const material = new THREE.MeshPhongMaterial({
color: 0xdc322f,
specular: 100,
shininess: 70,
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
const loader = new THREE_STL.STLLoader();
loader.load(src, (geometry: any) => {
const material = new THREE.MeshPhongMaterial({
color: 0xdc322f,
specular: 100,
shininess: 70
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
const middle = new THREE.Vector3();
geometry.computeBoundingBox();
geometry.boundingBox.getCenter(middle);
mesh.geometry.applyMatrix4(
new THREE.Matrix4().makeTranslation(-middle.x, -middle.y, -middle.z),
);
mesh.geometry.applyMatrix4(
new THREE.Matrix4().makeRotationX(-Math.PI / 2),
);
const middle = new THREE.Vector3();
geometry.computeBoundingBox();
geometry.boundingBox.getCenter(middle);
mesh.geometry.applyMatrix4(
new THREE.Matrix4().makeTranslation(-middle.x, -middle.y, -middle.z)
);
mesh.geometry.applyMatrix4(new THREE.Matrix4().makeRotationX(-Math.PI / 2));
const largestDimension = Math.max(
geometry.boundingBox.max.x,
geometry.boundingBox.max.y,
geometry.boundingBox.max.z,
);
camera.position.z = largestDimension * 2;
});
const largestDimension = Math.max(
geometry.boundingBox.max.x,
geometry.boundingBox.max.y,
geometry.boundingBox.max.z
);
camera.position.z = largestDimension * 2;
});
function animate() {
controls.update();
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
animate();
function animate() {
controls.update();
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
animate();
dispatch("loaded");
});
dispatch('loaded');
});
</script>
<div class="modelviewer" class:lookonly bind:this={root} />
<style>
.modelviewer {
width: 100%;
max-height: 100%;
}
.modelviewer {
width: 100%;
max-height: 100%;
}
.modelviewer.lookonly {
pointer-events: none;
}
.modelviewer.lookonly {
pointer-events: none;
}
</style>

View File

@ -2,6 +2,7 @@
import api from '$lib/api';
import IconButton from '../../utils/IconButton.svelte';
import Spinner from '../../utils/Spinner.svelte';
export let address: string;
let mode: 'preview' | 'full' | 'markdown' = 'preview';
@ -37,6 +38,8 @@
mode = targetMode;
}
}}
role="button"
tabindex="0"
>
<IconButton name={icon} active={mode == targetMode} on:click={() => (mode = targetMode)} />
<div class="label">{label}</div>
@ -48,6 +51,7 @@
<Spinner centered />
{:then text}
{#if mode === 'markdown'}
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html text}
{:else}
{text}{#if mode === 'preview'}{/if}

View File

@ -41,7 +41,7 @@ class ImageQueue {
});
while (this.active < this.concurrency && this.queue.length) {
const nextIdx = this.queue.findIndex((e) => e.check()) || 0;
const nextIdx = this.queue.findIndex((e) => e.check && e.check()) || 0;
const next = this.queue.splice(nextIdx, 1)[0];
dbg(`Getting ${next.id}...`);
this.active += 1;

View File

@ -24,6 +24,7 @@
export let active = 0;
$: active = activeJobs.length;
// eslint-disable-next-line no-undef
let timeout: NodeJS.Timeout;
async function updateJobs() {
clearTimeout(timeout);

View File

@ -1,6 +1,6 @@
<script lang="ts">
import type { UpNotification, UpNotificationLevel } from '../../notifications';
import { notify } from '../../notifications';
import type { UpNotification, UpNotificationLevel } from '$lib/notifications';
import { notify } from '$lib/notifications';
import { fade } from 'svelte/transition';
import Icon from '../utils/Icon.svelte';
import { DEBUG, lipsum } from '$lib/debug';
@ -55,9 +55,10 @@
}, 5000);
});
const icons = {
const icons: Record<UpNotificationLevel, string | undefined> = {
error: 'error-alt',
warning: 'error'
warning: 'error',
info: undefined
};
</script>

View File

@ -1,96 +1,94 @@
<script lang="ts">
import Selector, {
type SELECTOR_TYPE,
type SelectorValue,
} from "./Selector.svelte";
import { createEventDispatcher } from "svelte";
import type { IValue } from "@upnd/upend/types";
import IconButton from "./IconButton.svelte";
import Selector, { type SELECTOR_TYPE, type SelectorValue } from './Selector.svelte';
import { createEventDispatcher } from 'svelte';
import type { IValue } from '@upnd/upend/types';
import IconButton from './IconButton.svelte';
const dispatch = createEventDispatcher();
const dispatch = createEventDispatcher();
export let value: IValue | undefined = undefined;
export let types: SELECTOR_TYPE[] | undefined = undefined;
let newValue: SelectorValue = value;
export let value: IValue | undefined = undefined;
export let types: SELECTOR_TYPE[] | undefined = undefined;
let newValue: SelectorValue | undefined = value;
let editing = false;
let editing = false;
let selector: Selector;
let hover = false;
let focus = false;
let selector: Selector;
let hover = false;
let focus = false;
$: if (editing && selector) selector.focus();
$: if (!focus && !hover) editing = false;
$: if (editing && selector) selector.focus();
$: if (!focus && !hover) editing = false;
function onInput(ev: CustomEvent<SelectorValue>) {
newValue = ev.detail;
selector.focus();
}
function onInput(ev: CustomEvent<SelectorValue>) {
newValue = ev.detail;
selector.focus();
}
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="editable"
class:editing
on:mouseenter={() => (hover = true)}
on:mouseleave={() => (hover = false)}
class="editable"
class:editing
on:mouseenter={() => (hover = true)}
on:mouseleave={() => (hover = false)}
>
<div class="inner">
{#if editing}
<div
class="selector"
on:keydown={(ev) => {
if (ev.key === "Escape") {
editing = false;
}
}}
>
<Selector
{types}
bind:this={selector}
on:focus={(ev) => (focus = ev.detail)}
on:input={onInput}
/>
</div>
<IconButton
name="save"
on:click={() => {
dispatch("edit", newValue);
editing = false;
}}
/>
{:else}
<div class="content">
<slot />
</div>
<div class="edit-icon">
<IconButton name="edit" on:click={() => (editing = true)} />
</div>
{/if}
</div>
<div class="inner">
{#if editing}
<div
class="selector"
on:keydown={(ev) => {
if (ev.key === 'Escape') {
editing = false;
}
}}
>
<Selector
{types}
bind:this={selector}
on:focus={(ev) => (focus = ev.detail)}
on:input={onInput}
/>
</div>
<IconButton
name="save"
on:click={() => {
dispatch('edit', newValue);
editing = false;
}}
/>
{:else}
<div class="content">
<slot />
</div>
<div class="edit-icon">
<IconButton name="edit" on:click={() => (editing = true)} />
</div>
{/if}
</div>
</div>
<style lang="scss">
.edit-icon {
opacity: 0;
transition: opacity 0.2s ease-in-out;
}
.edit-icon {
opacity: 0;
transition: opacity 0.2s ease-in-out;
}
.editable:hover .edit-icon {
opacity: 0.8;
}
.editable:hover .edit-icon {
opacity: 0.8;
}
.inner {
display: flex;
gap: 0.25em;
align-items: center;
}
.inner {
display: flex;
gap: 0.25em;
align-items: center;
}
.content {
min-width: 0;
}
.content {
min-width: 0;
}
.selector {
flex-grow: 1;
min-width: 0;
}
.selector {
flex-grow: 1;
min-width: 0;
}
</style>

View File

@ -1,68 +1,68 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
let input: HTMLInputElement;
import { createEventDispatcher } from 'svelte';
let input: HTMLInputElement;
const dispatch = createEventDispatcher();
const dispatch = createEventDispatcher();
export let placeholder = "";
export let value = "";
export let disabled = false;
export let size: number | undefined = 7;
export let placeholder = '';
export let value = '';
export let disabled = false;
export let size: number | undefined = 7;
let focused = false;
$: dispatch("focusChange", focused);
let focused = false;
$: dispatch('focusChange', focused);
function onInput() {
dispatch("input", value);
}
function onInput() {
dispatch('input', value);
}
export function focus() {
input.focus();
}
export function focus() {
input.focus();
}
</script>
<div class="input" class:focused>
<slot name="prefix" />
<input
bind:this={input}
{placeholder}
bind:value
on:input={onInput}
on:focus={() => (focused = true)}
on:blur={() => (focused = false)}
size={Math.max(value.length, size)}
on:keydown
{disabled}
/>
<slot name="prefix" />
<input
bind:this={input}
{placeholder}
bind:value
on:input={onInput}
on:focus={() => (focused = true)}
on:blur={() => (focused = false)}
size={Math.max(value.length, size || 0)}
on:keydown
{disabled}
/>
</div>
<style lang="scss">
.input {
display: flex;
align-items: center;
gap: 0.25em;
padding: 0.25em;
.input {
display: flex;
align-items: center;
gap: 0.25em;
padding: 0.25em;
border: 1px solid var(--foreground-lighter);
border-radius: 4px;
background: var(--background);
border: 1px solid var(--foreground-lighter);
border-radius: 4px;
background: var(--background);
transition: box-shadow 0.25s;
&.focused {
box-shadow: 0 0 2px 3px var(--primary);
}
}
transition: box-shadow 0.25s;
&.focused {
box-shadow: 0 0 2px 3px var(--primary);
}
}
input {
flex-grow: 1;
min-width: 0;
input {
flex-grow: 1;
min-width: 0;
color: var(--foreground);
background: transparent;
border: none;
color: var(--foreground);
background: transparent;
border: none;
&:focus {
outline: none;
}
}
&:focus {
outline: none;
}
}
</style>

View File

@ -82,14 +82,14 @@
import { createEventDispatcher } from 'svelte';
import type { UpListing } from '@upnd/upend';
import type { Address } from '@upnd/upend/types';
import { baseSearchOnce, createLabelled } from '../../util/search';
import { baseSearchOnce, createLabelled } from '$lib/util/search';
import UpObject from '../display/UpObject.svelte';
import IconButton from './IconButton.svelte';
import Input from './Input.svelte';
import { matchSorter } from 'match-sorter';
import api from '$lib/api';
import { ATTR_LABEL } from '@upnd/upend/constants';
import { i18n } from '../../i18n';
import { i18n } from '$lib/i18n';
import debug from 'debug';
import Spinner from './Spinner.svelte';
@ -187,7 +187,7 @@
t: 'Address',
c: addr,
labels: addressToLabels[addr],
entry: null
entry: undefined
})
);
} else if (query.length && types.includes('NewAddress')) {
@ -212,7 +212,7 @@
t: 'Address' as const,
c: entry.entity
};
if (entry.attribute == ATTR_LABEL) {
if (entry.attribute == ATTR_LABEL && entry.value.c) {
result.push({
...common,
labels: [entry.value.c.toString()]
@ -226,7 +226,7 @@
if (types.includes('Attribute')) {
const allAttributes = await api.fetchAllAttributes();
const attributes = attributeOptions
? allAttributes.filter((attr) => attributeOptions.includes(attr.name))
? allAttributes.filter((attr) => attributeOptions!.includes(attr.name))
: allAttributes;
if (emptyOptions === undefined || query.length > 0) {
result.push(
@ -434,7 +434,7 @@
<li><Spinner centered /></li>
{/if}
{#each options.slice(0, MAX_OPTIONS) as option, idx}
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<!-- svelte-ignore a11y-no-noninteractive-tabindex a11y-no-noninteractive-element-interactions -->
<li
tabindex="0"
on:click={() => set(option)}
@ -463,7 +463,7 @@
<div class="content new">{option.c}</div>
<div class="type">{$i18n.t('Create object')}</div>
{:else if option.t === 'Attribute'}
{#if option.labels.length}
{#if option.labels?.length}
<div class="content">
{#each option.labels as label}
<div class="label">{label}</div>

View File

@ -6,11 +6,11 @@
import UpObject from '../display/UpObject.svelte';
import UpObjectCard from '../display/UpObjectCard.svelte';
import { ATTR_LABEL } from '@upnd/upend/constants';
import { i18n } from '../../i18n';
import { i18n } from '$lib/i18n';
import IconButton from '../utils/IconButton.svelte';
import Selector, { type SelectorValue } from '../utils/Selector.svelte';
import { createEventDispatcher } from 'svelte';
import type { WidgetChange } from 'src/types/base';
import type { WidgetChange } from '$lib/types/base';
import debug from 'debug';
const dispatch = createEventDispatcher();
const dbg = debug(`kestrel:EntityList`);
@ -70,7 +70,7 @@
}
// Labelling
let labelListing: Readable<UpListing> = readable(undefined);
let labelListing: Readable<UpListing | undefined> = readable(undefined);
$: {
const addressesString = deduplicatedEntities.map((addr) => `@${addr}`).join(' ');
@ -80,7 +80,7 @@
$: {
if ($labelListing) {
deduplicatedEntities.forEach((address) => {
addSortKeys(address, $labelListing.getObject(address).identify(), false);
addSortKeys(address, $labelListing?.getObject(address).identify() || [], false);
});
sortEntities();
}
@ -135,7 +135,7 @@
}
function removeEntity(address: string) {
if (confirm($i18n.t('Are you sure you want to remove this entry from members?'))) {
if (confirm($i18n.t('Are you sure you want to remove this entry from members?') || '')) {
dbg('Removing entity', address);
dispatch('change', {
type: 'entry-delete',
@ -194,7 +194,7 @@
{#if adding}
<Selector
bind:this={addSelector}
placeholder={$i18n.t('Search database or paste an URL')}
placeholder={$i18n.t('Search database or paste an URL') || ''}
types={['Address', 'NewAddress']}
on:input={addEntity}
on:focus={(ev) => {

View File

@ -4,17 +4,17 @@
import Ellipsis from '../utils/Ellipsis.svelte';
import UpObject from '../display/UpObject.svelte';
import { createEventDispatcher } from 'svelte';
import type { AttributeUpdate, WidgetChange } from '../../types/base';
import type { AttributeUpdate, WidgetChange } from '$lib/types/base';
import type { UpEntry, UpListing } from '@upnd/upend';
import IconButton from '../utils/IconButton.svelte';
import Selector, { type SelectorValue, selectorValueAsValue } from '../utils/Selector.svelte';
import Editable from '../utils/Editable.svelte';
import { query } from '$lib/entity';
import { type Readable, readable } from 'svelte/store';
import { defaultEntitySort, entityValueSort } from '../../util/sort';
import { attributeLabels } from '../../util/labels';
import { formatDuration } from '../../util/fragments/time';
import { i18n } from '../../i18n';
import { defaultEntitySort, entityValueSort } from '$lib/util/sort';
import { attributeLabels } from '$lib/util/labels';
import { formatDuration } from '$lib/util/fragments/time';
import { i18n } from '$lib/i18n';
import UpLink from '../display/UpLink.svelte';
import { ATTR_ADDED, ATTR_LABEL } from '@upnd/upend/constants';
@ -68,7 +68,7 @@
newEntryValue = undefined;
}
async function removeEntry(address: string) {
if (confirm($i18n.t('Are you sure you want to remove the property?'))) {
if (confirm($i18n.t('Are you sure you want to remove the property?') || '')) {
dispatch('change', { type: 'delete', address } as WidgetChange);
}
}
@ -82,9 +82,9 @@
}
// Labelling
let labelListing: Readable<UpListing> = readable(undefined);
let labelListing: Readable<UpListing | undefined> = readable(undefined);
$: {
const addresses = [];
const addresses: string[] = [];
entries
.flatMap((e) => (e.value.t === 'Address' ? [e.entity, e.value.c] : [e.entity]))
.forEach((addr) => {
@ -126,12 +126,12 @@
$: {
if ($labelListing) {
entries.forEach((entry) => {
addSortKeys(entry.entity, $labelListing.getObject(entry.entity).identify(), false);
addSortKeys(entry.entity, $labelListing!.getObject(entry.entity).identify(), false);
if (entry.value.t === 'Address') {
addSortKeys(
entry.value.c,
$labelListing.getObject(String(entry.value.c)).identify(),
$labelListing!.getObject(String(entry.value.c)).identify(),
false
);
}
@ -141,9 +141,13 @@
}
entries.forEach((entry) => {
addSortKeys(entry.entity, entry.listing.getObject(entry.entity).identify(), false);
addSortKeys(entry.entity, entry.listing?.getObject(entry.entity).identify() || [], false);
if (entry.value.t === 'Address') {
addSortKeys(entry.value.c, entry.listing.getObject(String(entry.value.c)).identify(), false);
addSortKeys(
entry.value.c,
entry.listing?.getObject(String(entry.value.c)).identify() || [],
false
);
}
});
sortEntries();
@ -184,7 +188,7 @@
value: $i18n.t('Value')
};
function formatValue(value: string | number, attribute: string): string {
function formatValue(value: string | number | null, attribute: string): string {
try {
switch (attribute) {
case 'FILE_SIZE':
@ -204,7 +208,7 @@
}
// Unused attributes
let unusedAttributes = [];
let unusedAttributes: string[] = [];
$: (async () => {
unusedAttributes = await Promise.all(
@ -335,6 +339,7 @@
{#if !attributes?.length}
{#if adding}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="add-row"
on:mouseenter={() => (addHover = true)}
@ -364,7 +369,10 @@
{/if}
{/each}
<div class="attr-action">
<IconButton name="save" on:click={() => addEntry(newEntryAttribute, newEntryValue)} />
<IconButton
name="save"
on:click={() => newEntryValue && addEntry(newEntryAttribute, newEntryValue)}
/>
</div>
</div>
{:else}

View File

@ -1,40 +1,38 @@
import { writable } from "svelte/store";
import { debug } from "debug";
const dbg = debug("kestrel:swrshim");
import { writable } from 'svelte/store';
import { debug } from 'debug';
const dbg = debug('kestrel:swrshim');
// 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,
options?: RequestInit,
) {
const data = writable<D | undefined>();
const error = writable<E | undefined>();
export function useSWR<D = unknown, E = Error>(key: SWRKey, options?: RequestInit) {
const data = writable<D | undefined>();
const error = writable<E | undefined>();
async function doFetch() {
dbg("Fetching: %s", key);
try {
const response = await fetch(key, options);
if (response.ok) {
data.set(await response.json());
} else {
let errorText = `${response.status} ${response.statusText}`;
const responseText = await response.text();
if (responseText) {
errorText += ` - ${responseText}`;
}
throw new Error(errorText);
}
} catch (err) {
error.set(err);
}
}
async function doFetch() {
dbg('Fetching: %s', key);
try {
const response = await fetch(key, options);
if (response.ok) {
data.set(await response.json());
} else {
let errorText = `${response.status} ${response.statusText}`;
const responseText = await response.text();
if (responseText) {
errorText += ` - ${responseText}`;
}
throw new Error(errorText);
}
} catch (err) {
error.set(err as any);
}
}
doFetch();
doFetch();
return {
data,
error,
revalidate: doFetch,
};
return {
data,
error,
revalidate: doFetch
};
}

View File

@ -2,45 +2,43 @@
* Both `start` and `end` are in seconds.
*/
export class TimeFragment {
start: number | null;
end: number | null;
start: number;
end: number | null;
constructor(start: number, end: number) {
this.start = start;
this.end = end;
}
constructor(start: number, end: number | null) {
this.start = start;
this.end = end;
}
public static parse(fragment: string): TimeFragment {
if (!fragment.startsWith("t=")) {
return undefined;
}
const data = fragment.substring("t=".length);
try {
const [start, end] = data.split(",").map((str) => parseFloat(str));
return new TimeFragment(start || null, end || null);
} catch {
return undefined;
}
}
public static parse(fragment: string): TimeFragment | undefined {
if (!fragment.startsWith('t=')) {
return undefined;
}
const data = fragment.substring('t='.length);
try {
const [start, end] = data.split(',').map((str) => parseFloat(str));
return new TimeFragment(start, end || null);
} catch {
return undefined;
}
}
public toString(): string {
return `t=${this.start || ""},${this.end || ""}`;
}
public toString(): string {
return `t=${this.start || ''},${this.end || ''}`;
}
}
export function formatDuration(duration: number): string {
let result = "";
const hours = Math.floor(duration / 3600);
const minutes = Math.floor((duration % 3600) / 60);
const seconds = Math.floor(duration - hours * 3600 - minutes * 60);
const hours = Math.floor(duration / 3600);
const minutes = Math.floor((duration % 3600) / 60);
const seconds = Math.floor(duration - hours * 3600 - minutes * 60);
let result = '';
if (hours > 0) {
result += `${hours}h`;
}
result += `${minutes}m`.padStart(hours > 0 ? 3 : 2, '0');
result += `${seconds}s`.padStart(3, '0');
result = "";
if (hours > 0) {
result += `${hours}h`;
}
result += `${minutes}m`.padStart(hours > 0 ? 3 : 2, "0");
result += `${seconds}s`.padStart(3, "0");
return result;
return result;
}

View File

@ -2,62 +2,61 @@
// https://github.com/tomayac/xywh.js
export type MediaFragment = (
| {
mediaItem: HTMLImageElement;
mediaType: "img";
}
| {
mediaItem: HTMLVideoElement;
mediaType: "video";
}
| {
mediaItem: HTMLImageElement;
mediaType: 'img';
}
| {
mediaItem: HTMLVideoElement;
mediaType: 'video';
}
) & {
unit: string;
x: number;
y: number;
w: number;
h: number;
unit: string;
x: number;
y: number;
w: number;
h: number;
};
export function xywh(mediaItem: HTMLImageElement | HTMLVideoElement) {
const source = mediaItem.src || mediaItem.currentSrc;
// See http://www.w3.org/TR/media-frags/#naming-space
const xywhRegEx =
/[#&?]xywh=(pixel:|percent:)?([\d.]+),([\d.]+),([\d.]+),([\d.]+)/;
const match = xywhRegEx.exec(source);
if (match) {
const mediaFragment = {
mediaItem: mediaItem,
mediaType: mediaItem.nodeName.toLowerCase(),
unit: match[1] ? match[1] : "pixel:",
x: parseFloat(match[2]),
y: parseFloat(match[3]),
w: parseFloat(match[4]),
h: parseFloat(match[5]),
} as MediaFragment;
if (mediaFragment.mediaType === "img") {
addImageLoadListener(mediaFragment);
} else {
addVideoLoadListener(mediaFragment);
}
}
const source = mediaItem.src || mediaItem.currentSrc;
// See http://www.w3.org/TR/media-frags/#naming-space
const xywhRegEx = /[#&?]xywh=(pixel:|percent:)?([\d.]+),([\d.]+),([\d.]+),([\d.]+)/;
const match = xywhRegEx.exec(source);
if (match) {
const mediaFragment = {
mediaItem: mediaItem,
mediaType: mediaItem.nodeName.toLowerCase(),
unit: match[1] ? match[1] : 'pixel:',
x: parseFloat(match[2]),
y: parseFloat(match[3]),
w: parseFloat(match[4]),
h: parseFloat(match[5])
} as MediaFragment;
if (mediaFragment.mediaType === 'img') {
addImageLoadListener(mediaFragment);
} else {
addVideoLoadListener(mediaFragment);
}
}
}
/**
* Applies the media fragment when the image has loaded.
*/
function addImageLoadListener(mediaFragment: MediaFragment) {
const mediaItem = mediaFragment.mediaItem;
// Prevent onload firing when the fragment loads; but still react when `src`
// is changed programatically.
let lastSrc: string;
function onload() {
if (mediaItem.src !== lastSrc) {
// Required on reloads because of size calculations.
applyFragment(mediaFragment);
lastSrc = mediaItem.src;
}
}
mediaItem.addEventListener("load", onload);
const mediaItem = mediaFragment.mediaItem;
// Prevent onload firing when the fragment loads; but still react when `src`
// is changed programatically.
let lastSrc: string;
function onload() {
if (mediaItem.src !== lastSrc) {
// Required on reloads because of size calculations.
applyFragment(mediaFragment);
lastSrc = mediaItem.src;
}
}
mediaItem.addEventListener('load', onload);
}
/**
@ -65,9 +64,9 @@ function addImageLoadListener(mediaFragment: MediaFragment) {
* need the video's original width and height.
*/
function addVideoLoadListener(mediaFragment: MediaFragment) {
mediaFragment.mediaItem.addEventListener("loadedmetadata", function () {
applyFragment(mediaFragment);
});
mediaFragment.mediaItem.addEventListener('loadedmetadata', function () {
applyFragment(mediaFragment);
});
}
/**
@ -82,77 +81,77 @@ function addVideoLoadListener(mediaFragment: MediaFragment) {
* 2D transformation according to the fragment's x and y values.
*/
function applyFragment(fragment: MediaFragment) {
// Media item is a video
if (fragment.mediaType === "video") {
let x: string, y: string, w: string, h: string;
const originalWidth = fragment.mediaItem.videoWidth;
const originalHeight = fragment.mediaItem.videoHeight;
// Unit is pixel:
if (fragment.unit === "pixel:") {
const scale = originalWidth / fragment.mediaItem.clientWidth;
w = fragment.w * scale + "px";
h = fragment.h * scale + "px";
x = "-" + fragment.x * scale + "px";
y = "-" + fragment.y * scale + "px";
// Unit is percent:
} else {
w = (originalWidth * fragment.w) / 100 + "px";
h = (originalHeight * fragment.h) / 100 + "px";
x = "-" + (originalWidth * fragment.x) / 100 + "px";
y = "-" + (originalHeight * fragment.y) / 100 + "px";
}
// Media item is a video
if (fragment.mediaType === 'video') {
let x: string, y: string, w: string, h: string;
const originalWidth = fragment.mediaItem.videoWidth;
const originalHeight = fragment.mediaItem.videoHeight;
// Unit is pixel:
if (fragment.unit === 'pixel:') {
const scale = originalWidth / fragment.mediaItem.clientWidth;
w = fragment.w * scale + 'px';
h = fragment.h * scale + 'px';
x = '-' + fragment.x * scale + 'px';
y = '-' + fragment.y * scale + 'px';
// Unit is percent:
} else {
w = (originalWidth * fragment.w) / 100 + 'px';
h = (originalHeight * fragment.h) / 100 + 'px';
x = '-' + (originalWidth * fragment.x) / 100 + 'px';
y = '-' + (originalHeight * fragment.y) / 100 + 'px';
}
const wrapper = document.createElement("div");
wrapper.style.cssText +=
"overflow:hidden;" +
"width:" +
w +
";" +
"height:" +
h +
";" +
"padding:0;" +
"margin:0;" +
"border-radius:0;" +
"border:none;";
fragment.mediaItem.style.cssText +=
"transform:translate(" +
x +
"," +
y +
");" +
"-webkit-transform:translate(" +
x +
"," +
y +
");";
// Evil DOM operations
fragment.mediaItem.parentNode.insertBefore(wrapper, fragment.mediaItem);
wrapper.appendChild(fragment.mediaItem);
const wrapper = document.createElement('div');
wrapper.style.cssText +=
'overflow:hidden;' +
'width:' +
w +
';' +
'height:' +
h +
';' +
'padding:0;' +
'margin:0;' +
'border-radius:0;' +
'border:none;';
fragment.mediaItem.style.cssText +=
'transform:translate(' +
x +
',' +
y +
');' +
'-webkit-transform:translate(' +
x +
',' +
y +
');';
// Evil DOM operations
fragment.mediaItem.parentNode?.insertBefore(wrapper, fragment.mediaItem);
wrapper.appendChild(fragment.mediaItem);
// We need to manually trigger @autoplay, as DOM access seems to kill it
if (fragment.mediaItem.hasAttribute("autoplay")) {
fragment.mediaItem.play();
}
// Media item is an image
} else {
let x: number, y: number, w: number, h: number;
if (fragment.unit === "pixel:") {
x = fragment.x;
y = fragment.y;
w = fragment.w;
h = fragment.h;
} else {
x = (fragment.x / 100) * fragment.mediaItem.naturalWidth;
y = (fragment.y / 100) * fragment.mediaItem.naturalHeight;
w = (fragment.w / 100) * fragment.mediaItem.naturalWidth;
h = (fragment.h / 100) * fragment.mediaItem.naturalHeight;
}
const canvas = document.createElement("canvas");
canvas.width = w;
canvas.height = h;
const context = canvas.getContext("2d");
context.drawImage(fragment.mediaItem, x, y, w, h, 0, 0, w, h);
fragment.mediaItem.src = canvas.toDataURL();
}
// We need to manually trigger @autoplay, as DOM access seems to kill it
if (fragment.mediaItem.hasAttribute('autoplay')) {
fragment.mediaItem.play();
}
// Media item is an image
} else {
let x: number, y: number, w: number, h: number;
if (fragment.unit === 'pixel:') {
x = fragment.x;
y = fragment.y;
w = fragment.w;
h = fragment.h;
} else {
x = (fragment.x / 100) * fragment.mediaItem.naturalWidth;
y = (fragment.y / 100) * fragment.mediaItem.naturalHeight;
w = (fragment.w / 100) * fragment.mediaItem.naturalWidth;
h = (fragment.h / 100) * fragment.mediaItem.naturalHeight;
}
const canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
const context = canvas.getContext('2d');
context?.drawImage(fragment.mediaItem, x, y, w, h, 0, 0, w, h);
fragment.mediaItem.src = canvas.toDataURL();
}
}

View File

@ -1,9 +1,9 @@
import api from '$lib/api';
import { readable, type Readable } from 'svelte/store';
import { readable } from 'svelte/store';
import type { VaultInfo } from '@upnd/upend/types';
export const vaultInfo: Readable<VaultInfo> = readable(undefined, (set) => {
api.fetchInfo().then(async (info) => {
export const vaultInfo = readable(undefined as VaultInfo | undefined, (set) => {
api.fetchInfo().then(async (info: VaultInfo) => {
set(info);
});
});

View File

@ -1,10 +1,11 @@
import api from '$lib/api';
import { i18n } from '../i18n';
import { i18n } from '$lib/i18n';
import { derived, readable, type Readable } from 'svelte/store';
import type { AttributeListingResult } from '@upnd/upend/types';
const databaseAttributeLabels: Readable<{ [key: string]: string }> = readable({}, (set) => {
const result = {};
api.fetchAllAttributes().then((attributes) => {
const result: Record<string, string> = {};
api.fetchAllAttributes().then((attributes: AttributeListingResult) => {
attributes.forEach((attribute) => {
if (attribute.labels.length) {
result[attribute.name] = attribute.labels.sort()[0];

View File

@ -1,102 +1,82 @@
import type { UpEntry } from "@upnd/upend";
import type { UpEntry } from '@upnd/upend';
export type SortKeys = { [key: string]: string[] };
export function sortByValue(entries: UpEntry[], sortKeys: SortKeys): void {
entries.sort((aEntry, bEntry) => {
if (aEntry.value.t === "Number" && bEntry.value.t === "Number") {
return bEntry.value.c - aEntry.value.c;
}
entries.sort((aEntry, bEntry) => {
if (aEntry.value.c === null || bEntry.value.c === null) {
// sort non-null first
return aEntry.value.c === null ? 1 : -1;
}
if (
!sortKeys[aEntry.value.c]?.length ||
!sortKeys[bEntry.value.c]?.length
) {
if (
Boolean(sortKeys[aEntry.value.c]?.length) &&
!sortKeys[bEntry.value.c]?.length
) {
return -1;
} else if (
!sortKeys[aEntry.value.c]?.length &&
Boolean(sortKeys[bEntry.value.c]?.length)
) {
return 1;
} else {
return String(aEntry.value.c).localeCompare(
String(bEntry.value.c),
undefined,
{ numeric: true, sensitivity: "base" },
);
}
} else {
return sortKeys[aEntry.value.c][0].localeCompare(
sortKeys[bEntry.value.c][0],
undefined,
{ numeric: true, sensitivity: "base" },
);
}
});
if (aEntry.value.t === 'Number' && bEntry.value.t === 'Number') {
return bEntry.value.c - aEntry.value.c;
}
if (!sortKeys[aEntry.value.c]?.length || !sortKeys[bEntry.value.c]?.length) {
if (Boolean(sortKeys[aEntry.value.c]?.length) && !sortKeys[bEntry.value.c]?.length) {
return -1;
} else if (!sortKeys[aEntry.value.c]?.length && Boolean(sortKeys[bEntry.value.c]?.length)) {
return 1;
} else {
return String(aEntry.value.c).localeCompare(String(bEntry.value.c), undefined, {
numeric: true,
sensitivity: 'base'
});
}
} else {
return sortKeys[aEntry.value.c][0].localeCompare(sortKeys[bEntry.value.c][0], undefined, {
numeric: true,
sensitivity: 'base'
});
}
});
}
export function sortByValueLength(entries: UpEntry[]) {
entries.sort((aEntry, bEntry) => {
return String(aEntry.value.c).length - String(bEntry.value.c).length;
});
entries.sort((aEntry, bEntry) => {
return String(aEntry.value.c).length - String(bEntry.value.c).length;
});
}
export function sortByAttribute(entries: UpEntry[], sortKeys: SortKeys): void {
entries.sort((aEntry, bEntry) => {
const aResolved = (sortKeys[aEntry.attribute] || [])[0] || aEntry.attribute;
const bResolved = (sortKeys[bEntry.attribute] || [])[0] || bEntry.attribute;
return aResolved.localeCompare(bResolved);
});
entries.sort((aEntry, bEntry) => {
const aResolved = (sortKeys[aEntry.attribute] || [])[0] || aEntry.attribute;
const bResolved = (sortKeys[bEntry.attribute] || [])[0] || bEntry.attribute;
return aResolved.localeCompare(bResolved);
});
}
export function sortByEntity(entries: UpEntry[], sortKeys: SortKeys): void {
entries.sort((aEntry, bEntry) => {
if (!sortKeys[aEntry.entity]?.length || !sortKeys[bEntry.entity]?.length) {
if (
Boolean(sortKeys[aEntry.entity]?.length) &&
!sortKeys[bEntry.entity]?.length
) {
return -1;
} else if (
!sortKeys[aEntry.entity]?.length &&
Boolean(sortKeys[bEntry.entity]?.length)
) {
return 1;
} else {
return aEntry.entity.localeCompare(bEntry.entity);
}
} else {
return sortKeys[aEntry.entity][0].localeCompare(
sortKeys[bEntry.entity][0],
);
}
});
entries.sort((aEntry, bEntry) => {
if (!sortKeys[aEntry.entity]?.length || !sortKeys[bEntry.entity]?.length) {
if (Boolean(sortKeys[aEntry.entity]?.length) && !sortKeys[bEntry.entity]?.length) {
return -1;
} else if (!sortKeys[aEntry.entity]?.length && Boolean(sortKeys[bEntry.entity]?.length)) {
return 1;
} else {
return aEntry.entity.localeCompare(bEntry.entity);
}
} else {
return sortKeys[aEntry.entity][0].localeCompare(sortKeys[bEntry.entity][0]);
}
});
}
export function defaultEntitySort(
entries: UpEntry[],
sortKeys: SortKeys,
): UpEntry[] {
const result = entries.concat();
sortByValue(result, sortKeys);
sortByValueLength(result);
sortByAttribute(result, sortKeys);
sortByEntity(result, sortKeys);
return result;
export function defaultEntitySort(entries: UpEntry[], sortKeys: SortKeys): UpEntry[] {
const result = entries.concat();
sortByValue(result, sortKeys);
sortByValueLength(result);
sortByAttribute(result, sortKeys);
sortByEntity(result, sortKeys);
return result;
}
export function entityValueSort(
entries: UpEntry[],
sortKeys: SortKeys,
): UpEntry[] {
const result = entries.concat();
sortByEntity(result, sortKeys);
sortByAttribute(result, sortKeys);
sortByValueLength(result);
sortByValue(result, sortKeys);
return result;
export function entityValueSort(entries: UpEntry[], sortKeys: SortKeys): UpEntry[] {
const result = entries.concat();
sortByEntity(result, sortKeys);
sortByAttribute(result, sortKeys);
sortByValueLength(result);
sortByValue(result, sortKeys);
return result;
}

View File

@ -0,0 +1,3 @@
export function isDefined<T>(value: T | undefined | null): value is T {
return value !== undefined;
}

View File

@ -25,7 +25,7 @@
$: looksLikeQuery = debouncedQuery.startsWith('(') && debouncedQuery.endsWith(')');
let result: Readable<UpListing> = readable();
let result: Readable<UpListing | undefined> = readable();
let error: Readable<unknown> = readable();
$: if (debouncedQuery.length) {
({ result, error } = looksLikeQuery ? queryFn(debouncedQuery) : baseSearch(debouncedQuery));

View File

@ -1,109 +1,107 @@
<script lang="ts">
import filesize from "filesize";
import UpObject from "../components/display/UpObject.svelte";
import Icon from "../components/utils/Icon.svelte";
import Spinner from "../components/utils/Spinner.svelte";
import api from "../lib/api";
import filesize from 'filesize';
import UpObject from '$lib/components/display/UpObject.svelte';
import Icon from '$lib/components/utils/Icon.svelte';
import Spinner from '$lib/components/utils/Spinner.svelte';
import api from '$lib/api';
const stores = api.fetchStoreInfo();
const stores = api.fetchStoreInfo();
</script>
<div class="store">
<h1>Stores</h1>
{#await stores}
<Spinner />
{:then stores}
{#each Object.entries(stores) as [key, store] (key)}
<h2>{key}</h2>
<div class="totals">
<strong>{store.totals.count}</strong> blobs,
<strong>{filesize(store.totals.size)}</strong>
</div>
<table>
<tr>
<th>Hash</th>
<th>Size</th>
<th>Path</th>
<th>Added</th>
<th>Valid</th>
</tr>
{#each store.blobs as blob}
<tbody>
<tr class:invalid={!blob.paths[0].valid}>
<td class="hash"
><UpObject link address={blob.hash} resolve={false} /></td
>
<td class="size">{filesize(blob.size)}</td>
<td class="path">{blob.paths[0].path}</td>
<td class="added">{blob.paths[0].added}</td>
<td class="valid">
{#if blob.paths[0].valid}
<Icon name="check" />
{:else}
<Icon name="x" />
{/if}
</td>
</tr>
{#each blob.paths.slice(1) as path}
<tr class:invalid={!path.valid}>
<td />
<td />
<td class="path">{path.path}</td>
<td class="added">{path.added}</td>
<td class="valid">
{#if path.valid}
<Icon name="check" />
{:else}
<Icon name="x" />
{/if}
</td>
</tr>
{/each}
</tbody>
{/each}
</table>
{/each}
{:catch error}
<div class="error">
{error}
</div>
{/await}
<h1>Stores</h1>
{#await stores}
<Spinner />
{:then stores}
{#each Object.entries(stores) as [key, store] (key)}
<h2>{key}</h2>
<div class="totals">
<strong>{store.totals.count}</strong> blobs,
<strong>{filesize(store.totals.size)}</strong>
</div>
<table>
<tr>
<th>Hash</th>
<th>Size</th>
<th>Path</th>
<th>Added</th>
<th>Valid</th>
</tr>
{#each store.blobs as blob}
<tbody>
<tr class:invalid={!blob.paths[0].valid}>
<td class="hash"><UpObject link address={blob.hash} resolve={false} /></td>
<td class="size">{filesize(blob.size)}</td>
<td class="path">{blob.paths[0].path}</td>
<td class="added">{blob.paths[0].added}</td>
<td class="valid">
{#if blob.paths[0].valid}
<Icon name="check" />
{:else}
<Icon name="x" />
{/if}
</td>
</tr>
{#each blob.paths.slice(1) as path}
<tr class:invalid={!path.valid}>
<td />
<td />
<td class="path">{path.path}</td>
<td class="added">{path.added}</td>
<td class="valid">
{#if path.valid}
<Icon name="check" />
{:else}
<Icon name="x" />
{/if}
</td>
</tr>
{/each}
</tbody>
{/each}
</table>
{/each}
{:catch error}
<div class="error">
{error}
</div>
{/await}
</div>
<style lang="scss">
.store {
text-align: center;
}
.store {
text-align: center;
}
.totals,
th {
font-size: larger;
}
.totals,
th {
font-size: larger;
}
table {
border-spacing: 1em 0.25em;
margin: 1em;
text-align: initial;
table {
border-spacing: 1em 0.25em;
margin: 1em;
text-align: initial;
.size,
.valid {
text-align: center;
}
.size,
.valid {
text-align: center;
}
.size {
white-space: nowrap;
}
.size {
white-space: nowrap;
}
.invalid {
font-weight: 200;
}
.invalid {
font-weight: 200;
}
tbody:nth-child(odd) {
font-weight: 350;
}
tbody:nth-child(odd) {
font-weight: 350;
}
tbody:nth-child(even) {
font-weight: 450;
}
}
tbody:nth-child(even) {
font-weight: 450;
}
}
</style>

4
webui/src/shims.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module '@recogito/annotorious';
declare module 'three/examples/jsm/controls/OrbitControls';
declare module 'three/examples/jsm/loaders/STLLoader';