triiiceratops 0.11.1 → 0.12.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/README.md +17 -9
- package/dist/{ArrowCounterClockwise-CN8KGaI0.js → ArrowCounterClockwise-CM9mGGcp.js} +1 -1
- package/dist/X-Bn7S7vUL.js +963 -0
- package/dist/{annotation_tool_point-BpZXtX5D.js → annotation_tool_point-LoRp_nrI.js} +1 -1
- package/dist/annotorious-openseadragon.es-tb5X-LtF.js +33045 -0
- package/dist/components/AnnotationOverlay.svelte +10 -17
- package/dist/components/DemoHeader.svelte +73 -5
- package/dist/components/MetadataDialog.svelte +4 -1
- package/dist/components/OSDViewer.svelte +39 -3
- package/dist/components/SearchPanel.svelte +8 -5
- package/dist/components/ThemeToggle.svelte +1 -1
- package/dist/components/ThumbnailGallery.svelte +229 -38
- package/dist/components/Toolbar.svelte +105 -6
- package/dist/components/TriiiceratopsViewer.svelte +37 -12
- package/dist/components/TriiiceratopsViewerElement.svelte +3 -1
- package/dist/custom-element.js +1 -0
- package/dist/{image_filters_reset-CyWg622b.js → image_filters_reset-CmWuQiOc.js} +1 -1
- package/dist/paraglide/messages/_index.d.ts +9 -0
- package/dist/paraglide/messages/_index.js +10 -1
- package/dist/paraglide/messages/settings_toggle_show_viewing_mode.d.ts +4 -0
- package/dist/paraglide/messages/settings_toggle_show_viewing_mode.js +33 -0
- package/dist/paraglide/messages/show_mode_toggle.d.ts +4 -0
- package/dist/paraglide/messages/show_mode_toggle.js +33 -0
- package/dist/paraglide/messages/toggle_single_page_mode.d.ts +4 -0
- package/dist/paraglide/messages/toggle_single_page_mode.js +33 -0
- package/dist/paraglide/messages/toggle_two_page_mode.d.ts +4 -0
- package/dist/paraglide/messages/toggle_two_page_mode.js +33 -0
- package/dist/paraglide/messages/two_page_mode.d.ts +4 -0
- package/dist/paraglide/messages/two_page_mode.js +33 -0
- package/dist/paraglide/messages/viewing_mode_individuals.d.ts +4 -0
- package/dist/paraglide/messages/viewing_mode_individuals.js +33 -0
- package/dist/paraglide/messages/viewing_mode_label.d.ts +4 -0
- package/dist/paraglide/messages/viewing_mode_label.js +33 -0
- package/dist/paraglide/messages/viewing_mode_paged.d.ts +4 -0
- package/dist/paraglide/messages/viewing_mode_paged.js +33 -0
- package/dist/paraglide/messages/viewing_mode_shift_pairing.d.ts +4 -0
- package/dist/paraglide/messages/viewing_mode_shift_pairing.js +33 -0
- package/dist/plugins/annotation-editor/AnnotationEditorController.svelte +5 -3
- package/dist/plugins/annotation-editor/AnnotationEditorPanel.svelte +3 -3
- package/dist/plugins/annotation-editor/AnnotationManager.svelte.d.ts +3 -0
- package/dist/plugins/annotation-editor/AnnotationManager.svelte.js +19 -14
- package/dist/plugins/annotation-editor/loader.svelte.js +2 -2
- package/dist/plugins/annotation-editor.js +1228 -32159
- package/dist/plugins/image-manipulation/ImageManipulationController.svelte +1 -1
- package/dist/plugins/image-manipulation.js +3 -3
- package/dist/state/manifests.svelte.d.ts +2 -1
- package/dist/state/manifests.svelte.js +5 -9
- package/dist/state/manifests.test.js +52 -50
- package/dist/state/viewer.svelte.d.ts +20 -1
- package/dist/state/viewer.svelte.js +150 -16
- package/dist/triiiceratops-bundle.js +3107 -2584
- package/dist/triiiceratops-element.iife.js +26 -26
- package/dist/triiiceratops.css +1 -1
- package/dist/types/config.d.ts +33 -0
- package/dist/utils/annotationAdapter.js +2 -2
- package/dist/utils/annotationAdapter.test.js +0 -1
- package/package.json +12 -2
- package/dist/X-i_EmjXwW.js +0 -906
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import ImageManipulationPanel from './ImageManipulationPanel.svelte';
|
|
10
10
|
|
|
11
11
|
// Props from the plugin system
|
|
12
|
-
let { isOpen = false, close } = $props();
|
|
12
|
+
let { isOpen: _isOpen = false, close } = $props();
|
|
13
13
|
|
|
14
14
|
const viewerState = getContext<ViewerState>(VIEWER_STATE_KEY);
|
|
15
15
|
let filters = $state<ImageFilters>({ ...DEFAULT_FILTERS });
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import "svelte/internal/disclose-version";
|
|
2
2
|
import * as e from "svelte/internal/client";
|
|
3
3
|
import { getContext as s0 } from "svelte";
|
|
4
|
-
import { l as l0, s as i0, X as n0, c as o0, V as c0, g as v0 } from "../X-
|
|
5
|
-
import { A as d0 } from "../ArrowCounterClockwise-
|
|
6
|
-
import { i as _0, a as g0, b as f0, g as u0, c as h0, e as m0, d as p0, f as b0 } from "../image_filters_reset-
|
|
4
|
+
import { l as l0, s as i0, X as n0, c as o0, V as c0, g as v0 } from "../X-Bn7S7vUL.js";
|
|
5
|
+
import { A as d0 } from "../ArrowCounterClockwise-CM9mGGcp.js";
|
|
6
|
+
import { i as _0, a as g0, b as f0, g as u0, c as h0, e as m0, d as p0, f as b0 } from "../image_filters_reset-CmWuQiOc.js";
|
|
7
7
|
const G = {
|
|
8
8
|
brightness: 100,
|
|
9
9
|
contrast: 100,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SvelteMap } from 'svelte/reactivity';
|
|
1
2
|
export interface ManifestEntry {
|
|
2
3
|
json?: any;
|
|
3
4
|
manifesto?: any;
|
|
@@ -6,7 +7,7 @@ export interface ManifestEntry {
|
|
|
6
7
|
}
|
|
7
8
|
export declare class ManifestsState {
|
|
8
9
|
manifests: Record<string, ManifestEntry>;
|
|
9
|
-
userAnnotations:
|
|
10
|
+
userAnnotations: SvelteMap<string, any[]>;
|
|
10
11
|
constructor();
|
|
11
12
|
private userAnnotationKey;
|
|
12
13
|
setUserAnnotations(manifestId: string, canvasId: string, annotations: any[]): void;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { SvelteMap } from 'svelte/reactivity';
|
|
1
2
|
import * as manifesto from 'manifesto.js';
|
|
2
3
|
export class ManifestsState {
|
|
3
4
|
manifests = $state({});
|
|
4
5
|
// User-created annotations (from plugins like annotation editor)
|
|
5
|
-
userAnnotations =
|
|
6
|
+
userAnnotations = new SvelteMap();
|
|
6
7
|
constructor() { }
|
|
7
8
|
// === User Annotations API ===
|
|
8
9
|
userAnnotationKey(manifestId, canvasId) {
|
|
@@ -10,17 +11,12 @@ export class ManifestsState {
|
|
|
10
11
|
}
|
|
11
12
|
setUserAnnotations(manifestId, canvasId, annotations) {
|
|
12
13
|
const key = this.userAnnotationKey(manifestId, canvasId);
|
|
13
|
-
|
|
14
|
-
const newMap = new Map(this.userAnnotations);
|
|
15
|
-
newMap.set(key, annotations);
|
|
16
|
-
this.userAnnotations = newMap;
|
|
14
|
+
this.userAnnotations.set(key, annotations);
|
|
17
15
|
}
|
|
18
16
|
clearUserAnnotations(manifestId, canvasId) {
|
|
19
17
|
const key = this.userAnnotationKey(manifestId, canvasId);
|
|
20
18
|
if (this.userAnnotations.has(key)) {
|
|
21
|
-
|
|
22
|
-
newMap.delete(key);
|
|
23
|
-
this.userAnnotations = newMap;
|
|
19
|
+
this.userAnnotations.delete(key);
|
|
24
20
|
}
|
|
25
21
|
}
|
|
26
22
|
getUserAnnotations(manifestId, canvasId) {
|
|
@@ -109,7 +105,7 @@ export class ManifestsState {
|
|
|
109
105
|
// Manifesto wraps the JSON. We can access the underlying JSON via canvas.__jsonld
|
|
110
106
|
// Or better, use canvas.getContent() if it works, but for external lists manual fetch is robust.
|
|
111
107
|
const canvasJson = canvas.__jsonld;
|
|
112
|
-
|
|
108
|
+
const annotations = [];
|
|
113
109
|
// Helper to parse list using Manifesto
|
|
114
110
|
const parseList = (listJson) => {
|
|
115
111
|
// manifesto.create is not available in 4.3.0 or not exported nicely?
|
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from
|
|
2
|
-
import { ManifestsState } from
|
|
3
|
-
import * as manifesto from
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { ManifestsState } from './manifests.svelte';
|
|
3
|
+
import * as manifesto from 'manifesto.js';
|
|
4
4
|
// Mock manifesto.js since it's an external dependency
|
|
5
|
-
vi.mock(
|
|
5
|
+
vi.mock('manifesto.js', async (importOriginal) => {
|
|
6
6
|
const actual = await importOriginal();
|
|
7
7
|
return {
|
|
8
8
|
...actual,
|
|
9
|
-
parseManifest: vi.fn((
|
|
9
|
+
parseManifest: vi.fn((_json) => {
|
|
10
10
|
// Minimal mock of a manifesto object
|
|
11
11
|
return {
|
|
12
12
|
getSequences: () => [
|
|
13
13
|
{
|
|
14
|
-
getCanvases: () => [{ id:
|
|
14
|
+
getCanvases: () => [{ id: 'canvas1' }],
|
|
15
15
|
getCanvasById: (id) => {
|
|
16
|
-
if (id ===
|
|
16
|
+
if (id === 'canvas1') {
|
|
17
17
|
return {
|
|
18
|
-
id:
|
|
18
|
+
id: 'canvas1',
|
|
19
19
|
__jsonld: {
|
|
20
20
|
otherContent: [
|
|
21
21
|
{
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
'@id': 'http://example.org/list1',
|
|
23
|
+
'@type': 'sc:AnnotationList',
|
|
24
24
|
},
|
|
25
25
|
],
|
|
26
26
|
annotations: [
|
|
27
27
|
// v3 style
|
|
28
28
|
{
|
|
29
|
-
id:
|
|
30
|
-
type:
|
|
29
|
+
id: 'http://example.org/list2',
|
|
30
|
+
type: 'AnnotationPage',
|
|
31
31
|
},
|
|
32
32
|
],
|
|
33
33
|
},
|
|
@@ -41,80 +41,82 @@ vi.mock("manifesto.js", async (importOriginal) => {
|
|
|
41
41
|
}),
|
|
42
42
|
};
|
|
43
43
|
});
|
|
44
|
-
describe(
|
|
44
|
+
describe('ManifestsState', () => {
|
|
45
45
|
let state;
|
|
46
46
|
const mockFetch = vi.fn();
|
|
47
47
|
beforeEach(() => {
|
|
48
|
-
vi.stubGlobal(
|
|
48
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
49
49
|
state = new ManifestsState();
|
|
50
50
|
mockFetch.mockReset();
|
|
51
51
|
});
|
|
52
52
|
afterEach(() => {
|
|
53
53
|
vi.restoreAllMocks();
|
|
54
54
|
});
|
|
55
|
-
describe(
|
|
56
|
-
it(
|
|
55
|
+
describe('fetchManifest', () => {
|
|
56
|
+
it('should fetch and store a manifest', async () => {
|
|
57
57
|
const mockManifest = {
|
|
58
|
-
|
|
59
|
-
label:
|
|
58
|
+
'@id': 'http://example.org/manifest',
|
|
59
|
+
label: 'Test Manifest',
|
|
60
60
|
};
|
|
61
61
|
mockFetch.mockResolvedValueOnce({
|
|
62
62
|
ok: true,
|
|
63
63
|
json: async () => mockManifest,
|
|
64
64
|
});
|
|
65
|
-
await state.fetchManifest(
|
|
66
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
67
|
-
expect(state.manifests[
|
|
68
|
-
expect(state.manifests[
|
|
69
|
-
expect(state.manifests[
|
|
65
|
+
await state.fetchManifest('http://example.org/manifest');
|
|
66
|
+
expect(mockFetch).toHaveBeenCalledWith('http://example.org/manifest');
|
|
67
|
+
expect(state.manifests['http://example.org/manifest']).toBeDefined();
|
|
68
|
+
expect(state.manifests['http://example.org/manifest'].json).toEqual(mockManifest);
|
|
69
|
+
expect(state.manifests['http://example.org/manifest'].isFetching).toBe(false);
|
|
70
70
|
expect(manifesto.parseManifest).toHaveBeenCalledWith(mockManifest);
|
|
71
71
|
});
|
|
72
|
-
it(
|
|
73
|
-
mockFetch.mockRejectedValueOnce(new Error(
|
|
74
|
-
await state.fetchManifest(
|
|
75
|
-
expect(state.manifests[
|
|
76
|
-
expect(state.manifests[
|
|
72
|
+
it('should handle fetch errors', async () => {
|
|
73
|
+
mockFetch.mockRejectedValueOnce(new Error('Network Error'));
|
|
74
|
+
await state.fetchManifest('http://example.org/error');
|
|
75
|
+
expect(state.manifests['http://example.org/error'].error).toBe('Network Error');
|
|
76
|
+
expect(state.manifests['http://example.org/error'].isFetching).toBe(false);
|
|
77
77
|
});
|
|
78
|
-
it(
|
|
78
|
+
it('should not fetch if already fetched', async () => {
|
|
79
79
|
// Prime the state
|
|
80
|
-
state.manifests[
|
|
80
|
+
state.manifests['http://example.org/cached'] = {
|
|
81
81
|
isFetching: false,
|
|
82
82
|
json: {},
|
|
83
83
|
};
|
|
84
|
-
await state.fetchManifest(
|
|
84
|
+
await state.fetchManifest('http://example.org/cached');
|
|
85
85
|
expect(mockFetch).not.toHaveBeenCalled();
|
|
86
86
|
});
|
|
87
87
|
});
|
|
88
|
-
describe(
|
|
89
|
-
it(
|
|
88
|
+
describe('getCanvases', () => {
|
|
89
|
+
it('should return canvases from parsed manifest', async () => {
|
|
90
90
|
// Mock internal state directly to avoid fetch overhead
|
|
91
|
-
state.manifests[
|
|
91
|
+
state.manifests['http://example.org/manifest'] = {
|
|
92
92
|
manifesto: {
|
|
93
93
|
getSequences: () => [
|
|
94
94
|
{
|
|
95
|
-
getCanvases: () => [
|
|
95
|
+
getCanvases: () => ['mockCanvas1', 'mockCanvas2'],
|
|
96
96
|
},
|
|
97
97
|
],
|
|
98
98
|
},
|
|
99
99
|
};
|
|
100
|
-
const canvases = state.getCanvases(
|
|
101
|
-
expect(canvases).toEqual([
|
|
100
|
+
const canvases = state.getCanvases('http://example.org/manifest');
|
|
101
|
+
expect(canvases).toEqual(['mockCanvas1', 'mockCanvas2']);
|
|
102
102
|
});
|
|
103
|
-
it(
|
|
104
|
-
const canvases = state.getCanvases(
|
|
103
|
+
it('should return empty array if manifest not found', () => {
|
|
104
|
+
const canvases = state.getCanvases('http://example.org/missing');
|
|
105
105
|
expect(canvases).toEqual([]);
|
|
106
106
|
});
|
|
107
107
|
});
|
|
108
|
-
describe(
|
|
109
|
-
it(
|
|
108
|
+
describe('manualGetAnnotations', () => {
|
|
109
|
+
it('should extract annotations and trigger fetch for external lists', async () => {
|
|
110
110
|
// Setup mock state with a manifest that has a canvas
|
|
111
|
-
state.manifests[
|
|
111
|
+
state.manifests['http://example.org/manifest'] = {
|
|
112
112
|
manifesto: {
|
|
113
113
|
getSequences: () => [
|
|
114
114
|
{
|
|
115
115
|
getCanvasById: () => ({
|
|
116
116
|
__jsonld: {
|
|
117
|
-
otherContent: [
|
|
117
|
+
otherContent: [
|
|
118
|
+
{ '@id': 'http://example.org/list1' },
|
|
119
|
+
],
|
|
118
120
|
},
|
|
119
121
|
}),
|
|
120
122
|
},
|
|
@@ -124,24 +126,24 @@ describe("ManifestsState", () => {
|
|
|
124
126
|
// Mock the fetch for the annotation list
|
|
125
127
|
mockFetch.mockResolvedValue({
|
|
126
128
|
ok: true,
|
|
127
|
-
json: async () => ({ resources: [{
|
|
129
|
+
json: async () => ({ resources: [{ '@id': 'anno1' }] }),
|
|
128
130
|
});
|
|
129
131
|
// First call triggers fetch
|
|
130
132
|
// manualGetAnnotations calls fetchAnnotationList which is async, but manualGetAnnotations itself is synchronous and returns partial data
|
|
131
|
-
state.manualGetAnnotations(
|
|
133
|
+
state.manualGetAnnotations('http://example.org/manifest', 'canvas1');
|
|
132
134
|
// We need to wait for the async fetchAnnotationList to complete.
|
|
133
135
|
// Since it's not returned, we can wait a tick or use `vi.waitFor` if available,
|
|
134
136
|
// but simpler here is just to await a small delay since we are mocking.
|
|
135
137
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
136
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
138
|
+
expect(mockFetch).toHaveBeenCalledWith('http://example.org/list1');
|
|
137
139
|
// Simulate update after fetch (in real app this is reactive, here we manually update state)
|
|
138
|
-
state.manifests[
|
|
139
|
-
json: { resources: [{
|
|
140
|
+
state.manifests['http://example.org/list1'] = {
|
|
141
|
+
json: { resources: [{ '@id': 'anno1' }] },
|
|
140
142
|
};
|
|
141
143
|
// Second call should return the annotations
|
|
142
|
-
const annos = state.manualGetAnnotations(
|
|
144
|
+
const annos = state.manualGetAnnotations('http://example.org/manifest', 'canvas1');
|
|
143
145
|
expect(annos).toHaveLength(1);
|
|
144
|
-
expect(annos[0][
|
|
146
|
+
expect(annos[0]['@id']).toBe('anno1');
|
|
145
147
|
});
|
|
146
148
|
});
|
|
147
149
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SvelteSet } from 'svelte/reactivity';
|
|
1
2
|
import type { ViewerConfig } from '../types/config';
|
|
2
3
|
import type { PluginMenuButton, PluginPanel, PluginDef } from '../types/plugin';
|
|
3
4
|
/**
|
|
@@ -15,6 +16,15 @@ export interface ViewerStateSnapshot {
|
|
|
15
16
|
searchQuery: string;
|
|
16
17
|
isFullScreen: boolean;
|
|
17
18
|
dockSide: string;
|
|
19
|
+
viewingMode: 'individuals' | 'paged';
|
|
20
|
+
galleryPosition: {
|
|
21
|
+
x: number;
|
|
22
|
+
y: number;
|
|
23
|
+
};
|
|
24
|
+
gallerySize: {
|
|
25
|
+
width: number;
|
|
26
|
+
height: number;
|
|
27
|
+
};
|
|
18
28
|
}
|
|
19
29
|
export declare class ViewerState {
|
|
20
30
|
manifestId: string | null;
|
|
@@ -27,11 +37,15 @@ export declare class ViewerState {
|
|
|
27
37
|
isFullScreen: boolean;
|
|
28
38
|
showMetadataDialog: boolean;
|
|
29
39
|
dockSide: string;
|
|
30
|
-
visibleAnnotationIds:
|
|
40
|
+
visibleAnnotationIds: SvelteSet<string>;
|
|
31
41
|
config: ViewerConfig;
|
|
32
42
|
get showToggle(): boolean;
|
|
33
43
|
get showCanvasNav(): boolean;
|
|
34
44
|
get showZoomControls(): boolean;
|
|
45
|
+
get galleryFixedHeight(): number;
|
|
46
|
+
get viewingMode(): "individuals" | "paged";
|
|
47
|
+
set viewingMode(value: 'individuals' | 'paged');
|
|
48
|
+
pagedOffset: number;
|
|
35
49
|
galleryPosition: {
|
|
36
50
|
x: number;
|
|
37
51
|
y: number;
|
|
@@ -98,6 +112,8 @@ export declare class ViewerState {
|
|
|
98
112
|
setViewerElement(element: HTMLElement): void;
|
|
99
113
|
toggleFullScreen(): void;
|
|
100
114
|
toggleMetadataDialog(): void;
|
|
115
|
+
setViewingMode(mode: 'individuals' | 'paged'): void;
|
|
116
|
+
togglePagedOffset(): void;
|
|
101
117
|
searchQuery: string;
|
|
102
118
|
pendingSearchQuery: string | null;
|
|
103
119
|
searchResults: any[];
|
|
@@ -105,6 +121,9 @@ export declare class ViewerState {
|
|
|
105
121
|
showSearchPanel: boolean;
|
|
106
122
|
toggleSearchPanel(): void;
|
|
107
123
|
searchAnnotations: any[];
|
|
124
|
+
/**
|
|
125
|
+
* This function now accounts for two-page mode when returning current canvas search annotations offset accordingly.
|
|
126
|
+
*/
|
|
108
127
|
get currentCanvasSearchAnnotations(): any[];
|
|
109
128
|
search(query: string): Promise<void>;
|
|
110
129
|
private _performSearch;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SvelteSet, SvelteMap } from 'svelte/reactivity';
|
|
1
2
|
import { manifestsState } from './manifests.svelte.js';
|
|
2
3
|
export class ViewerState {
|
|
3
4
|
manifestId = $state(null);
|
|
@@ -10,7 +11,7 @@ export class ViewerState {
|
|
|
10
11
|
isFullScreen = $state(false);
|
|
11
12
|
showMetadataDialog = $state(false);
|
|
12
13
|
dockSide = $state('bottom');
|
|
13
|
-
visibleAnnotationIds =
|
|
14
|
+
visibleAnnotationIds = new SvelteSet();
|
|
14
15
|
// UI Configuration
|
|
15
16
|
config = $state({});
|
|
16
17
|
// Derived configuration specific getters
|
|
@@ -23,6 +24,17 @@ export class ViewerState {
|
|
|
23
24
|
get showZoomControls() {
|
|
24
25
|
return this.config.showZoomControls ?? true;
|
|
25
26
|
}
|
|
27
|
+
get galleryFixedHeight() {
|
|
28
|
+
return this.config.gallery?.fixedHeight ?? 120;
|
|
29
|
+
}
|
|
30
|
+
get viewingMode() {
|
|
31
|
+
return this.config.viewingMode ?? 'individuals';
|
|
32
|
+
}
|
|
33
|
+
set viewingMode(value) {
|
|
34
|
+
this.config.viewingMode = value;
|
|
35
|
+
}
|
|
36
|
+
// Pairing offset for paged mode: 0 = default (pairs start at 1+2), 1 = shifted (page 1 alone, pairs start at 2+3)
|
|
37
|
+
pagedOffset = $state(0);
|
|
26
38
|
// Gallery State (Lifted for persistence during re-docking)
|
|
27
39
|
galleryPosition = $state({ x: 20, y: 100 });
|
|
28
40
|
gallerySize = $state({ width: 300, height: 400 });
|
|
@@ -74,6 +86,9 @@ export class ViewerState {
|
|
|
74
86
|
searchQuery: this.searchQuery,
|
|
75
87
|
isFullScreen: this.isFullScreen,
|
|
76
88
|
dockSide: this.dockSide,
|
|
89
|
+
viewingMode: this.viewingMode,
|
|
90
|
+
galleryPosition: this.galleryPosition,
|
|
91
|
+
gallerySize: this.gallerySize,
|
|
77
92
|
};
|
|
78
93
|
}
|
|
79
94
|
/**
|
|
@@ -141,23 +156,61 @@ export class ViewerState {
|
|
|
141
156
|
});
|
|
142
157
|
}
|
|
143
158
|
get hasNext() {
|
|
144
|
-
|
|
159
|
+
if (this.viewingMode === 'paged') {
|
|
160
|
+
// Account for paged offset: with offset 1, page 1 is single, pairs start at 2+3
|
|
161
|
+
const singlePages = this.pagedOffset;
|
|
162
|
+
if (this.currentCanvasIndex < singlePages) {
|
|
163
|
+
return this.currentCanvasIndex < this.canvases.length - 1;
|
|
164
|
+
}
|
|
165
|
+
return this.currentCanvasIndex < this.canvases.length - 2;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
return this.currentCanvasIndex < this.canvases.length - 1;
|
|
169
|
+
}
|
|
145
170
|
}
|
|
146
171
|
get hasPrevious() {
|
|
147
172
|
return this.currentCanvasIndex > 0;
|
|
148
173
|
}
|
|
149
174
|
nextCanvas() {
|
|
150
175
|
if (this.hasNext) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
176
|
+
if (this.viewingMode === 'paged') {
|
|
177
|
+
// Single pages at the start: pagedOffset (default 0, shifted = 1)
|
|
178
|
+
const singlePages = this.pagedOffset;
|
|
179
|
+
const nextIndex = this.currentCanvasIndex < singlePages
|
|
180
|
+
? this.currentCanvasIndex + 1
|
|
181
|
+
: this.currentCanvasIndex + 2;
|
|
182
|
+
const canvas = this.canvases[nextIndex];
|
|
183
|
+
this.setCanvas(canvas.id);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
const nextIndex = this.currentCanvasIndex + 1;
|
|
187
|
+
const canvas = this.canvases[nextIndex];
|
|
188
|
+
this.setCanvas(canvas.id);
|
|
189
|
+
}
|
|
154
190
|
}
|
|
155
191
|
}
|
|
156
192
|
previousCanvas() {
|
|
157
193
|
if (this.hasPrevious) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
194
|
+
if (this.viewingMode === 'paged') {
|
|
195
|
+
// Single pages at the start: pagedOffset (default 0, shifted = 1)
|
|
196
|
+
const singlePages = this.pagedOffset;
|
|
197
|
+
let prevIndex;
|
|
198
|
+
if (this.currentCanvasIndex <= singlePages) {
|
|
199
|
+
// Going back within single pages or to a single page
|
|
200
|
+
prevIndex = this.currentCanvasIndex - 1;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
// Going back in paired pages, but don't go past the last single page
|
|
204
|
+
prevIndex = Math.max(this.currentCanvasIndex - 2, singlePages);
|
|
205
|
+
}
|
|
206
|
+
const canvas = this.canvases[prevIndex];
|
|
207
|
+
this.setCanvas(canvas.id);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
const prevIndex = this.currentCanvasIndex - 1;
|
|
211
|
+
const canvas = this.canvases[prevIndex];
|
|
212
|
+
this.setCanvas(canvas.id);
|
|
213
|
+
}
|
|
161
214
|
}
|
|
162
215
|
}
|
|
163
216
|
zoomIn() {
|
|
@@ -189,6 +242,10 @@ export class ViewerState {
|
|
|
189
242
|
if (newConfig.toolbarOpen !== undefined) {
|
|
190
243
|
this.toolbarOpen = newConfig.toolbarOpen;
|
|
191
244
|
}
|
|
245
|
+
if (newConfig.viewingMode) {
|
|
246
|
+
// direct assignment works because of the setter
|
|
247
|
+
this.viewingMode = newConfig.viewingMode;
|
|
248
|
+
}
|
|
192
249
|
if (newConfig.gallery) {
|
|
193
250
|
if (newConfig.gallery.open !== undefined) {
|
|
194
251
|
this.showThumbnailGallery = newConfig.gallery.open;
|
|
@@ -196,6 +253,18 @@ export class ViewerState {
|
|
|
196
253
|
if (newConfig.gallery.dockPosition !== undefined) {
|
|
197
254
|
this.dockSide = newConfig.gallery.dockPosition;
|
|
198
255
|
}
|
|
256
|
+
if (newConfig.gallery.width !== undefined) {
|
|
257
|
+
this.gallerySize.width = newConfig.gallery.width;
|
|
258
|
+
}
|
|
259
|
+
if (newConfig.gallery.height !== undefined) {
|
|
260
|
+
this.gallerySize.height = newConfig.gallery.height;
|
|
261
|
+
}
|
|
262
|
+
if (newConfig.gallery.x !== undefined) {
|
|
263
|
+
this.galleryPosition.x = newConfig.gallery.x;
|
|
264
|
+
}
|
|
265
|
+
if (newConfig.gallery.y !== undefined) {
|
|
266
|
+
this.galleryPosition.y = newConfig.gallery.y;
|
|
267
|
+
}
|
|
199
268
|
}
|
|
200
269
|
if (newConfig.search) {
|
|
201
270
|
if (newConfig.search.open !== undefined) {
|
|
@@ -262,6 +331,39 @@ export class ViewerState {
|
|
|
262
331
|
toggleMetadataDialog() {
|
|
263
332
|
this.showMetadataDialog = !this.showMetadataDialog;
|
|
264
333
|
}
|
|
334
|
+
setViewingMode(mode) {
|
|
335
|
+
this.viewingMode = mode;
|
|
336
|
+
if (mode === 'paged') {
|
|
337
|
+
const singlePages = this.pagedOffset;
|
|
338
|
+
// If we're past the single pages, check if we're on a right-hand page
|
|
339
|
+
if (this.currentCanvasIndex >= singlePages) {
|
|
340
|
+
// Calculate position relative to where pairs start
|
|
341
|
+
const pairPosition = (this.currentCanvasIndex - singlePages) % 2;
|
|
342
|
+
if (pairPosition === 1) {
|
|
343
|
+
// We're on a right-hand page, move back one
|
|
344
|
+
const newIndex = this.currentCanvasIndex - 1;
|
|
345
|
+
const canvas = this.canvases[newIndex];
|
|
346
|
+
this.setCanvas(canvas.id);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
this.dispatchStateChange();
|
|
351
|
+
}
|
|
352
|
+
togglePagedOffset() {
|
|
353
|
+
this.pagedOffset = this.pagedOffset === 0 ? 1 : 0;
|
|
354
|
+
// Adjust current canvas position if needed
|
|
355
|
+
const singlePages = this.pagedOffset;
|
|
356
|
+
if (this.currentCanvasIndex >= singlePages) {
|
|
357
|
+
const pairPosition = (this.currentCanvasIndex - singlePages) % 2;
|
|
358
|
+
if (pairPosition === 1) {
|
|
359
|
+
// We're now on a right-hand page after the shift, move back
|
|
360
|
+
const newIndex = this.currentCanvasIndex - 1;
|
|
361
|
+
const canvas = this.canvases[newIndex];
|
|
362
|
+
this.setCanvas(canvas.id);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
this.dispatchStateChange();
|
|
366
|
+
}
|
|
265
367
|
searchQuery = $state('');
|
|
266
368
|
pendingSearchQuery = $state(null);
|
|
267
369
|
searchResults = $state([]);
|
|
@@ -276,10 +378,43 @@ export class ViewerState {
|
|
|
276
378
|
this.dispatchStateChange();
|
|
277
379
|
}
|
|
278
380
|
searchAnnotations = $state([]);
|
|
381
|
+
/**
|
|
382
|
+
* This function now accounts for two-page mode when returning current canvas search annotations offset accordingly.
|
|
383
|
+
*/
|
|
279
384
|
get currentCanvasSearchAnnotations() {
|
|
280
385
|
if (!this.canvasId)
|
|
281
386
|
return [];
|
|
282
|
-
|
|
387
|
+
if (this.viewingMode === 'paged') {
|
|
388
|
+
let annotations = this.searchAnnotations.filter((a) => a.canvasId === this.canvasId);
|
|
389
|
+
const currentIndex = this.currentCanvasIndex;
|
|
390
|
+
const singlePages = this.pagedOffset;
|
|
391
|
+
// Only include next canvas annotations if we're in a two-page spread
|
|
392
|
+
if (currentIndex >= singlePages) {
|
|
393
|
+
const nextIndex = currentIndex + 1;
|
|
394
|
+
if (nextIndex < this.canvases.length) {
|
|
395
|
+
const nextCanvas = this.canvases[nextIndex];
|
|
396
|
+
const nextCanvasId = nextCanvas.id || nextCanvas['@id'];
|
|
397
|
+
const xOffset = 1.025; // account for small gap between pages
|
|
398
|
+
const annoOffset = this.canvases[currentIndex].getWidth() * xOffset;
|
|
399
|
+
const nextAnnotations = this.searchAnnotations.filter((a) => a.canvasId === nextCanvasId);
|
|
400
|
+
// update x coordinates for display on the right side in two-page mode
|
|
401
|
+
const nextAnnotationsUpdated = nextAnnotations.map((a) => {
|
|
402
|
+
const parts = a.on.split('#xywh=');
|
|
403
|
+
const coords = parts[1].split(',').map(Number);
|
|
404
|
+
const shiftedX = coords[0] + annoOffset;
|
|
405
|
+
return {
|
|
406
|
+
...a,
|
|
407
|
+
on: `${parts[0]}#xywh=${shiftedX},${coords[1]},${coords[2]},${coords[3]}`,
|
|
408
|
+
};
|
|
409
|
+
});
|
|
410
|
+
annotations = annotations.concat(nextAnnotationsUpdated);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return annotations;
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
return this.searchAnnotations.filter((a) => a.canvasId === this.canvasId);
|
|
417
|
+
}
|
|
283
418
|
}
|
|
284
419
|
async search(query) {
|
|
285
420
|
this.dispatchStateChange();
|
|
@@ -328,7 +463,7 @@ export class ViewerState {
|
|
|
328
463
|
const data = await response.json();
|
|
329
464
|
const resources = data.resources || [];
|
|
330
465
|
// Group results by canvas index
|
|
331
|
-
const resultsByCanvas = new
|
|
466
|
+
const resultsByCanvas = new SvelteMap();
|
|
332
467
|
// Helper to parse xywh
|
|
333
468
|
const parseSelector = (onVal) => {
|
|
334
469
|
const val = typeof onVal === 'string'
|
|
@@ -362,7 +497,7 @@ export class ViewerState {
|
|
|
362
497
|
// We will take the first valid canvas we find for the annotations.
|
|
363
498
|
let canvasIndex = -1;
|
|
364
499
|
let bounds = null;
|
|
365
|
-
|
|
500
|
+
const allBounds = [];
|
|
366
501
|
for (const annoId of annotations) {
|
|
367
502
|
const annotation = resources.find((r) => r['@id'] === annoId || r.id === annoId);
|
|
368
503
|
if (annotation && annotation.on) {
|
|
@@ -406,7 +541,7 @@ export class ViewerState {
|
|
|
406
541
|
label = canvas.label[0]?.value;
|
|
407
542
|
}
|
|
408
543
|
}
|
|
409
|
-
catch (
|
|
544
|
+
catch (_e) {
|
|
410
545
|
/* ignore */
|
|
411
546
|
}
|
|
412
547
|
resultsByCanvas.set(canvasIndex, {
|
|
@@ -454,7 +589,7 @@ export class ViewerState {
|
|
|
454
589
|
label = canvas.label[0]?.value;
|
|
455
590
|
}
|
|
456
591
|
}
|
|
457
|
-
catch (
|
|
592
|
+
catch (_e) {
|
|
458
593
|
/* ignore */
|
|
459
594
|
}
|
|
460
595
|
if (!resultsByCanvas.has(canvasIndex)) {
|
|
@@ -527,7 +662,7 @@ export class ViewerState {
|
|
|
527
662
|
/** OpenSeadragon viewer instance (set by OSDViewer) */
|
|
528
663
|
osdViewer = $state.raw(null);
|
|
529
664
|
/** Event handlers for inter-plugin communication */
|
|
530
|
-
pluginEventHandlers = new
|
|
665
|
+
pluginEventHandlers = new SvelteMap();
|
|
531
666
|
// ==================== PLUGIN METHODS ====================
|
|
532
667
|
/**
|
|
533
668
|
* Register a plugin with this viewer instance.
|
|
@@ -557,8 +692,7 @@ export class ViewerState {
|
|
|
557
692
|
isVisible: () => isOpen,
|
|
558
693
|
props: {
|
|
559
694
|
...def.props,
|
|
560
|
-
// Pass
|
|
561
|
-
isOpen: isOpen,
|
|
695
|
+
// Pass closer to component
|
|
562
696
|
close: () => {
|
|
563
697
|
isOpen = false;
|
|
564
698
|
},
|