@e280/quay 0.0.0-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/README.md +117 -0
- package/package.json +57 -0
- package/s/cellar/cask.ts +18 -0
- package/s/cellar/cellar.test.ts +100 -0
- package/s/cellar/cellar.ts +53 -0
- package/s/cellar/forklift.ts +9 -0
- package/s/cellar/index.ts +7 -0
- package/s/cellar/memory-forklift.ts +29 -0
- package/s/cellar/opfs-forklift.ts +49 -0
- package/s/demo/main.bundle.ts +39 -0
- package/s/demo/main.css +126 -0
- package/s/dom/components/breadcrumb/component.ts +29 -0
- package/s/dom/components/browser/component.ts +75 -0
- package/s/dom/components/browser/style.css.ts +118 -0
- package/s/dom/components/dropzone/component.ts +33 -0
- package/s/dom/components/dropzone/style.css.ts +54 -0
- package/s/dom/components/filter/component.ts +44 -0
- package/s/dom/components/outliner/component.ts +16 -0
- package/s/dom/components/searchbar/component.ts +29 -0
- package/s/dom/components/sort/component.ts +48 -0
- package/s/dom/components.ts +15 -0
- package/s/dom/context.ts +5 -0
- package/s/dom/theme.css.ts +11 -0
- package/s/dom/utils/find-group-name.ts +12 -0
- package/s/dom/utils/find-local-group.ts +9 -0
- package/s/dom/views/tree/component.ts +64 -0
- package/s/dom/views/tree/style.css.ts +64 -0
- package/s/genius/folio.md +29 -0
- package/s/genius/parts/hy-tree.ts +142 -0
- package/s/genius/quay.md +13 -0
- package/s/genius/turbo.md +21 -0
- package/s/genius/twig.ts +34 -0
- package/s/index.html.ts +89 -0
- package/s/index.ts +14 -0
- package/s/logic/aspects/codex/codex.ts +46 -0
- package/s/logic/aspects/codex/example.ts +65 -0
- package/s/logic/aspects/codex/parts/clade.ts +28 -0
- package/s/logic/aspects/codex/parts/codex-item.ts +94 -0
- package/s/logic/aspects/codex/parts/hierarchy.ts +82 -0
- package/s/logic/aspects/codex/parts/taxonomy.ts +18 -0
- package/s/logic/aspects/codex/parts/types.ts +23 -0
- package/s/logic/aspects/codex/utils/generate-id.ts +7 -0
- package/s/logic/aspects/dropzone.ts +77 -0
- package/s/logic/aspects/tree-trail.ts +33 -0
- package/s/logic/brain.ts +22 -0
- package/s/logic/group.ts +104 -0
- package/s/logic/permissions.ts +34 -0
- package/s/logic/presets/media/group.ts +94 -0
- package/s/logic/presets/media/schema.ts +22 -0
- package/s/logic/presets/opfs/schema.ts +21 -0
- package/s/logic/presets/plain/group.ts +44 -0
- package/s/logic/presets/plain/schema.ts +15 -0
- package/s/logic/types.ts +33 -0
- package/s/tests.test.ts +8 -0
- package/x/cellar/cask.d.ts +8 -0
- package/x/cellar/cask.js +19 -0
- package/x/cellar/cask.js.map +1 -0
- package/x/cellar/cellar.d.ts +22 -0
- package/x/cellar/cellar.js +47 -0
- package/x/cellar/cellar.js.map +1 -0
- package/x/cellar/cellar.test.d.ts +11 -0
- package/x/cellar/cellar.test.js +79 -0
- package/x/cellar/cellar.test.js.map +1 -0
- package/x/cellar/forklift.d.ts +7 -0
- package/x/cellar/forklift.js +2 -0
- package/x/cellar/forklift.js.map +1 -0
- package/x/cellar/index.d.ts +5 -0
- package/x/cellar/index.js +6 -0
- package/x/cellar/index.js.map +1 -0
- package/x/cellar/memory-forklift.d.ts +9 -0
- package/x/cellar/memory-forklift.js +21 -0
- package/x/cellar/memory-forklift.js.map +1 -0
- package/x/cellar/opfs-forklift.d.ts +11 -0
- package/x/cellar/opfs-forklift.js +44 -0
- package/x/cellar/opfs-forklift.js.map +1 -0
- package/x/demo/main.bundle.d.ts +1 -0
- package/x/demo/main.bundle.js +30 -0
- package/x/demo/main.bundle.js.map +1 -0
- package/x/demo/main.bundle.min.js +471 -0
- package/x/demo/main.css +126 -0
- package/x/dom/components/breadcrumb/component.d.ts +1 -0
- package/x/dom/components/breadcrumb/component.js +21 -0
- package/x/dom/components/breadcrumb/component.js.map +1 -0
- package/x/dom/components/browser/component.d.ts +1 -0
- package/x/dom/components/browser/component.js +65 -0
- package/x/dom/components/browser/component.js.map +1 -0
- package/x/dom/components/browser/style.css.d.ts +2 -0
- package/x/dom/components/browser/style.css.js +117 -0
- package/x/dom/components/browser/style.css.js.map +1 -0
- package/x/dom/components/dropzone/component.d.ts +1 -0
- package/x/dom/components/dropzone/component.js +27 -0
- package/x/dom/components/dropzone/component.js.map +1 -0
- package/x/dom/components/dropzone/style.css.d.ts +2 -0
- package/x/dom/components/dropzone/style.css.js +53 -0
- package/x/dom/components/dropzone/style.css.js.map +1 -0
- package/x/dom/components/filter/component.d.ts +1 -0
- package/x/dom/components/filter/component.js +40 -0
- package/x/dom/components/filter/component.js.map +1 -0
- package/x/dom/components/outliner/component.d.ts +1 -0
- package/x/dom/components/outliner/component.js +13 -0
- package/x/dom/components/outliner/component.js.map +1 -0
- package/x/dom/components/searchbar/component.d.ts +1 -0
- package/x/dom/components/searchbar/component.js +24 -0
- package/x/dom/components/searchbar/component.js.map +1 -0
- package/x/dom/components/sort/component.d.ts +1 -0
- package/x/dom/components/sort/component.js +44 -0
- package/x/dom/components/sort/component.js.map +1 -0
- package/x/dom/components.d.ts +18 -0
- package/x/dom/components.js +13 -0
- package/x/dom/components.js.map +1 -0
- package/x/dom/context.d.ts +2 -0
- package/x/dom/context.js +3 -0
- package/x/dom/context.js.map +1 -0
- package/x/dom/theme.css.d.ts +2 -0
- package/x/dom/theme.css.js +11 -0
- package/x/dom/theme.css.js.map +1 -0
- package/x/dom/utils/find-group-name.d.ts +1 -0
- package/x/dom/utils/find-group-name.js +9 -0
- package/x/dom/utils/find-group-name.js.map +1 -0
- package/x/dom/utils/find-local-group.d.ts +1 -0
- package/x/dom/utils/find-local-group.js +7 -0
- package/x/dom/utils/find-local-group.js.map +1 -0
- package/x/dom/views/tree/component.d.ts +7 -0
- package/x/dom/views/tree/component.js +52 -0
- package/x/dom/views/tree/component.js.map +1 -0
- package/x/dom/views/tree/style.css.d.ts +2 -0
- package/x/dom/views/tree/style.css.js +63 -0
- package/x/dom/views/tree/style.css.js.map +1 -0
- package/x/genius/folio.md +29 -0
- package/x/genius/parts/hy-tree.d.ts +44 -0
- package/x/genius/parts/hy-tree.js +116 -0
- package/x/genius/parts/hy-tree.js.map +1 -0
- package/x/genius/quay.md +13 -0
- package/x/genius/turbo.md +21 -0
- package/x/genius/twig.d.ts +34 -0
- package/x/genius/twig.js +2 -0
- package/x/genius/twig.js.map +1 -0
- package/x/importmap.json +47 -0
- package/x/index.d.ts +11 -0
- package/x/index.html +170 -0
- package/x/index.html.d.ts +3 -0
- package/x/index.html.js +87 -0
- package/x/index.html.js.map +1 -0
- package/x/index.js +9 -0
- package/x/index.js.map +1 -0
- package/x/logic/aspects/codex/codex.d.ts +17 -0
- package/x/logic/aspects/codex/codex.js +39 -0
- package/x/logic/aspects/codex/codex.js.map +1 -0
- package/x/logic/aspects/codex/example.d.ts +13 -0
- package/x/logic/aspects/codex/example.js +42 -0
- package/x/logic/aspects/codex/example.js.map +1 -0
- package/x/logic/aspects/codex/parts/clade.d.ts +16 -0
- package/x/logic/aspects/codex/parts/clade.js +23 -0
- package/x/logic/aspects/codex/parts/clade.js.map +1 -0
- package/x/logic/aspects/codex/parts/codex-item.d.ts +33 -0
- package/x/logic/aspects/codex/parts/codex-item.js +74 -0
- package/x/logic/aspects/codex/parts/codex-item.js.map +1 -0
- package/x/logic/aspects/codex/parts/hierarchy.d.ts +23 -0
- package/x/logic/aspects/codex/parts/hierarchy.js +69 -0
- package/x/logic/aspects/codex/parts/hierarchy.js.map +1 -0
- package/x/logic/aspects/codex/parts/taxonomy.d.ts +7 -0
- package/x/logic/aspects/codex/parts/taxonomy.js +13 -0
- package/x/logic/aspects/codex/parts/taxonomy.js.map +1 -0
- package/x/logic/aspects/codex/parts/types.d.ts +22 -0
- package/x/logic/aspects/codex/parts/types.js +2 -0
- package/x/logic/aspects/codex/parts/types.js.map +1 -0
- package/x/logic/aspects/codex/utils/generate-id.d.ts +1 -0
- package/x/logic/aspects/codex/utils/generate-id.js +5 -0
- package/x/logic/aspects/codex/utils/generate-id.js.map +1 -0
- package/x/logic/aspects/dropzone.d.ts +16 -0
- package/x/logic/aspects/dropzone.js +60 -0
- package/x/logic/aspects/dropzone.js.map +1 -0
- package/x/logic/aspects/tree-trail.d.ts +8 -0
- package/x/logic/aspects/tree-trail.js +24 -0
- package/x/logic/aspects/tree-trail.js.map +1 -0
- package/x/logic/brain.d.ts +11 -0
- package/x/logic/brain.js +17 -0
- package/x/logic/brain.js.map +1 -0
- package/x/logic/group.d.ts +51 -0
- package/x/logic/group.js +79 -0
- package/x/logic/group.js.map +1 -0
- package/x/logic/permissions.d.ts +18 -0
- package/x/logic/permissions.js +26 -0
- package/x/logic/permissions.js.map +1 -0
- package/x/logic/presets/media/group.d.ts +7 -0
- package/x/logic/presets/media/group.js +75 -0
- package/x/logic/presets/media/group.js.map +1 -0
- package/x/logic/presets/media/schema.d.ts +18 -0
- package/x/logic/presets/media/schema.js +2 -0
- package/x/logic/presets/media/schema.js.map +1 -0
- package/x/logic/presets/opfs/schema.d.ts +18 -0
- package/x/logic/presets/opfs/schema.js +2 -0
- package/x/logic/presets/opfs/schema.js.map +1 -0
- package/x/logic/presets/plain/group.d.ts +7 -0
- package/x/logic/presets/plain/group.js +33 -0
- package/x/logic/presets/plain/group.js.map +1 -0
- package/x/logic/presets/plain/schema.d.ts +12 -0
- package/x/logic/presets/plain/schema.js +2 -0
- package/x/logic/presets/plain/schema.js.map +1 -0
- package/x/logic/types.d.ts +20 -0
- package/x/logic/types.js +11 -0
- package/x/logic/types.js.map +1 -0
- package/x/tests.test.d.ts +1 -0
- package/x/tests.test.js +6 -0
- package/x/tests.test.js.map +1 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import {ShockDragDrop, signal} from "@benev/slate"
|
|
2
|
+
|
|
3
|
+
import {Group} from "../group.js"
|
|
4
|
+
import {CodexItem} from "./codex/parts/codex-item.js"
|
|
5
|
+
|
|
6
|
+
export class Dropzone {
|
|
7
|
+
#drag_drop = new ShockDragDrop<CodexItem, CodexItem>({
|
|
8
|
+
handle_drop: (_e, grabbed, hovering) => this.group.move(grabbed, hovering)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
constructor(private group: Group) {}
|
|
12
|
+
|
|
13
|
+
// to make drop file to import ui work
|
|
14
|
+
#hovering = signal<CodexItem | undefined>(undefined)
|
|
15
|
+
|
|
16
|
+
get grabbed() {
|
|
17
|
+
return this.#drag_drop.grabbed
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get hovering() {
|
|
21
|
+
return this.#drag_drop.hovering ?? this.#hovering.value
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
dragenter = (e: DragEvent, target?: CodexItem) => {
|
|
25
|
+
e.preventDefault()
|
|
26
|
+
this.#drag_drop.dropzone.dragenter()(e)
|
|
27
|
+
this.#hovering.value = target
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
dragleave = (e: DragEvent) => {
|
|
31
|
+
this.#drag_drop.dropzone.dragleave()(e)
|
|
32
|
+
this.#hovering.value = undefined
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
dragstart = (e: DragEvent, n: CodexItem) => {
|
|
36
|
+
e.stopPropagation()
|
|
37
|
+
this.#drag_drop.dragzone.dragstart(n)(e)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
dragover = (e: DragEvent, n: CodexItem) => {
|
|
41
|
+
e.preventDefault()
|
|
42
|
+
this.#drag_drop.dropzone.dragover(n)(e)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
dragend = (e: DragEvent) => {
|
|
46
|
+
this.#drag_drop.dragzone.dragend()(e)
|
|
47
|
+
this.#hovering.value = undefined
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
drop = (e: DragEvent, target: CodexItem) => {
|
|
51
|
+
e.preventDefault()
|
|
52
|
+
const files = Array.from(e.dataTransfer?.files || [])
|
|
53
|
+
|
|
54
|
+
if (files.length) {
|
|
55
|
+
this.group.upload(files, target)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const same = this.grabbed?.id === this.hovering?.id
|
|
59
|
+
const child = this.grabbed?.children.some(c => this.hovering?.id === c.id)
|
|
60
|
+
const parent = this.grabbed?.parent?.id === this.hovering?.id
|
|
61
|
+
|
|
62
|
+
if(!same && !child && !parent)
|
|
63
|
+
this.#drag_drop.dropzone.drop(target)(e)
|
|
64
|
+
|
|
65
|
+
this.#hovering.value = undefined
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
change = (e: DragEvent, targetFolder: CodexItem) => {
|
|
69
|
+
const input = e.currentTarget as HTMLInputElement
|
|
70
|
+
const files = Array.from(input.files ?? [])
|
|
71
|
+
|
|
72
|
+
if (files.length) {
|
|
73
|
+
this.group.upload(files, targetFolder)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {signal} from "@benev/slate"
|
|
2
|
+
import {Schema} from "./codex/parts/types.js"
|
|
3
|
+
import {CodexItem} from "./codex/parts/codex-item.js"
|
|
4
|
+
|
|
5
|
+
export class TreeTrail<Sc extends Schema> {
|
|
6
|
+
readonly signal = signal<CodexItem<Sc>[]>([])
|
|
7
|
+
|
|
8
|
+
constructor(root: CodexItem<Sc>) {
|
|
9
|
+
this.signal.value = [root]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
setTrail(e: Event, item: CodexItem<Sc>) {
|
|
13
|
+
if (e.target !== e.currentTarget) return
|
|
14
|
+
|
|
15
|
+
const trail: CodexItem<Sc>[] = []
|
|
16
|
+
|
|
17
|
+
let cur = item.kind === "folder"
|
|
18
|
+
? item
|
|
19
|
+
: item.parent
|
|
20
|
+
|
|
21
|
+
while (cur) {
|
|
22
|
+
trail.unshift(cur)
|
|
23
|
+
cur = cur.parent
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
this.signal.value = trail
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get currentFolder() {
|
|
30
|
+
return this.signal.value.at(-1)?.children ?? []
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
package/s/logic/brain.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
|
|
2
|
+
import {MapG} from "@e280/stz"
|
|
3
|
+
import {Group} from "./group.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Quay's global state
|
|
7
|
+
* - manages groups
|
|
8
|
+
* - each group is a different hierarchy, which can have unique types and stuff
|
|
9
|
+
*/
|
|
10
|
+
export class Brain {
|
|
11
|
+
#groups = new MapG<string, Group>()
|
|
12
|
+
|
|
13
|
+
setGroup<G extends Group>(name: string, group: G) {
|
|
14
|
+
this.#groups.set(name, group)
|
|
15
|
+
return group
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
getGroup<G extends Group>(name: string) {
|
|
19
|
+
return this.#groups.require(name) as G
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
package/s/logic/group.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import {sub} from "@e280/stz"
|
|
2
|
+
import {signal} from "@benev/slate"
|
|
3
|
+
|
|
4
|
+
import {Permission} from "./permissions.js"
|
|
5
|
+
import {Dropzone} from "./aspects/dropzone.js"
|
|
6
|
+
import {TreeTrail} from "./aspects/tree-trail.js"
|
|
7
|
+
import {Schema} from "./aspects/codex/parts/types.js"
|
|
8
|
+
import {GroupConfig, SearchFn, SortFn} from "./types.js"
|
|
9
|
+
import {CodexItem} from "./aspects/codex/parts/codex-item.js"
|
|
10
|
+
|
|
11
|
+
export class Group<Sc extends Schema = any> {
|
|
12
|
+
trail: TreeTrail<Sc>
|
|
13
|
+
dropzone = new Dropzone(this)
|
|
14
|
+
searchText = signal("")
|
|
15
|
+
selectedFilter = signal("")
|
|
16
|
+
selectedSort = signal("")
|
|
17
|
+
|
|
18
|
+
on = {
|
|
19
|
+
newFolder: sub<[{parent: CodexItem<Sc>}]>(),
|
|
20
|
+
move: sub<[{item: CodexItem<Sc>, target: CodexItem<Sc>}]>(),
|
|
21
|
+
delete: sub<[{item: CodexItem<Sc>}]>(),
|
|
22
|
+
rename: sub<[{item: CodexItem<Sc>, newName: string}]>(),
|
|
23
|
+
upload: sub<[{files: File[], target: CodexItem<Sc>}]>(),
|
|
24
|
+
search: sub<[{terms: string[]}]>(),
|
|
25
|
+
refresh: sub<[{}]>(),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
constructor(public config: GroupConfig<Sc>) {
|
|
29
|
+
this.selectedFilter.value = config.defaultFilter
|
|
30
|
+
this.selectedSort.value = config.defaultSort
|
|
31
|
+
this.trail = new TreeTrail(config.root)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get permissions() {
|
|
35
|
+
return this.config.permissions as (item: CodexItem) => Permission
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getFilterFn(filterKey: string): SearchFn<Sc> {
|
|
39
|
+
return this.config.filters.get(filterKey) ?? (() => true)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
getSearchFn(searchText: string): SearchFn<Sc> {
|
|
43
|
+
const terms = searchText.trim().toLowerCase().split(/\s+/)
|
|
44
|
+
return item => this.config.search(terms, item)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
getSortFn(sortKey: string): SortFn<Sc> {
|
|
48
|
+
return this.config.sorts?.get(sortKey) ?? (() => 0)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
sort(items: CodexItem<Sc>[]): CodexItem<Sc>[] {
|
|
52
|
+
const sortFn = this.getSortFn(this.selectedSort.value)
|
|
53
|
+
return [...items].sort(sortFn)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
matches(item: CodexItem<Sc>) {
|
|
57
|
+
const filterFn = this.getFilterFn(this.selectedFilter.value)
|
|
58
|
+
const searchFn = this.getSearchFn(this.searchText.value)
|
|
59
|
+
return filterFn(item) && searchFn(item)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
move(item: CodexItem<Sc>, target: CodexItem<Sc>) {
|
|
63
|
+
if(!this.permissions(item).move)
|
|
64
|
+
throw new Error("move permission not granted")
|
|
65
|
+
|
|
66
|
+
item.detach()
|
|
67
|
+
target.attach(item)
|
|
68
|
+
|
|
69
|
+
this.on.move.pub({item, target})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
delete(item: CodexItem<Sc>) {
|
|
73
|
+
if(!this.permissions(item).delete)
|
|
74
|
+
throw new Error("delete permission not granted")
|
|
75
|
+
|
|
76
|
+
item.destroy()
|
|
77
|
+
|
|
78
|
+
this.on.delete.pub({item})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
rename(item: CodexItem<Sc>, newName: string) {
|
|
82
|
+
if(!this.permissions(item).rename)
|
|
83
|
+
throw new Error("rename permission not granted")
|
|
84
|
+
|
|
85
|
+
this.on.rename.pub({item, newName})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
upload(files: File[], folder: CodexItem<Sc>) {
|
|
89
|
+
if(!this.permissions(folder).upload)
|
|
90
|
+
throw new Error("upload permission not granted")
|
|
91
|
+
|
|
92
|
+
this.on.upload.pub({files, target: folder})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
addFolder(parent: CodexItem<Sc>) {
|
|
96
|
+
if(!this.permissions(parent).newFolder)
|
|
97
|
+
throw new Error("add folder permission not granted")
|
|
98
|
+
|
|
99
|
+
this.on.newFolder.pub({parent})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
components = () => this.components
|
|
103
|
+
}
|
|
104
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
enum PermissionKey {
|
|
2
|
+
rename = "rename",
|
|
3
|
+
move = "move",
|
|
4
|
+
delete = "delete",
|
|
5
|
+
refresh = "refresh",
|
|
6
|
+
newFolder = "newFolder",
|
|
7
|
+
search = "search",
|
|
8
|
+
upload = "upload"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type Permission = {
|
|
12
|
+
[K in PermissionKey]: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const ALL_KEYS = Object.values(PermissionKey) as PermissionKey[]
|
|
16
|
+
|
|
17
|
+
function makePermissions(
|
|
18
|
+
defaultValue: boolean,
|
|
19
|
+
overrides: Partial<Permission> = {}
|
|
20
|
+
): Permission {
|
|
21
|
+
const perms = {} as Permission
|
|
22
|
+
for (const key of ALL_KEYS)
|
|
23
|
+
perms[key] = key in overrides ? overrides[key]! : defaultValue
|
|
24
|
+
return perms
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const Permissions = {
|
|
28
|
+
all: makePermissions(true),
|
|
29
|
+
readOnly: makePermissions(false, {
|
|
30
|
+
[PermissionKey.refresh]: true,
|
|
31
|
+
[PermissionKey.search]: true,
|
|
32
|
+
}),
|
|
33
|
+
none: makePermissions(false),
|
|
34
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
|
|
2
|
+
import {MapG} from "@e280/stz"
|
|
3
|
+
import {Content, html} from "@benev/slate"
|
|
4
|
+
|
|
5
|
+
import Quay from "../../../index.js"
|
|
6
|
+
import {Group} from "../../group.js"
|
|
7
|
+
import {Codex} from "../../aspects/codex/codex.js"
|
|
8
|
+
import {MediaFormat, MediaSchema} from "./schema.js"
|
|
9
|
+
import {GroupConfig, SearchFn, SortFn} from "../../types.js"
|
|
10
|
+
import {CodexItem} from "../../aspects/codex/parts/codex-item.js"
|
|
11
|
+
|
|
12
|
+
export class MediaGroup extends Group<MediaSchema> implements Group {
|
|
13
|
+
static config = (): GroupConfig<MediaSchema> => {
|
|
14
|
+
const formatIcons = new MapG<MediaFormat, Content>()
|
|
15
|
+
.set("audio", html`<sl-icon name=music-note-beamed></sl-icon>`)
|
|
16
|
+
.set("video", html`<sl-icon name=film></sl-icon>`)
|
|
17
|
+
.set("image", html`<sl-icon name=image></sl-icon>`)
|
|
18
|
+
.set("other", html`<sl-icon name=file-earmark></sl-icon>`)
|
|
19
|
+
|
|
20
|
+
const filters = new Map<string, SearchFn<MediaSchema>>()
|
|
21
|
+
.set("all", () => true)
|
|
22
|
+
.set("video", item => item.isKind("folder") || (item.isKind("file") && item.specimen.format === "video"))
|
|
23
|
+
.set("audio", item => item.isKind("folder") || (item.isKind("file") && item.specimen.format === "audio"))
|
|
24
|
+
|
|
25
|
+
const sorts = new Map<string, SortFn<MediaSchema>>()
|
|
26
|
+
.set("label", (a, b) => a.specimen.label.localeCompare(b.specimen.label))
|
|
27
|
+
.set("format", (a, b) => {
|
|
28
|
+
if (a.isKind("file") && b.isKind("file"))
|
|
29
|
+
return a.specimen.format.localeCompare(b.specimen.format)
|
|
30
|
+
|
|
31
|
+
if (a.isKind("file")) return -1
|
|
32
|
+
if (b.isKind("file")) return 1
|
|
33
|
+
|
|
34
|
+
return 0
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const search: (terms: string[], item: CodexItem<MediaSchema>) => boolean = (terms, item) => {
|
|
38
|
+
const query = terms.join(" ").trim().toLowerCase()
|
|
39
|
+
if (query === "") return true
|
|
40
|
+
|
|
41
|
+
if (item.kind === "folder") {
|
|
42
|
+
return item.children.some(child => search(terms, child))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return item.specimen.label.toLowerCase().includes(query)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const codex = Codex.setup<MediaSchema>({
|
|
49
|
+
folder: {icon: html`<sl-icon name="folder"></sl-icon>`},
|
|
50
|
+
file: {icon: html`<sl-icon name="file"></sl-icon>`},
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const root = codex.root(
|
|
54
|
+
codex.create("folder", {label: "project"})
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
const renderIcon = (item: CodexItem<MediaSchema>, opened: boolean) => {
|
|
58
|
+
return item.isKind("file")
|
|
59
|
+
? formatIcons.require(item.specimen.format)
|
|
60
|
+
: opened ? html`<sl-icon name="folder2-open"></sl-icon>` : item.taxon.icon
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
codex,
|
|
65
|
+
root,
|
|
66
|
+
sorts,
|
|
67
|
+
filters: filters,
|
|
68
|
+
defaultFilter: "all",
|
|
69
|
+
defaultSort: "label",
|
|
70
|
+
search: (terms, item) => {
|
|
71
|
+
return terms.some(term => (
|
|
72
|
+
item.kind.includes(term) ||
|
|
73
|
+
item.id.includes(term) ||
|
|
74
|
+
item.specimen.label.includes(term)
|
|
75
|
+
)) || search(terms, item)
|
|
76
|
+
},
|
|
77
|
+
renderIcon,
|
|
78
|
+
renderLabel: item => item.specimen.label,
|
|
79
|
+
renderPreview: (item: CodexItem<MediaSchema>) => {
|
|
80
|
+
return (item.isKind("file") && item.specimen.previewUrl)
|
|
81
|
+
? html`<img src=${item.specimen.previewUrl}>`
|
|
82
|
+
: renderIcon(item, false)
|
|
83
|
+
// // TODO icons
|
|
84
|
+
// : html`<sl-icon name=${item.taxon.icon}></sl-icon>`
|
|
85
|
+
},
|
|
86
|
+
permissions: item => Quay.permissions.all,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
constructor() {
|
|
91
|
+
super(MediaGroup.config())
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
|
|
2
|
+
import {Content} from "@benev/slate"
|
|
3
|
+
import {AsSchema} from "../../aspects/codex/parts/types.js"
|
|
4
|
+
|
|
5
|
+
export type MediaFormat = "video" | "image" | "audio" | "other"
|
|
6
|
+
|
|
7
|
+
export type MediaSchema = AsSchema<{
|
|
8
|
+
taxon: {
|
|
9
|
+
icon: Content
|
|
10
|
+
}
|
|
11
|
+
specimens: {
|
|
12
|
+
folder: {
|
|
13
|
+
label: string
|
|
14
|
+
}
|
|
15
|
+
file: {
|
|
16
|
+
label: string
|
|
17
|
+
format: MediaFormat
|
|
18
|
+
previewUrl: string | null
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}>
|
|
22
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
|
|
2
|
+
import {Content} from "@benev/slate"
|
|
3
|
+
import {AsSchema} from "../../aspects/codex/parts/types.js"
|
|
4
|
+
|
|
5
|
+
export type OpfsSchema = AsSchema<{
|
|
6
|
+
taxon: {
|
|
7
|
+
icon: Content
|
|
8
|
+
}
|
|
9
|
+
specimens: {
|
|
10
|
+
directory: {
|
|
11
|
+
label: string
|
|
12
|
+
}
|
|
13
|
+
file: {
|
|
14
|
+
label: string
|
|
15
|
+
size: string
|
|
16
|
+
type: string
|
|
17
|
+
lastModified: number
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}>
|
|
21
|
+
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
|
|
2
|
+
import Quay from "../../../index.js"
|
|
3
|
+
import {Group} from "../../group.js"
|
|
4
|
+
import {PlainSchema} from "./schema.js"
|
|
5
|
+
import {GroupConfig, SortFn} from "../../types.js"
|
|
6
|
+
import {Codex} from "../../aspects/codex/codex.js"
|
|
7
|
+
|
|
8
|
+
export class PlainGroup extends Group<PlainSchema> {
|
|
9
|
+
static config = (): GroupConfig<PlainSchema> => {
|
|
10
|
+
const codex = Codex.setup<PlainSchema>({
|
|
11
|
+
item: {icon: "📄"},
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const root = codex.root(
|
|
15
|
+
codex.create("item", {label: "root"})
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
codex,
|
|
20
|
+
root,
|
|
21
|
+
defaultSort: "id",
|
|
22
|
+
sorts: new Map<string, SortFn<PlainSchema>>()
|
|
23
|
+
.set("label", (a, b) => a.id.localeCompare(b.id)),
|
|
24
|
+
defaultFilter: "all",
|
|
25
|
+
filters: new Map()
|
|
26
|
+
.set("all", () => true),
|
|
27
|
+
search: (terms, item) => {
|
|
28
|
+
return terms.some(term => (
|
|
29
|
+
item.kind.includes(term) ||
|
|
30
|
+
item.id.includes(term)
|
|
31
|
+
))
|
|
32
|
+
},
|
|
33
|
+
renderLabel: item => item.id,
|
|
34
|
+
renderIcon: item => item.taxon.icon,
|
|
35
|
+
renderPreview: () => null,
|
|
36
|
+
permissions: item => Quay.permissions.all,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
constructor() {
|
|
41
|
+
super(PlainGroup.config())
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
package/s/logic/types.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
|
|
2
|
+
import {Permission} from "./permissions.js"
|
|
3
|
+
import {Codex} from "./aspects/codex/codex.js"
|
|
4
|
+
import {Schema} from "./aspects/codex/parts/types.js"
|
|
5
|
+
import {CodexItem} from "./aspects/codex/parts/codex-item.js"
|
|
6
|
+
import { Content } from "@benev/slate"
|
|
7
|
+
|
|
8
|
+
export type SearchFn<Sc extends Schema> = (item: CodexItem<Sc>) => boolean
|
|
9
|
+
export type SortFn<Sc extends Schema> = (a: CodexItem<Sc>, b: CodexItem<Sc>) => number
|
|
10
|
+
|
|
11
|
+
export interface GroupConfig<Sc extends Schema> {
|
|
12
|
+
codex: Codex<Sc>
|
|
13
|
+
root: CodexItem<Sc>
|
|
14
|
+
defaultFilter: string
|
|
15
|
+
filters: Map<string, SearchFn<Sc>>
|
|
16
|
+
defaultSort: string
|
|
17
|
+
sorts: Map<string, SortFn<Sc>>
|
|
18
|
+
search: (terms: string[], item: CodexItem<Sc>) => boolean
|
|
19
|
+
renderIcon: (item: CodexItem<Sc>, opened: boolean) => Content
|
|
20
|
+
renderLabel: (item: CodexItem<Sc>) => Content
|
|
21
|
+
renderPreview: (item: CodexItem<Sc>) => Content
|
|
22
|
+
permissions: (item: CodexItem<Sc>) => Permission
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// export interface GroupActions<Sc extends Schema> {
|
|
26
|
+
// newFolder: (parent: CodexItem<Sc>) => Promise<void>
|
|
27
|
+
// move: (item: CodexItem<Sc>, target: CodexItem<Sc>) => Promise<void>
|
|
28
|
+
// delete: (item: CodexItem<Sc>) => Promise<void>
|
|
29
|
+
// rename: (item: CodexItem<Sc>, newName: string) => Promise<void>
|
|
30
|
+
// upload: (files: File[], target: CodexItem<Sc>) => Promise<void>
|
|
31
|
+
// search: (terms: string[]) => Promise<CodexItem<Sc>[]>
|
|
32
|
+
// refresh: () => Promise<void>
|
|
33
|
+
// }
|
package/s/tests.test.ts
ADDED
package/x/cellar/cask.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Hex } from "@e280/stz";
|
|
2
|
+
/** File with a hash label */
|
|
3
|
+
export class Cask {
|
|
4
|
+
hash;
|
|
5
|
+
bytes;
|
|
6
|
+
static async hash(bytes) {
|
|
7
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", bytes);
|
|
8
|
+
return Hex.fromBytes(new Uint8Array(hashBuffer));
|
|
9
|
+
}
|
|
10
|
+
static async make(bytes) {
|
|
11
|
+
const hash = await this.hash(bytes);
|
|
12
|
+
return new this(hash, bytes);
|
|
13
|
+
}
|
|
14
|
+
constructor(hash, bytes) {
|
|
15
|
+
this.hash = hash;
|
|
16
|
+
this.bytes = bytes;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=cask.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cask.js","sourceRoot":"","sources":["../../s/cellar/cask.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,GAAG,EAAC,MAAM,WAAW,CAAA;AAE7B,6BAA6B;AAC7B,MAAM,OAAO,IAAI;IAWG;IAAqB;IAVxC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,KAAiB;QAClC,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,KAAK,CAAC,CAAA;QAC/D,OAAO,GAAG,CAAC,SAAS,CAAC,IAAI,UAAU,CAAC,UAAU,CAAC,CAAC,CAAA;IACjD,CAAC;IAED,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,KAAiB;QAClC,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACnC,OAAO,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;IAC7B,CAAC;IAED,YAAmB,IAAY,EAAS,KAAiB;QAAtC,SAAI,GAAJ,IAAI,CAAQ;QAAS,UAAK,GAAL,KAAK,CAAY;IAAG,CAAC;CAC7D"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Cask } from "./cask.js";
|
|
2
|
+
import { Forklift } from "./forklift.js";
|
|
3
|
+
/**
|
|
4
|
+
* Cellar is an immutable content-addressable file store
|
|
5
|
+
* - files are identified by their hashes (duplicates are impossible)
|
|
6
|
+
* - files cannot be modified (delete it and save another)
|
|
7
|
+
* - no filenames or metadata (store those somewhere else)
|
|
8
|
+
* - pass in a forklift, which is the backend that actually stores the data
|
|
9
|
+
* - MemoryForklift is the default
|
|
10
|
+
* - OpfsForklift is probably what you really want in the browser
|
|
11
|
+
*/
|
|
12
|
+
export declare class Cellar {
|
|
13
|
+
private forklift;
|
|
14
|
+
static opfs(dirname?: string): Promise<Cellar>;
|
|
15
|
+
constructor(forklift?: Forklift);
|
|
16
|
+
list(): AsyncIterable<string>;
|
|
17
|
+
has(hash: string): Promise<boolean>;
|
|
18
|
+
save(bytes: Uint8Array): Promise<void>;
|
|
19
|
+
load(hash: string): Promise<Cask>;
|
|
20
|
+
delete(hash: string): Promise<void>;
|
|
21
|
+
clear(): Promise<void>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Cask } from "./cask.js";
|
|
2
|
+
import { OpfsForklift } from "./opfs-forklift.js";
|
|
3
|
+
import { MemoryForklift } from "./memory-forklift.js";
|
|
4
|
+
/**
|
|
5
|
+
* Cellar is an immutable content-addressable file store
|
|
6
|
+
* - files are identified by their hashes (duplicates are impossible)
|
|
7
|
+
* - files cannot be modified (delete it and save another)
|
|
8
|
+
* - no filenames or metadata (store those somewhere else)
|
|
9
|
+
* - pass in a forklift, which is the backend that actually stores the data
|
|
10
|
+
* - MemoryForklift is the default
|
|
11
|
+
* - OpfsForklift is probably what you really want in the browser
|
|
12
|
+
*/
|
|
13
|
+
export class Cellar {
|
|
14
|
+
forklift;
|
|
15
|
+
static async opfs(dirname = "cellar") {
|
|
16
|
+
return new this(await OpfsForklift.setup(dirname));
|
|
17
|
+
}
|
|
18
|
+
constructor(forklift = new MemoryForklift()) {
|
|
19
|
+
this.forklift = forklift;
|
|
20
|
+
}
|
|
21
|
+
async *list() {
|
|
22
|
+
yield* this.forklift.list();
|
|
23
|
+
}
|
|
24
|
+
async has(hash) {
|
|
25
|
+
return this.forklift.has(hash);
|
|
26
|
+
}
|
|
27
|
+
async save(bytes) {
|
|
28
|
+
const cask = await Cask.make(bytes);
|
|
29
|
+
if (!await this.forklift.has(cask.hash))
|
|
30
|
+
await this.forklift.save(cask.hash, cask.bytes);
|
|
31
|
+
}
|
|
32
|
+
async load(hash) {
|
|
33
|
+
const bytes = await this.forklift.load(hash);
|
|
34
|
+
const cask = await Cask.make(bytes);
|
|
35
|
+
if (cask.hash !== hash)
|
|
36
|
+
throw new Error(`corruption error, hash mismatch (requested ${hash}) (got ${cask.hash})`);
|
|
37
|
+
return cask;
|
|
38
|
+
}
|
|
39
|
+
async delete(hash) {
|
|
40
|
+
await this.forklift.delete(hash);
|
|
41
|
+
}
|
|
42
|
+
async clear() {
|
|
43
|
+
for await (const hash of this.list())
|
|
44
|
+
await this.delete(hash);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=cellar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cellar.js","sourceRoot":"","sources":["../../s/cellar/cellar.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,IAAI,EAAC,MAAM,WAAW,CAAA;AAE9B,OAAO,EAAC,YAAY,EAAC,MAAM,oBAAoB,CAAA;AAC/C,OAAO,EAAC,cAAc,EAAC,MAAM,sBAAsB,CAAA;AAEnD;;;;;;;;GAQG;AACH,MAAM,OAAO,MAAM;IAKE;IAJpB,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,GAAG,QAAQ;QACnC,OAAO,IAAI,IAAI,CAAC,MAAM,YAAY,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAA;IACnD,CAAC;IAED,YAAoB,WAAqB,IAAI,cAAc,EAAE;QAAzC,aAAQ,GAAR,QAAQ,CAAiC;IAAG,CAAC;IAEjE,KAAK,CAAC,CAAC,IAAI;QACV,KAAK,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAA;IAC5B,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,IAAY;QACrB,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IAC/B,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,KAAiB;QAC3B,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACnC,IAAI,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC;YACtC,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAA;IACjD,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,IAAY;QACtB,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC5C,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACnC,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI;YAAE,MAAM,IAAI,KAAK,CAAC,8CAA8C,IAAI,UAAU,IAAI,CAAC,IAAI,GAAG,CAAC,CAAA;QACjH,OAAO,IAAI,CAAA;IACZ,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,IAAY;QACxB,MAAM,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IACjC,CAAC;IAED,KAAK,CAAC,KAAK;QACV,IAAI,KAAK,EAAE,MAAM,IAAI,IAAI,IAAI,CAAC,IAAI,EAAE;YACnC,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IACzB,CAAC;CACD"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Science } from "@e280/science";
|
|
2
|
+
declare const _default: {
|
|
3
|
+
"save + load roundtrip": Science.Test;
|
|
4
|
+
"save is idempotent": Science.Test;
|
|
5
|
+
"throws on corruption": Science.Test;
|
|
6
|
+
"delete removes file": Science.Test;
|
|
7
|
+
"load missing throws": Science.Test;
|
|
8
|
+
"list returns saved hashes": Science.Test;
|
|
9
|
+
"clear removes all entries": Science.Test;
|
|
10
|
+
};
|
|
11
|
+
export default _default;
|