@abraca/nuxt 1.6.0 → 1.8.0
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/dist/module.d.mts +6 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +16 -2
- package/dist/runtime/assets/sources.css +1 -0
- package/dist/runtime/components/ADocumentTree.d.vue.ts +11 -1
- package/dist/runtime/components/ADocumentTree.vue +13 -6
- package/dist/runtime/components/ADocumentTree.vue.d.ts +11 -1
- package/dist/runtime/components/renderers/AChecklistRenderer.vue +22 -4
- package/dist/runtime/components/renderers/ADashboardRenderer.vue +4 -2
- package/dist/runtime/components/renderers/AGalleryRenderer.vue +97 -70
- package/dist/runtime/components/renderers/AGraphRenderer.vue +209 -58
- package/dist/runtime/components/renderers/AKanbanRenderer.vue +145 -34
- package/dist/runtime/components/renderers/AMediaRenderer.vue +27 -17
- package/dist/runtime/components/renderers/AOutlineRenderer.vue +38 -23
- package/dist/runtime/components/renderers/ASlidesRenderer.d.vue.ts +21 -0
- package/dist/runtime/components/renderers/ASlidesRenderer.vue +591 -0
- package/dist/runtime/components/renderers/ASlidesRenderer.vue.d.ts +21 -0
- package/dist/runtime/components/renderers/ASpatialRenderer.vue +23 -0
- package/dist/runtime/components/renderers/ATableRenderer.vue +20 -391
- package/dist/runtime/components/renderers/gallery/AGalleryItemCard.d.vue.ts +40 -0
- package/dist/runtime/components/renderers/gallery/AGalleryItemCard.vue +227 -0
- package/dist/runtime/components/renderers/gallery/AGalleryItemCard.vue.d.ts +40 -0
- package/dist/runtime/components/renderers/spatial/SpatialTransformInputs.d.vue.ts +16 -0
- package/dist/runtime/components/renderers/spatial/SpatialTransformInputs.vue +66 -0
- package/dist/runtime/components/renderers/spatial/SpatialTransformInputs.vue.d.ts +16 -0
- package/dist/runtime/components/renderers/table/ATableFlatMode.d.vue.ts +2 -0
- package/dist/runtime/components/renderers/table/ATableFlatMode.vue +184 -21
- package/dist/runtime/components/renderers/table/ATableFlatMode.vue.d.ts +2 -0
- package/dist/runtime/components/renderers/table/ATableHierarchyMode.d.vue.ts +26 -0
- package/dist/runtime/components/renderers/table/ATableHierarchyMode.vue +662 -0
- package/dist/runtime/components/renderers/table/ATableHierarchyMode.vue.d.ts +26 -0
- package/dist/runtime/composables/useAwareness.js +14 -3
- package/dist/runtime/composables/useBackgroundSync.js +19 -1
- package/dist/runtime/composables/useFileIndex.js +38 -17
- package/dist/runtime/composables/useSearchIndex.js +41 -16
- package/dist/runtime/composables/useSlidesNavigation.d.ts +45 -0
- package/dist/runtime/composables/useSlidesNavigation.js +185 -0
- package/dist/runtime/composables/useYDoc.d.ts +1 -1
- package/dist/runtime/composables/useYDoc.js +47 -9
- package/dist/runtime/locale.d.ts +38 -0
- package/dist/runtime/locale.js +41 -3
- package/dist/runtime/utils/docTypes.js +17 -0
- package/package.json +3 -3
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { useTableView } from '../../../composables/useTableView.js';
|
|
2
|
+
import type { AbracadabraLocale } from '../../../locale.js';
|
|
3
|
+
type TableLabels = AbracadabraLocale['renderers']['table'];
|
|
4
|
+
type __VLS_Props = {
|
|
5
|
+
tree: ReturnType<typeof import('../../../composables/useChildTree').useChildTree>;
|
|
6
|
+
tableView: ReturnType<typeof useTableView>;
|
|
7
|
+
states: Array<Record<string, any>>;
|
|
8
|
+
myClientId: number;
|
|
9
|
+
setLocalState: (state: Record<string, any>) => void;
|
|
10
|
+
editable?: boolean;
|
|
11
|
+
labels?: Partial<TableLabels>;
|
|
12
|
+
};
|
|
13
|
+
declare function addColumn(): void;
|
|
14
|
+
declare function addRow(): void;
|
|
15
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
|
|
16
|
+
addColumn: typeof addColumn;
|
|
17
|
+
addRow: typeof addRow;
|
|
18
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
19
|
+
openNode: (id: string, label: string) => any;
|
|
20
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
21
|
+
onOpenNode?: ((id: string, label: string) => any) | undefined;
|
|
22
|
+
}>, {
|
|
23
|
+
editable: boolean;
|
|
24
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
25
|
+
declare const _default: typeof __VLS_export;
|
|
26
|
+
export default _default;
|
|
@@ -13,20 +13,31 @@ export function useAwareness() {
|
|
|
13
13
|
states.value = new Map(awareness.getStates());
|
|
14
14
|
}
|
|
15
15
|
let _unsub = null;
|
|
16
|
+
let _rafId = null;
|
|
17
|
+
let _rafPending = false;
|
|
18
|
+
const _throttledSync = () => {
|
|
19
|
+
if (_rafPending) return;
|
|
20
|
+
_rafPending = true;
|
|
21
|
+
_rafId = requestAnimationFrame(() => {
|
|
22
|
+
_rafPending = false;
|
|
23
|
+
_rafId = null;
|
|
24
|
+
_syncStates();
|
|
25
|
+
});
|
|
26
|
+
};
|
|
16
27
|
function _subscribe() {
|
|
17
28
|
_unsub?.();
|
|
18
29
|
_unsub = null;
|
|
19
30
|
const awareness = provider.value?.awareness;
|
|
20
31
|
if (!awareness) return;
|
|
21
32
|
_syncStates();
|
|
22
|
-
|
|
23
|
-
awareness.
|
|
24
|
-
_unsub = () => awareness.off("change", handler);
|
|
33
|
+
awareness.on("change", _throttledSync);
|
|
34
|
+
_unsub = () => awareness.off("change", _throttledSync);
|
|
25
35
|
}
|
|
26
36
|
watch(provider, () => _subscribe(), { immediate: true });
|
|
27
37
|
tryOnScopeDispose(() => {
|
|
28
38
|
_unsub?.();
|
|
29
39
|
_unsub = null;
|
|
40
|
+
if (_rafId !== null) cancelAnimationFrame(_rafId);
|
|
30
41
|
});
|
|
31
42
|
const localState = computed(() => {
|
|
32
43
|
const awareness = provider.value?.awareness;
|
|
@@ -6,6 +6,8 @@ const lastCompletedAt = ref(null);
|
|
|
6
6
|
let _manager = null;
|
|
7
7
|
let _stopPeriodicSync = null;
|
|
8
8
|
let _periodicIntervalMs = 5 * 60 * 1e3;
|
|
9
|
+
let _pendingStateChanges = null;
|
|
10
|
+
let _flushScheduled = false;
|
|
9
11
|
const syncedCount = computed(() => {
|
|
10
12
|
let n = 0;
|
|
11
13
|
for (const s of syncStates.value.values()) {
|
|
@@ -32,8 +34,22 @@ const progress = computed(() => {
|
|
|
32
34
|
export function _initBackgroundSync(manager, intervalMs = 5 * 60 * 1e3) {
|
|
33
35
|
_manager = manager;
|
|
34
36
|
_periodicIntervalMs = intervalMs;
|
|
37
|
+
_pendingStateChanges = null;
|
|
38
|
+
_flushScheduled = false;
|
|
35
39
|
_manager.on("stateChanged", ({ docId, state }) => {
|
|
36
|
-
|
|
40
|
+
if (!_pendingStateChanges) _pendingStateChanges = /* @__PURE__ */ new Map();
|
|
41
|
+
_pendingStateChanges.set(docId, state);
|
|
42
|
+
if (!_flushScheduled) {
|
|
43
|
+
_flushScheduled = true;
|
|
44
|
+
queueMicrotask(() => {
|
|
45
|
+
_flushScheduled = false;
|
|
46
|
+
if (!_pendingStateChanges) return;
|
|
47
|
+
const next = new Map(syncStates.value);
|
|
48
|
+
for (const [id, s] of _pendingStateChanges) next.set(id, s);
|
|
49
|
+
_pendingStateChanges = null;
|
|
50
|
+
syncStates.value = next;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
37
53
|
});
|
|
38
54
|
_manager.init().then(() => {
|
|
39
55
|
const existing = _manager.getSyncStatus();
|
|
@@ -47,6 +63,8 @@ export function _destroyBackgroundSync() {
|
|
|
47
63
|
_stopPeriodicSync = null;
|
|
48
64
|
_manager?.destroy();
|
|
49
65
|
_manager = null;
|
|
66
|
+
_pendingStateChanges = null;
|
|
67
|
+
_flushScheduled = false;
|
|
50
68
|
syncStates.value = /* @__PURE__ */ new Map();
|
|
51
69
|
isSyncing.value = false;
|
|
52
70
|
isPaused.value = false;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { watch } from "vue";
|
|
2
|
+
import { useDebounceFn } from "@vueuse/core";
|
|
2
3
|
import * as Y from "yjs";
|
|
3
4
|
import { useAbracadabra } from "./useAbracadabra.js";
|
|
4
5
|
import { useBackgroundSync } from "./useBackgroundSync.js";
|
|
@@ -29,26 +30,46 @@ export function _destroyFileIndex() {
|
|
|
29
30
|
function _startWatcher() {
|
|
30
31
|
const { syncStates } = useBackgroundSync();
|
|
31
32
|
const { provider } = useAbracadabra();
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
33
|
+
const pendingDocIds = /* @__PURE__ */ new Set();
|
|
34
|
+
let isProcessing = false;
|
|
35
|
+
async function processPending() {
|
|
36
|
+
if (isProcessing) return;
|
|
37
|
+
isProcessing = true;
|
|
38
|
+
try {
|
|
39
|
+
const batch = [...pendingDocIds];
|
|
40
|
+
pendingDocIds.clear();
|
|
41
|
+
const p = provider.value;
|
|
42
|
+
if (!p) return;
|
|
43
|
+
for (let i = 0; i < batch.length; i++) {
|
|
44
|
+
const docId = batch[i];
|
|
45
|
+
try {
|
|
46
|
+
const childProvider = await p.loadChild(docId);
|
|
47
|
+
await childProvider.ready;
|
|
48
|
+
const treeMap = p.document.getMap("doc-tree");
|
|
49
|
+
const docLabel = treeMap.get(docId)?.label ?? "";
|
|
50
|
+
const files = [];
|
|
51
|
+
walkXmlForFiles(childProvider.document.getXmlFragment("default"), docId, docLabel, files);
|
|
52
|
+
for (const [key, val] of _fileEntries.entries()) {
|
|
53
|
+
if (val.docId === docId) _fileEntries.delete(key);
|
|
54
|
+
}
|
|
55
|
+
for (const f of files) _fileEntries.set(f.uploadId, f);
|
|
56
|
+
} catch (e) {
|
|
57
|
+
if (import.meta.dev) console.warn(`[abracadabra] file-index: failed to index doc ${docId}:`, e);
|
|
46
58
|
}
|
|
47
|
-
|
|
48
|
-
} catch (e) {
|
|
49
|
-
if (import.meta.dev) console.warn(`[abracadabra] file-index: failed to index doc ${docId}:`, e);
|
|
59
|
+
if (i % 5 === 4) await new Promise((r) => setTimeout(r, 0));
|
|
50
60
|
}
|
|
61
|
+
} finally {
|
|
62
|
+
isProcessing = false;
|
|
63
|
+
if (pendingDocIds.size > 0) debouncedProcess();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const debouncedProcess = useDebounceFn(processPending, 500);
|
|
67
|
+
const stop = watch(syncStates, (states) => {
|
|
68
|
+
for (const [docId, state] of states.entries()) {
|
|
69
|
+
if (state.status !== "synced") continue;
|
|
70
|
+
pendingDocIds.add(docId);
|
|
51
71
|
}
|
|
72
|
+
if (pendingDocIds.size > 0) debouncedProcess();
|
|
52
73
|
}, { deep: false });
|
|
53
74
|
_stopWatcher = stop;
|
|
54
75
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { ref, computed, watch } from "vue";
|
|
2
|
+
import { useDebounceFn } from "@vueuse/core";
|
|
2
3
|
import * as Y from "yjs";
|
|
3
4
|
import { useAbracadabra } from "./useAbracadabra.js";
|
|
4
5
|
import { useBackgroundSync } from "./useBackgroundSync.js";
|
|
5
6
|
let _searchIndex = null;
|
|
6
7
|
const _indexedVersions = /* @__PURE__ */ new Map();
|
|
8
|
+
const _lastSyncedCache = /* @__PURE__ */ new Map();
|
|
7
9
|
const _isReady = ref(false);
|
|
8
10
|
const _indexedCount = ref(0);
|
|
9
11
|
const _error = ref(null);
|
|
@@ -51,6 +53,7 @@ export function _initSearchIndex(serverOrigin) {
|
|
|
51
53
|
_searchIndex = new SearchIndex(serverOrigin);
|
|
52
54
|
_isReady.value = true;
|
|
53
55
|
_indexedVersions.clear();
|
|
56
|
+
_lastSyncedCache.clear();
|
|
54
57
|
_indexedCount.value = 0;
|
|
55
58
|
_startWatcher();
|
|
56
59
|
}).catch((e) => {
|
|
@@ -65,34 +68,56 @@ export function _destroySearchIndex() {
|
|
|
65
68
|
_searchIndex = null;
|
|
66
69
|
_isReady.value = false;
|
|
67
70
|
_indexedVersions.clear();
|
|
71
|
+
_lastSyncedCache.clear();
|
|
68
72
|
_indexedCount.value = 0;
|
|
69
73
|
_error.value = null;
|
|
70
74
|
}
|
|
71
75
|
function _startWatcher() {
|
|
72
76
|
const { syncStates } = useBackgroundSync();
|
|
73
77
|
const { provider } = useAbracadabra();
|
|
74
|
-
const
|
|
78
|
+
const pendingDocIds = /* @__PURE__ */ new Set();
|
|
79
|
+
let isProcessing = false;
|
|
80
|
+
async function processPending() {
|
|
81
|
+
if (isProcessing) return;
|
|
82
|
+
isProcessing = true;
|
|
83
|
+
try {
|
|
84
|
+
const batch = [...pendingDocIds];
|
|
85
|
+
pendingDocIds.clear();
|
|
86
|
+
const p = provider.value;
|
|
87
|
+
if (!p || !_searchIndex) return;
|
|
88
|
+
for (let i = 0; i < batch.length; i++) {
|
|
89
|
+
const docId = batch[i];
|
|
90
|
+
try {
|
|
91
|
+
const childProvider = await p.loadChild(docId);
|
|
92
|
+
await childProvider.ready;
|
|
93
|
+
const rootDoc = p.document;
|
|
94
|
+
const treeMap = rootDoc.getMap("doc-tree");
|
|
95
|
+
const entry = treeMap.get(docId);
|
|
96
|
+
const texts = extractDocTexts(childProvider.document, entry?.label ?? "", entry?.meta);
|
|
97
|
+
await _searchIndex.index(docId, texts);
|
|
98
|
+
_indexedVersions.set(docId, _lastSyncedCache.get(docId) ?? 0);
|
|
99
|
+
_indexedCount.value = _indexedVersions.size;
|
|
100
|
+
} catch (e) {
|
|
101
|
+
if (import.meta.dev) console.warn(`[abracadabra] search: failed to index doc ${docId}:`, e);
|
|
102
|
+
}
|
|
103
|
+
if (i % 5 === 4) await new Promise((r) => setTimeout(r, 0));
|
|
104
|
+
}
|
|
105
|
+
} finally {
|
|
106
|
+
isProcessing = false;
|
|
107
|
+
if (pendingDocIds.size > 0) debouncedProcess();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const debouncedProcess = useDebounceFn(processPending, 500);
|
|
111
|
+
const stop = watch(syncStates, (states) => {
|
|
75
112
|
if (!_searchIndex) return;
|
|
76
|
-
const p = provider.value;
|
|
77
|
-
if (!p) return;
|
|
78
113
|
for (const [docId, state] of states.entries()) {
|
|
79
114
|
if (state.status !== "synced") continue;
|
|
80
115
|
const lastSynced = state.lastSynced ?? 0;
|
|
81
116
|
if (_indexedVersions.get(docId) === lastSynced) continue;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
await childProvider.ready;
|
|
85
|
-
const rootDoc = p.document;
|
|
86
|
-
const treeMap = rootDoc.getMap("doc-tree");
|
|
87
|
-
const entry = treeMap.get(docId);
|
|
88
|
-
const texts = extractDocTexts(childProvider.document, entry?.label ?? "", entry?.meta);
|
|
89
|
-
await _searchIndex.index(docId, texts);
|
|
90
|
-
_indexedVersions.set(docId, lastSynced);
|
|
91
|
-
_indexedCount.value = _indexedVersions.size;
|
|
92
|
-
} catch (e) {
|
|
93
|
-
if (import.meta.dev) console.warn(`[abracadabra] search: failed to index doc ${docId}:`, e);
|
|
94
|
-
}
|
|
117
|
+
_lastSyncedCache.set(docId, lastSynced);
|
|
118
|
+
pendingDocIds.add(docId);
|
|
95
119
|
}
|
|
120
|
+
if (pendingDocIds.size > 0) debouncedProcess();
|
|
96
121
|
}, { deep: false });
|
|
97
122
|
_stopWatcher = stop;
|
|
98
123
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Ref } from 'vue';
|
|
2
|
+
import type { TreeEntry } from './useChildTree.js';
|
|
3
|
+
export interface SlidePosition {
|
|
4
|
+
groupIndex: number;
|
|
5
|
+
subIndex: number;
|
|
6
|
+
}
|
|
7
|
+
export interface SlidesNavigationOptions {
|
|
8
|
+
tree: {
|
|
9
|
+
childrenOf: (parentId: string | null) => TreeEntry[];
|
|
10
|
+
treeMap: any;
|
|
11
|
+
};
|
|
12
|
+
docId: string;
|
|
13
|
+
setLocalState: (state: any) => void;
|
|
14
|
+
followingUser: () => string | null | undefined;
|
|
15
|
+
states: Ref<any[]>;
|
|
16
|
+
}
|
|
17
|
+
export declare function useSlidesNavigation(options: SlidesNavigationOptions): {
|
|
18
|
+
position: Ref<{
|
|
19
|
+
groupIndex: number;
|
|
20
|
+
subIndex: number;
|
|
21
|
+
}, SlidePosition | {
|
|
22
|
+
groupIndex: number;
|
|
23
|
+
subIndex: number;
|
|
24
|
+
}>;
|
|
25
|
+
currentSlideId: import("vue").ComputedRef<string | null>;
|
|
26
|
+
currentSlide: import("vue").ComputedRef<TreeEntry | null>;
|
|
27
|
+
canGoLeft: import("vue").ComputedRef<boolean>;
|
|
28
|
+
canGoRight: import("vue").ComputedRef<boolean>;
|
|
29
|
+
canGoUp: import("vue").ComputedRef<boolean>;
|
|
30
|
+
canGoDown: import("vue").ComputedRef<boolean>;
|
|
31
|
+
goLeft: () => void;
|
|
32
|
+
goRight: () => void;
|
|
33
|
+
goUp: () => void;
|
|
34
|
+
goDown: () => void;
|
|
35
|
+
goTo: (pos: SlidePosition) => void;
|
|
36
|
+
goNext: () => void;
|
|
37
|
+
goPrev: () => void;
|
|
38
|
+
groups: import("vue").ComputedRef<TreeEntry[]>;
|
|
39
|
+
subSlidesOf: (groupId: string) => TreeEntry[];
|
|
40
|
+
currentNumber: import("vue").ComputedRef<number>;
|
|
41
|
+
totalSlides: import("vue").ComputedRef<number>;
|
|
42
|
+
currentTransition: import("vue").ComputedRef<string>;
|
|
43
|
+
lastDirection: Ref<"left" | "right" | "up" | "down", "left" | "right" | "up" | "down">;
|
|
44
|
+
adjacentSlideIds: import("vue").ComputedRef<string[]>;
|
|
45
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { ref, computed, watch, nextTick, onBeforeUnmount } from "vue";
|
|
2
|
+
export function useSlidesNavigation(options) {
|
|
3
|
+
const { tree, setLocalState, followingUser, states } = options;
|
|
4
|
+
const position = ref({ groupIndex: 0, subIndex: 0 });
|
|
5
|
+
let syncingFromFollow = false;
|
|
6
|
+
let lastAppliedT = 0;
|
|
7
|
+
const lastDirection = ref("right");
|
|
8
|
+
const groups = computed(() => tree.childrenOf(null));
|
|
9
|
+
function subSlidesOf(groupId) {
|
|
10
|
+
return tree.childrenOf(groupId);
|
|
11
|
+
}
|
|
12
|
+
const currentSlideId = computed(() => {
|
|
13
|
+
const g = groups.value[position.value.groupIndex];
|
|
14
|
+
if (!g) return null;
|
|
15
|
+
if (position.value.subIndex === 0) return g.id;
|
|
16
|
+
const subs = subSlidesOf(g.id);
|
|
17
|
+
const sub = subs[position.value.subIndex - 1];
|
|
18
|
+
return sub?.id ?? g.id;
|
|
19
|
+
});
|
|
20
|
+
const currentSlide = computed(() => {
|
|
21
|
+
const g = groups.value[position.value.groupIndex];
|
|
22
|
+
if (!g) return null;
|
|
23
|
+
if (position.value.subIndex === 0) return g;
|
|
24
|
+
const subs = subSlidesOf(g.id);
|
|
25
|
+
return subs[position.value.subIndex - 1] ?? g;
|
|
26
|
+
});
|
|
27
|
+
const canGoLeft = computed(() => position.value.groupIndex > 0);
|
|
28
|
+
const canGoRight = computed(() => position.value.groupIndex < groups.value.length - 1);
|
|
29
|
+
const canGoUp = computed(() => position.value.subIndex > 0);
|
|
30
|
+
const canGoDown = computed(() => {
|
|
31
|
+
const g = groups.value[position.value.groupIndex];
|
|
32
|
+
if (!g) return false;
|
|
33
|
+
const subs = subSlidesOf(g.id);
|
|
34
|
+
return position.value.subIndex < subs.length;
|
|
35
|
+
});
|
|
36
|
+
function goLeft() {
|
|
37
|
+
if (!canGoLeft.value) return;
|
|
38
|
+
lastDirection.value = "left";
|
|
39
|
+
position.value = { groupIndex: position.value.groupIndex - 1, subIndex: 0 };
|
|
40
|
+
broadcastPosition();
|
|
41
|
+
}
|
|
42
|
+
function goRight() {
|
|
43
|
+
if (!canGoRight.value) return;
|
|
44
|
+
lastDirection.value = "right";
|
|
45
|
+
position.value = { groupIndex: position.value.groupIndex + 1, subIndex: 0 };
|
|
46
|
+
broadcastPosition();
|
|
47
|
+
}
|
|
48
|
+
function goUp() {
|
|
49
|
+
if (!canGoUp.value) return;
|
|
50
|
+
lastDirection.value = "up";
|
|
51
|
+
position.value = { ...position.value, subIndex: position.value.subIndex - 1 };
|
|
52
|
+
broadcastPosition();
|
|
53
|
+
}
|
|
54
|
+
function goDown() {
|
|
55
|
+
if (!canGoDown.value) return;
|
|
56
|
+
lastDirection.value = "down";
|
|
57
|
+
position.value = { ...position.value, subIndex: position.value.subIndex + 1 };
|
|
58
|
+
broadcastPosition();
|
|
59
|
+
}
|
|
60
|
+
function goTo(pos) {
|
|
61
|
+
const gi = Math.max(0, Math.min(pos.groupIndex, groups.value.length - 1));
|
|
62
|
+
const g = groups.value[gi];
|
|
63
|
+
const maxSub = g ? subSlidesOf(g.id).length : 0;
|
|
64
|
+
const si = Math.max(0, Math.min(pos.subIndex, maxSub));
|
|
65
|
+
position.value = { groupIndex: gi, subIndex: si };
|
|
66
|
+
broadcastPosition();
|
|
67
|
+
}
|
|
68
|
+
function goNext() {
|
|
69
|
+
if (canGoDown.value) goDown();
|
|
70
|
+
else if (canGoRight.value) goRight();
|
|
71
|
+
}
|
|
72
|
+
function goPrev() {
|
|
73
|
+
if (canGoUp.value) goUp();
|
|
74
|
+
else if (canGoLeft.value) {
|
|
75
|
+
lastDirection.value = "left";
|
|
76
|
+
const newGi = position.value.groupIndex - 1;
|
|
77
|
+
const g = groups.value[newGi];
|
|
78
|
+
const maxSub = g ? subSlidesOf(g.id).length : 0;
|
|
79
|
+
position.value = { groupIndex: newGi, subIndex: maxSub };
|
|
80
|
+
broadcastPosition();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const totalSlides = computed(() => {
|
|
84
|
+
let count = 0;
|
|
85
|
+
for (const g of groups.value) {
|
|
86
|
+
count += 1 + subSlidesOf(g.id).length;
|
|
87
|
+
}
|
|
88
|
+
return count;
|
|
89
|
+
});
|
|
90
|
+
const currentNumber = computed(() => {
|
|
91
|
+
let n = 0;
|
|
92
|
+
for (let i = 0; i < groups.value.length; i++) {
|
|
93
|
+
const g = groups.value[i];
|
|
94
|
+
if (i < position.value.groupIndex) {
|
|
95
|
+
n += 1 + subSlidesOf(g.id).length;
|
|
96
|
+
} else if (i === position.value.groupIndex) {
|
|
97
|
+
n += position.value.subIndex + 1;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return n;
|
|
102
|
+
});
|
|
103
|
+
const currentTransition = computed(() => {
|
|
104
|
+
return currentSlide.value?.meta?.slidesTransition ?? "fade";
|
|
105
|
+
});
|
|
106
|
+
const adjacentSlideIds = computed(() => {
|
|
107
|
+
const ids = [];
|
|
108
|
+
const gi = position.value.groupIndex;
|
|
109
|
+
const si = position.value.subIndex;
|
|
110
|
+
if (gi > 0) ids.push(groups.value[gi - 1].id);
|
|
111
|
+
if (gi < groups.value.length - 1) ids.push(groups.value[gi + 1].id);
|
|
112
|
+
const g = groups.value[gi];
|
|
113
|
+
if (g) {
|
|
114
|
+
const subs = subSlidesOf(g.id);
|
|
115
|
+
if (si === 1) ids.push(g.id);
|
|
116
|
+
else if (si > 1 && subs[si - 2]) ids.push(subs[si - 2].id);
|
|
117
|
+
if (si === 0 && subs.length > 0) ids.push(subs[0].id);
|
|
118
|
+
else if (si > 0 && subs[si]) ids.push(subs[si].id);
|
|
119
|
+
}
|
|
120
|
+
return ids;
|
|
121
|
+
});
|
|
122
|
+
function broadcastPosition() {
|
|
123
|
+
if (syncingFromFollow || followingUser()) return;
|
|
124
|
+
setLocalState({
|
|
125
|
+
"slides:position": {
|
|
126
|
+
groupIndex: position.value.groupIndex,
|
|
127
|
+
subIndex: position.value.subIndex,
|
|
128
|
+
t: Date.now()
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
watch([followingUser, states], () => {
|
|
133
|
+
const key = followingUser();
|
|
134
|
+
if (!key) return;
|
|
135
|
+
const state = states.value.find((s) => s.user?.publicKey === key);
|
|
136
|
+
const pos = state?.["slides:position"];
|
|
137
|
+
if (!pos || pos.t <= lastAppliedT) return;
|
|
138
|
+
lastAppliedT = pos.t;
|
|
139
|
+
syncingFromFollow = true;
|
|
140
|
+
goTo({ groupIndex: pos.groupIndex, subIndex: pos.subIndex });
|
|
141
|
+
nextTick(() => {
|
|
142
|
+
syncingFromFollow = false;
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
watch(groups, (gs) => {
|
|
146
|
+
if (gs.length === 0) {
|
|
147
|
+
position.value = { groupIndex: 0, subIndex: 0 };
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const gi = Math.min(position.value.groupIndex, gs.length - 1);
|
|
151
|
+
const g = gs[gi];
|
|
152
|
+
const maxSub = g ? subSlidesOf(g.id).length : 0;
|
|
153
|
+
const si = Math.min(position.value.subIndex, maxSub);
|
|
154
|
+
if (gi !== position.value.groupIndex || si !== position.value.subIndex) {
|
|
155
|
+
position.value = { groupIndex: gi, subIndex: si };
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
broadcastPosition();
|
|
159
|
+
onBeforeUnmount(() => {
|
|
160
|
+
setLocalState({ "slides:position": null });
|
|
161
|
+
});
|
|
162
|
+
return {
|
|
163
|
+
position,
|
|
164
|
+
currentSlideId,
|
|
165
|
+
currentSlide,
|
|
166
|
+
canGoLeft,
|
|
167
|
+
canGoRight,
|
|
168
|
+
canGoUp,
|
|
169
|
+
canGoDown,
|
|
170
|
+
goLeft,
|
|
171
|
+
goRight,
|
|
172
|
+
goUp,
|
|
173
|
+
goDown,
|
|
174
|
+
goTo,
|
|
175
|
+
goNext,
|
|
176
|
+
goPrev,
|
|
177
|
+
groups,
|
|
178
|
+
subSlidesOf,
|
|
179
|
+
currentNumber,
|
|
180
|
+
totalSlides,
|
|
181
|
+
currentTransition,
|
|
182
|
+
lastDirection,
|
|
183
|
+
adjacentSlideIds
|
|
184
|
+
};
|
|
185
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* IMPORTANT: All `doc` and `provider` parameters must be ShallowRef.
|
|
5
5
|
* Never pass plain refs or reactive() wrappers — Vue's deep proxy breaks Yjs internals.
|
|
6
6
|
*
|
|
7
|
-
* Ported from cou-sh/app/composables/useYDoc.ts
|
|
7
|
+
* Ported from cou-sh/app/composables/useYDoc.ts.
|
|
8
8
|
* Three.js multiplayer utilities (useMultiplayerObject, useSmoothedCursors) are
|
|
9
9
|
* included for parity but require Three.js in the consuming app.
|
|
10
10
|
*/
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
shallowRef,
|
|
5
5
|
computed,
|
|
6
6
|
watch,
|
|
7
|
+
nextTick,
|
|
7
8
|
onMounted,
|
|
8
9
|
onBeforeUnmount
|
|
9
10
|
} from "vue";
|
|
@@ -11,11 +12,17 @@ export function useSyncedMap(doc, mapName) {
|
|
|
11
12
|
const state = reactive({});
|
|
12
13
|
const ymap = shallowRef(null);
|
|
13
14
|
const lastUpdateLocal = shallowRef(true);
|
|
15
|
+
function readVal(map, key) {
|
|
16
|
+
const raw = map.get(key);
|
|
17
|
+
if (raw == null) return void 0;
|
|
18
|
+
return raw && typeof raw === "object" && "toJSON" in raw && typeof raw.toJSON === "function" && raw._map !== void 0 ? raw.toJSON() : raw;
|
|
19
|
+
}
|
|
14
20
|
const observer = (event) => {
|
|
15
21
|
lastUpdateLocal.value = event.transaction.local;
|
|
16
22
|
event.keysChanged.forEach((key) => {
|
|
17
|
-
if (ymap.value.has(key))
|
|
18
|
-
|
|
23
|
+
if (ymap.value.has(key)) {
|
|
24
|
+
state[key] = readVal(ymap.value, key);
|
|
25
|
+
} else delete state[key];
|
|
19
26
|
});
|
|
20
27
|
};
|
|
21
28
|
const init = (newDoc) => {
|
|
@@ -23,9 +30,20 @@ export function useSyncedMap(doc, mapName) {
|
|
|
23
30
|
ymap.value = newDoc.getMap(mapName);
|
|
24
31
|
for (const key in state) delete state[key];
|
|
25
32
|
ymap.value.forEach((value, key) => {
|
|
26
|
-
state[key] = value;
|
|
33
|
+
state[key] = value && typeof value === "object" && "toJSON" in value && typeof value.toJSON === "function" && value._map !== void 0 ? value.toJSON() : value;
|
|
27
34
|
});
|
|
28
35
|
ymap.value.observe(observer);
|
|
36
|
+
nextTick(() => {
|
|
37
|
+
if (!ymap.value) return;
|
|
38
|
+
const map = ymap.value;
|
|
39
|
+
map.forEach((value, key) => {
|
|
40
|
+
const v = value && typeof value === "object" && "toJSON" in value && typeof value.toJSON === "function" && value._map !== void 0 ? value.toJSON() : value;
|
|
41
|
+
if (state[key] !== v) state[key] = v;
|
|
42
|
+
});
|
|
43
|
+
for (const key in state) {
|
|
44
|
+
if (!map.has(key)) delete state[key];
|
|
45
|
+
}
|
|
46
|
+
});
|
|
29
47
|
};
|
|
30
48
|
watch(doc, (d) => d && init(d), { immediate: true });
|
|
31
49
|
onBeforeUnmount(() => {
|
|
@@ -37,7 +55,7 @@ export function useSyncedMap(doc, mapName) {
|
|
|
37
55
|
set: (key, value) => ymap.value?.set(key, value),
|
|
38
56
|
remove: (key) => ymap.value?.delete(key),
|
|
39
57
|
clear: () => ymap.value?.clear(),
|
|
40
|
-
get: (key) => ymap.value
|
|
58
|
+
get: (key) => ymap.value ? readVal(ymap.value, key) : void 0,
|
|
41
59
|
yMap: computed(() => ymap.value)
|
|
42
60
|
};
|
|
43
61
|
}
|
|
@@ -63,6 +81,10 @@ export function useSyncedArray(doc, arrayName) {
|
|
|
63
81
|
yarray.value = newDoc.getArray(arrayName);
|
|
64
82
|
list.value = yarray.value.toArray();
|
|
65
83
|
yarray.value.observe(observer);
|
|
84
|
+
nextTick(() => {
|
|
85
|
+
if (!yarray.value) return;
|
|
86
|
+
list.value = yarray.value.toArray();
|
|
87
|
+
});
|
|
66
88
|
};
|
|
67
89
|
watch(doc, (d) => d && init(d), { immediate: true });
|
|
68
90
|
onBeforeUnmount(() => {
|
|
@@ -95,6 +117,10 @@ export function useSyncedText(doc, textName) {
|
|
|
95
117
|
ytext.value = newDoc.getText(textName);
|
|
96
118
|
text.value = ytext.value.toString();
|
|
97
119
|
ytext.value.observe(observer);
|
|
120
|
+
nextTick(() => {
|
|
121
|
+
if (!ytext.value) return;
|
|
122
|
+
text.value = ytext.value.toString();
|
|
123
|
+
});
|
|
98
124
|
};
|
|
99
125
|
watch(doc, (d) => d && init(d), { immediate: true });
|
|
100
126
|
onBeforeUnmount(() => {
|
|
@@ -118,20 +144,32 @@ export function useSyncedXml(doc, xmlName) {
|
|
|
118
144
|
export function useAwarenessOf(provider) {
|
|
119
145
|
const states = ref([]);
|
|
120
146
|
const currentUser = ref(null);
|
|
121
|
-
|
|
122
|
-
|
|
147
|
+
let _rafId = null;
|
|
148
|
+
const flush = () => {
|
|
149
|
+
_rafId = null;
|
|
150
|
+
if (!provider.value?.awareness) return;
|
|
123
151
|
const awareness = provider.value.awareness;
|
|
124
152
|
states.value = Array.from(awareness.getStates().entries()).map(([id, state]) => ({ clientId: id, ...state }));
|
|
125
153
|
const local = awareness.getLocalState();
|
|
126
154
|
if (local) currentUser.value = { clientId: awareness.clientID, ...local };
|
|
127
155
|
};
|
|
156
|
+
const update = () => {
|
|
157
|
+
if (_rafId === null) _rafId = requestAnimationFrame(flush);
|
|
158
|
+
};
|
|
128
159
|
watch(provider, (p, oldP) => {
|
|
129
|
-
if (oldP) oldP.awareness.off("change", update);
|
|
130
|
-
if (
|
|
160
|
+
if (oldP?.awareness) oldP.awareness.off("change", update);
|
|
161
|
+
if (_rafId !== null) {
|
|
162
|
+
cancelAnimationFrame(_rafId);
|
|
163
|
+
_rafId = null;
|
|
164
|
+
}
|
|
165
|
+
if (p?.awareness) {
|
|
131
166
|
p.awareness.on("change", update);
|
|
132
|
-
|
|
167
|
+
flush();
|
|
133
168
|
}
|
|
134
169
|
}, { immediate: true });
|
|
170
|
+
onBeforeUnmount(() => {
|
|
171
|
+
if (_rafId !== null) cancelAnimationFrame(_rafId);
|
|
172
|
+
});
|
|
135
173
|
const setLocalState = (state) => {
|
|
136
174
|
if (!provider.value) return;
|
|
137
175
|
const awareness = provider.value.awareness;
|