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,12 +1,12 @@
<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;
function onDrop(ev: DragEvent) {
if (ev.dataTransfer.files.length > 0) {
addEmitter.emit("files", Array.from(ev.dataTransfer.files));
if (ev.dataTransfer?.files.length) {
addEmitter.emit('files', Array.from(ev.dataTransfer?.files || []));
} // TODO: else check for URLs
dragging = false;
}
@ -16,7 +16,7 @@
}
function onDragOver(ev: DragEvent) {
if (Array.from(ev.dataTransfer.items).some((it) => it.kind === "file")) {
if (Array.from(ev.dataTransfer?.items || []).some((it) => it.kind === 'file')) {
dragging = true;
}
}
@ -26,8 +26,8 @@
}
function onPaste(ev: ClipboardEvent) {
if (ev.clipboardData.files.length > 0) {
addEmitter.emit("files", Array.from(ev.clipboardData.files));
if (ev.clipboardData?.files.length) {
addEmitter.emit('files', Array.from(ev.clipboardData?.files || []));
} // TODO: else check for URLs
}
</script>
@ -37,7 +37,8 @@
on:dragover|preventDefault={onDragOver}
on:dragleave|preventDefault={onDragLeave}
on:drop|preventDefault={onDrop}
on:paste={onPaste} />
on:paste={onPaste}
/>
<div class="dropindicator" class:dragging>
<div class="content">

View File

@ -1,78 +1,77 @@
<script lang="ts" context="module">
import { writable } from "svelte/store";
import { writable } from 'svelte/store';
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;
onMount(() => {
const ctx = canvas.getContext("2d");
const ctx = canvas.getContext('2d');
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
window.addEventListener("resize", resizeCanvas);
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
let selecting = false;
let selectAllArea: DOMRect | undefined = undefined;
let selectAllAddresses: string[] | undefined = undefined;
let selectAllAddresses: string[] = [];
let addressesToRemove = new Set();
document.addEventListener("mousedown", (ev) => {
document.addEventListener('mousedown', (ev) => {
if (!ctx) return;
if (ev.ctrlKey || ev.metaKey) {
ev.preventDefault();
selecting = true;
addressesToRemove = new Set();
const el = document.elementFromPoint(
ev.clientX,
ev.clientY,
) as HTMLElement;
const el = document.elementFromPoint(ev.clientX, ev.clientY) as HTMLElement;
const multiElement = el.closest("[data-address-multi]") as
| HTMLElement
| undefined;
const multiElement = el.closest('[data-address-multi]') as HTMLElement | undefined;
if (multiElement) {
const banner = multiElement.querySelector("h2");
const banner = multiElement.querySelector('h2');
if (banner) {
const rect = banner.getBoundingClientRect();
selectAllArea = rect;
selectAllAddresses = multiElement.dataset.addressMulti.split(",");
selectAllAddresses = multiElement.dataset.addressMulti?.split(',') || [];
ctx.rect(rect.left, rect.top, rect.width, rect.height);
ctx.fillStyle = "#dc322f33";
ctx.fillStyle = '#dc322f33';
ctx.fill();
ctx.fillStyle = "#dc322f77";
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.textAlign = 'center';
ctx.textBaseline = 'middle';
const fix = ctx.measureText('M').actualBoundingBoxDescent / 2;
ctx.fillText(
$i18n.t("Select All"),
$i18n.t('Select All'),
rect.left + rect.width / 2,
rect.top + rect.height / 2 + fix,
rect.top + rect.height / 2 + fix
);
}
}
ctx.strokeStyle = "#dc322f77";
ctx.strokeStyle = '#dc322f77';
ctx.lineWidth = 7;
ctx.beginPath();
ctx.moveTo(ev.clientX, ev.clientY);
}
});
document.addEventListener("mousemove", (ev) => {
document.addEventListener('mousemove', (ev) => {
if (!ctx) return;
if (selecting) {
ev.preventDefault();
@ -88,33 +87,28 @@
...selected,
...selectAllAddresses.filter((a) => {
return !selected.includes(a);
}),
})
];
});
stop();
}
}
const el = document.elementFromPoint(
ev.clientX,
ev.clientY,
) as HTMLElement;
const el = document.elementFromPoint(ev.clientX, ev.clientY) as HTMLElement;
const addressElement = el.closest("[data-address]") as
| HTMLElement
| undefined;
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) {
if (selectMode === 'add' || selectMode === undefined) {
selected.update((selected) => {
if (!selected.includes(address)) {
if (address && !selected.includes(address)) {
return [...selected, address];
} else {
return selected;
}
});
} else if (selectMode === "remove") {
} else if (selectMode === 'remove') {
addressesToRemove.add(address);
}
}
@ -126,15 +120,15 @@
}
});
document.addEventListener("mouseup", () => {
document.addEventListener('mouseup', () => {
stop();
});
function stop() {
selectAllArea = undefined;
selectAllAddresses = undefined;
selectAllAddresses = [];
selecting = false;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx?.clearRect(0, 0, canvas.width, canvas.height);
for (const address of addressesToRemove) {
selected.update((selected) => {
return selected.filter((a) => a !== address);

View File

@ -1,20 +1,18 @@
<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";
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 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;
@ -22,15 +20,15 @@
$: if (adding && selector) selector.focus();
async function add(ev: CustomEvent<SelectorValue>) {
if (ev.detail.t !== "Address") {
if (ev.detail.t !== 'Address') {
return;
}
dispatch("add", ev.detail.c);
dispatch('add', ev.detail.c);
}
async function remove(address: string) {
if (!confirmRemoveMessage || confirm(confirmRemoveMessage)) {
dispatch("remove", address);
dispatch('remove', address);
}
}
</script>
@ -42,12 +40,12 @@
<div class="selector">
<Selector
bind:this={selector}
types={["Address", "NewAddress"]}
types={['Address', 'NewAddress']}
on:input={add}
on:focus={(ev) => {
if (!ev.detail) adding = false;
}}
placeholder={$i18n.t("Choose an entity...")}
placeholder={$i18n.t('Choose an entity...') || ''}
/>
</div>
{/if}
@ -55,10 +53,11 @@
<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)}
on:mouseenter={() => dispatch('highlighted', entity)}
on:mouseleave={() => dispatch('highlighted', undefined)}
>
<UpObjectDisplay address={entity} link />
<IconButton subdued name="x-circle" on:click={() => remove(entity)} />
@ -71,12 +70,7 @@
</div>
{#if !adding}
<div class="add-button">
<IconButton
outline
small
name="folder-plus"
on:click={() => (adding = true)}
/>
<IconButton outline small name="folder-plus" on:click={() => (adding = true)} />
</div>
{/if}
</div>

View File

@ -1,4 +1,5 @@
<script lang="ts" context="module">
import type { ComponentType } from 'svelte';
export interface WidgetComponent {
component: ComponentType;
props: { [key: string]: unknown };
@ -17,13 +18,13 @@
</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";
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[] = [];
@ -40,7 +41,7 @@
function switchWidget(widget: string) {
currentWidget = widget;
dispatch("widgetSwitched", currentWidget);
dispatch('widgetSwitched', currentWidget);
}
let availableWidgets: Widget[] = [];
@ -51,15 +52,15 @@
availableWidgets = [
...availableWidgets,
{
name: "Entry List",
icon: "table",
name: 'Entry List',
icon: 'table',
components: ({ entries }) => [
{
component: EntryList,
props: { entries, columns: "entity, attribute, value" },
},
],
},
props: { entries, columns: 'entity, attribute, value' }
}
]
}
];
}
@ -67,7 +68,7 @@
availableWidgets = [...widgets, ...availableWidgets];
}
if (availableWidgets.map((w) => w.name).includes(initialWidget)) {
if (initialWidget && availableWidgets.map((w) => w.name).includes(initialWidget)) {
currentWidget = initialWidget;
} else {
currentWidget = availableWidgets[0].name;
@ -76,9 +77,10 @@
let components: WidgetComponent[] = [];
$: {
components = availableWidgets
components =
availableWidgets
.find((w) => w.name === currentWidget)
.components({ entries, entities, group, address });
?.components({ entries, entities, group, address }) || [];
}
</script>
@ -98,7 +100,7 @@
<Icon name={icon} />
</div>
{/if}
{title || ""}
{title || ''}
{/if}
</h3>
@ -106,7 +108,7 @@
<div class="views">
{#each availableWidgets as widget (widget.name)}
<IconButton
name={widget.icon || "cube"}
name={widget.icon || 'cube'}
title={widget.name}
active={widget.name === currentWidget}
--active-color="var(--foreground)"
@ -119,11 +121,7 @@
{/if}
</svelte:fragment>
{#each components as component}
<svelte:component
this={component.component}
{...component.props || {}}
on:change
/>
<svelte:component this={component.component} {...component.props || {}} on:change />
{/each}
</LabelBorder>

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,54 +1,55 @@
<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 = [
const selectedWidgets: Widget[] = [
{
name: "List",
icon: "list-check",
name: 'List',
icon: 'list-check',
components: ({ entities }) => [
{
component: EntityList,
props: {
entities,
thumbnails: false,
select: "remove",
},
},
],
select: 'remove'
}
}
]
},
{
name: "EntityList",
icon: "image",
name: 'EntityList',
icon: 'image',
components: ({ entities }) => [
{
component: EntityList,
props: {
entities,
thumbnails: true,
select: "remove",
},
},
],
},
select: 'remove'
}
}
]
}
];
</script>
<div class="view">
<h2>
<Icon plain name="select-multiple" />
{$i18n.t("Selected")}: {$selected.length}
{$i18n.t('Selected')}: {$selected.length}
</h2>
<div class="actions">
<MultiGroupEditor entities={$selected} />
</div>
<div class="entities">
<EntryView
title={$i18n.t("Selected entities")}
title={$i18n.t('Selected entities') || ''}
entities={$selected}
widgets={selectedWidgets}
/>

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]) =>
] 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
$: 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)
.slice(0, $entity?.backlinks.filter((e) => e.attribute === ATTR_IN).length || 0 - 4)
.includes(addr)
)
.slice(0, 4);
.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,5 +1,5 @@
<script lang="ts">
import { onMount } from "svelte";
import { onMount } from 'svelte';
const BADGE_HEIGHT = 3;
export let address: string;
@ -15,7 +15,7 @@
width = Math.ceil(bytes.length / 3 / BADGE_HEIGHT);
onMount(() => {
const ctx = canvas?.getContext("2d");
const ctx = canvas?.getContext('2d');
if (!ctx) {
console.warn("Couldn't initialize canvas!");
return;
@ -26,6 +26,8 @@
let idx = 0;
function draw() {
if (!ctx) return;
const tmp = [];
while (bytes.length > 0 && tmp.length < 3) {
tmp.push(bytes.shift());
@ -34,9 +36,9 @@
tmp.push(tmp[tmp.length - 1]);
}
const h = (tmp[0] / 128) * hueRange + hueCenter - hueRange / 2;
const s = (tmp[1] / 128) * 100;
const l = (tmp[2] / 128) * 100;
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++;
@ -51,6 +53,7 @@
<canvas bind:this={canvas} {width} height="3" title={address} />
<!--suppress CssOverwrittenProperties -->
<style>
canvas {
display: inline-block;

View File

@ -1,15 +1,15 @@
<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;
</script>
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<UpLink passthrough to={{ entity: address }}>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="surface-point"
on:mouseover={() => (popup = true)}
@ -24,7 +24,7 @@
</UpLink>
<style lang="scss">
@use "../../styles/colors.scss";
@use '../../styles/colors.scss';
.surface-point {
display: relative;

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,5 +1,5 @@
<script lang="ts">
import { createEventDispatcher, onMount } from "svelte";
import { createEventDispatcher, onMount } from 'svelte';
const dispatch = createEventDispatcher();
export let src: string;
@ -10,14 +10,11 @@
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);
@ -34,11 +31,11 @@
scene.add(new THREE.HemisphereLight(0xffffff, 1.5));
const loader = new THREE_STL.STLLoader();
loader.load(src, (geometry) => {
loader.load(src, (geometry: any) => {
const material = new THREE.MeshPhongMaterial({
color: 0xdc322f,
specular: 100,
shininess: 70,
shininess: 70
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
@ -47,16 +44,14 @@
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),
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,
geometry.boundingBox.max.z
);
camera.position.z = largestDimension * 2;
});
@ -68,7 +63,7 @@
}
animate();
dispatch("loaded");
dispatch('loaded');
});
</script>

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,17 +1,14 @@
<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();
export let value: IValue | undefined = undefined;
export let types: SELECTOR_TYPE[] | undefined = undefined;
let newValue: SelectorValue = value;
let newValue: SelectorValue | undefined = value;
let editing = false;
@ -28,6 +25,7 @@
}
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="editable"
class:editing
@ -39,7 +37,7 @@
<div
class="selector"
on:keydown={(ev) => {
if (ev.key === "Escape") {
if (ev.key === 'Escape') {
editing = false;
}
}}
@ -54,7 +52,7 @@
<IconButton
name="save"
on:click={() => {
dispatch("edit", newValue);
dispatch('edit', newValue);
editing = false;
}}
/>

View File

@ -1,19 +1,19 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { createEventDispatcher } from 'svelte';
let input: HTMLInputElement;
const dispatch = createEventDispatcher();
export let placeholder = "";
export let value = "";
export let placeholder = '';
export let value = '';
export let disabled = false;
export let size: number | undefined = 7;
let focused = false;
$: dispatch("focusChange", focused);
$: dispatch('focusChange', focused);
function onInput() {
dispatch("input", value);
dispatch('input', value);
}
export function focus() {
@ -30,7 +30,7 @@
on:input={onInput}
on:focus={() => (focused = true)}
on:blur={() => (focused = false)}
size={Math.max(value.length, size)}
size={Math.max(value.length, size || 0)}
on:keydown
{disabled}
/>

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,18 +1,16 @@
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,
) {
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);
dbg('Fetching: %s', key);
try {
const response = await fetch(key, options);
if (response.ok) {
@ -26,7 +24,7 @@ export function useSWR<D = unknown, E = Error>(
throw new Error(errorText);
}
} catch (err) {
error.set(err);
error.set(err as any);
}
}
@ -35,6 +33,6 @@ export function useSWR<D = unknown, E = Error>(
return {
data,
error,
revalidate: doFetch,
revalidate: doFetch
};
}

View File

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

View File

@ -4,11 +4,11 @@
export type MediaFragment = (
| {
mediaItem: HTMLImageElement;
mediaType: "img";
mediaType: 'img';
}
| {
mediaItem: HTMLVideoElement;
mediaType: "video";
mediaType: 'video';
}
) & {
unit: string;
@ -21,20 +21,19 @@ export type MediaFragment = (
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 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:",
unit: match[1] ? match[1] : 'pixel:',
x: parseFloat(match[2]),
y: parseFloat(match[3]),
w: parseFloat(match[4]),
h: parseFloat(match[5]),
h: parseFloat(match[5])
} as MediaFragment;
if (mediaFragment.mediaType === "img") {
if (mediaFragment.mediaType === 'img') {
addImageLoadListener(mediaFragment);
} else {
addVideoLoadListener(mediaFragment);
@ -57,7 +56,7 @@ function addImageLoadListener(mediaFragment: MediaFragment) {
lastSrc = mediaItem.src;
}
}
mediaItem.addEventListener("load", onload);
mediaItem.addEventListener('load', onload);
}
/**
@ -65,7 +64,7 @@ function addImageLoadListener(mediaFragment: MediaFragment) {
* need the video's original width and height.
*/
function addVideoLoadListener(mediaFragment: MediaFragment) {
mediaFragment.mediaItem.addEventListener("loadedmetadata", function () {
mediaFragment.mediaItem.addEventListener('loadedmetadata', function () {
applyFragment(mediaFragment);
});
}
@ -83,61 +82,61 @@ function addVideoLoadListener(mediaFragment: MediaFragment) {
*/
function applyFragment(fragment: MediaFragment) {
// Media item is a video
if (fragment.mediaType === "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:") {
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";
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";
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");
const wrapper = document.createElement('div');
wrapper.style.cssText +=
"overflow:hidden;" +
"width:" +
'overflow:hidden;' +
'width:' +
w +
";" +
"height:" +
';' +
'height:' +
h +
";" +
"padding:0;" +
"margin:0;" +
"border-radius:0;" +
"border:none;";
';' +
'padding:0;' +
'margin:0;' +
'border-radius:0;' +
'border:none;';
fragment.mediaItem.style.cssText +=
"transform:translate(" +
'transform:translate(' +
x +
"," +
',' +
y +
");" +
"-webkit-transform:translate(" +
');' +
'-webkit-transform:translate(' +
x +
"," +
',' +
y +
");";
');';
// Evil DOM operations
fragment.mediaItem.parentNode.insertBefore(wrapper, fragment.mediaItem);
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")) {
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:") {
if (fragment.unit === 'pixel:') {
x = fragment.x;
y = fragment.y;
w = fragment.w;
@ -148,11 +147,11 @@ function applyFragment(fragment: MediaFragment) {
w = (fragment.w / 100) * fragment.mediaItem.naturalWidth;
h = (fragment.h / 100) * fragment.mediaItem.naturalHeight;
}
const canvas = document.createElement("canvas");
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);
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,40 +1,34 @@
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") {
if (aEntry.value.c === null || bEntry.value.c === null) {
// sort non-null first
return aEntry.value.c === null ? 1 : -1;
}
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
) {
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)
) {
} 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" },
);
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" },
);
return sortKeys[aEntry.value.c][0].localeCompare(sortKeys[bEntry.value.c][0], undefined, {
numeric: true,
sensitivity: 'base'
});
}
});
}
@ -56,31 +50,20 @@ export function sortByAttribute(entries: UpEntry[], sortKeys: SortKeys): void {
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
) {
if (Boolean(sortKeys[aEntry.entity]?.length) && !sortKeys[bEntry.entity]?.length) {
return -1;
} else if (
!sortKeys[aEntry.entity]?.length &&
Boolean(sortKeys[bEntry.entity]?.length)
) {
} 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],
);
return sortKeys[aEntry.entity][0].localeCompare(sortKeys[bEntry.entity][0]);
}
});
}
export function defaultEntitySort(
entries: UpEntry[],
sortKeys: SortKeys,
): UpEntry[] {
export function defaultEntitySort(entries: UpEntry[], sortKeys: SortKeys): UpEntry[] {
const result = entries.concat();
sortByValue(result, sortKeys);
sortByValueLength(result);
@ -89,10 +72,7 @@ export function defaultEntitySort(
return result;
}
export function entityValueSort(
entries: UpEntry[],
sortKeys: SortKeys,
): UpEntry[] {
export function entityValueSort(entries: UpEntry[], sortKeys: SortKeys): UpEntry[] {
const result = entries.concat();
sortByEntity(result, sortKeys);
sortByAttribute(result, sortKeys);

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,9 +1,9 @@
<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();
</script>
@ -30,9 +30,7 @@
{#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="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>

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';