diff --git a/package-lock.json b/package-lock.json index f6c3c65..9e1026a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "adno", - "version": "1.0.0", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "adno", - "version": "1.0.0", + "version": "1.0.2", "license": "MIT", "dependencies": { "@fortawesome/fontawesome-svg-core": "6.7.2", diff --git a/src/components/AdnoEditor/AdnoEditor.js b/src/components/AdnoEditor/AdnoEditor.js index 2c45c37..9d429c9 100644 --- a/src/components/AdnoEditor/AdnoEditor.js +++ b/src/components/AdnoEditor/AdnoEditor.js @@ -14,12 +14,16 @@ import "./AdnoEditor.css"; // Add translations import { withTranslation } from "react-i18next"; import { projectDB } from "../../services/db"; +import AdnoNavigator from '../AdnoNavigator/AdnoNavigator'; class AdnoEditor extends Component { constructor(props) { super(props); this.state = { - isMovingItem: false + isMovingItem: false, + imageRatio: null, + navigatorLayout: null, + viewerReady: false } } @@ -51,7 +55,7 @@ class AdnoEditor extends Component { OpenSeadragon.setString("Tooltips.RotateRight", this.props.t('editor.rotate_right')); OpenSeadragon.setString("Tooltips.Flip", this.props.t('editor.flip')); - this.AdnoAnnotorious = OpenSeadragon.Annotorious(OpenSeadragon({ + this.openSeadragon = OpenSeadragon({ id: 'openseadragon1', tileSources: tileSources, prefixUrl: 'https://cdn.jsdelivr.net/gh/Benomrans/openseadragon-icons@main/images/', @@ -59,7 +63,21 @@ class AdnoEditor extends Component { toolbar: "toolbar-osd", showRotationControl: this.props.rotation, showFullPageControl: false, - }), { + }); + + this.openSeadragon.addOnceHandler('open', () => { + const item = this.openSeadragon.world.getItemAt(0); + if (item) { + const size = item.getContentSize(); + const ratio = size.y / size.x; + let layout = 'bottom-right'; + if (ratio < 0.30) layout = 'bottom-center'; + else if (ratio > 3.33) layout = 'right-vertical'; + this.setState({ imageRatio: ratio, navigatorLayout: layout, viewerReady: true }); + } + }); + + this.AdnoAnnotorious = OpenSeadragon.Annotorious(this.openSeadragon, { locale: 'auto', drawOnSingleClick: true, allowEmpty: true, @@ -174,9 +192,19 @@ class AdnoEditor extends Component { render() { return (
-
-
-
+
+
+
+
+
+ {this.props.showNavigator && this.state.viewerReady && ( + + )}
{ this.state.isMovingItem && diff --git a/src/components/AdnoEmbed/AdnoEmbed.js b/src/components/AdnoEmbed/AdnoEmbed.js index 1ff1cd1..cb727cb 100644 --- a/src/components/AdnoEmbed/AdnoEmbed.js +++ b/src/components/AdnoEmbed/AdnoEmbed.js @@ -26,6 +26,7 @@ import { withTranslation } from "react-i18next"; // Import Style import "./AdnoEmbed.css"; import { extractIIIFContent } from "./IIIFHelper"; +import AdnoNavigator from '../AdnoNavigator/AdnoNavigator'; class AdnoEmbed extends Component { constructor(props) { @@ -39,7 +40,11 @@ class AdnoEmbed extends Component { currentTrack: undefined, soundMode: 'no_sound', audioContexts: [], - hasInteracted: false + hasInteracted: false, + imageRatio: null, + navigatorLayout: null, + viewerReady: false, + navigatorImgUrl: null }; } @@ -203,6 +208,18 @@ class AdnoEmbed extends Component { prefixUrl: "https://cdn.jsdelivr.net/gh/Benomrans/openseadragon-icons@main/images/", }); + this.openSeadragon.addOnceHandler('open', () => { + const item = this.openSeadragon.world.getItemAt(0); + if (item) { + const size = item.getContentSize(); + const ratio = size.y / size.x; + let layout = 'bottom-right'; + if (ratio < 0.30) layout = 'bottom-center'; + else if (ratio > 3.33) layout = 'right-vertical'; + this.setState({ imageRatio: ratio, navigatorLayout: layout, viewerReady: true }); + } + }); + OpenSeadragon.setString("Tooltips.FullPage", this.props.t('editor.fullpage')); OpenSeadragon.setString("Tooltips.Home", this.props.t('editor.home')); OpenSeadragon.setString("Tooltips.ZoomIn", this.props.t('editor.zoom_in')); @@ -852,6 +869,7 @@ class AdnoEmbed extends Component { } : [imported_project.source]; + this.setState({ navigatorImgUrl: imported_project.source }); this.displayViewer(tileSources, annos); // Add annotations to the state @@ -980,7 +998,7 @@ class AdnoEmbed extends Component { url, }; - this.setState({ isLoaded: true }); + this.setState({ isLoaded: true, navigatorImgUrl: url }); this.displayViewer(tileSources, []); } @@ -1017,7 +1035,16 @@ class AdnoEmbed extends Component {
return ( -
+
+ + {this.state.showNavigator && this.state.viewerReady && ( + + )} { this.state.selectedAnno && this.state.selectedAnno.body && diff --git a/src/components/AdnoNavigator/AdnoNavigator.css b/src/components/AdnoNavigator/AdnoNavigator.css new file mode 100644 index 0000000..16a315e --- /dev/null +++ b/src/components/AdnoNavigator/AdnoNavigator.css @@ -0,0 +1,27 @@ +/* AdnoNavigator.css */ +.adno-navigator { + position: absolute; + cursor: crosshair; + border-radius: 4px; + border: 1px solid rgba(255, 255, 255, 0.25); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5); + z-index: 100; + background: #111; +} + +.adno-navigator--bottom-right { + bottom: 12px; + right: 12px; +} + +.adno-navigator--bottom-center { + bottom: 0; + left: 50%; + transform: translateX(-50%); +} + +.adno-navigator--right-vertical { + right: 12px; + top: calc(50% + 58px); + transform: translateY(-50%); +} \ No newline at end of file diff --git a/src/components/AdnoNavigator/AdnoNavigator.js b/src/components/AdnoNavigator/AdnoNavigator.js new file mode 100644 index 0000000..fb4d374 --- /dev/null +++ b/src/components/AdnoNavigator/AdnoNavigator.js @@ -0,0 +1,161 @@ +import { Component, createRef } from "react"; +import "./AdnoNavigator.css"; + +class AdnoNavigator extends Component { + constructor(props) { + super(props); + this.canvasRef = createRef(); + this.isDragging = false; + this._bgImage = null; + this._timer = null; + } + + componentDidMount() { + const { viewer } = this.props; + + this.loadThumbnail(); + this._timer = setTimeout(() => this.draw(), 500); + viewer.addHandler('animation', this.draw); + viewer.addHandler('resize', this.draw); + } + + componentWillUnmount() { + const { viewer } = this.props; + + clearTimeout(this._timer); + viewer.removeHandler('animation', this.draw); + viewer.removeHandler('resize', this.draw); + } + + loadThumbnail = () => { + const { imgUrl } = this.props; + if (!imgUrl) return; + + this._bgImage = new Image(); + this._bgImage.crossOrigin = 'anonymous'; + this._bgImage.src = imgUrl.endsWith('/info.json') + ? `${imgUrl.replace('/info.json', '')}/full/!512,512/0/default.jpg` + : imgUrl; + this._bgImage.onload = () => this.draw(); + } + + draw = () => { + const { viewer } = this.props; + const canvas = this.canvasRef.current; + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const W = canvas.width; + const H = canvas.height; + + ctx.clearRect(0, 0, W, H); + + const item = viewer.world.getItemAt(0); + if (!item) return; + + ctx.fillStyle = 'rgba(0, 0, 0, 0.85)'; + ctx.fillRect(0, 0, W, H); + + if (this._bgImage?.complete && this._bgImage.naturalWidth) { + ctx.drawImage(this._bgImage, 0, 0, W, H); + } + + this.drawViewportRect(ctx, item, W, H); + } + + drawViewportRect = (ctx, item, W, H) => { + const { viewer } = this.props; + const imgBounds = item.getBounds(); + const viewBounds = viewer.viewport.getBounds(true); + + const toCanvas = (x, y) => ({ + x: ((x - imgBounds.x) / imgBounds.width) * W, + y: ((y - imgBounds.y) / imgBounds.height) * H, + }); + + const tl = toCanvas(viewBounds.x, viewBounds.y); + const br = toCanvas( + viewBounds.x + viewBounds.width, + viewBounds.y + viewBounds.height + ); + + const rx = Math.max(0, tl.x); + const ry = Math.max(0, tl.y); + const rw = Math.min(W, br.x) - rx; + const rh = Math.min(H, br.y) - ry; + + ctx.fillStyle = 'rgba(0, 0, 0, 0.4)'; + ctx.fillRect(0, 0, W, ry); + ctx.fillRect(0, ry + rh, W, H - ry - rh); + ctx.fillRect(0, ry, rx, rh); + ctx.fillRect(rx + rw, ry, W - rx - rw, rh); + + ctx.strokeStyle = 'rgba(255, 220, 50, 0.9)'; + ctx.lineWidth = 2; + ctx.strokeRect(rx, ry, rw, rh); + } + + panTo = (e) => { + const { viewer } = this.props; + const canvas = this.canvasRef.current; + const item = viewer.world.getItemAt(0); + if (!canvas || !item) return; + + const rect = canvas.getBoundingClientRect(); + const imgBounds = item.getBounds(); + + const vpX = imgBounds.x + ((e.clientX - rect.left) / canvas.width) * imgBounds.width; + const vpY = imgBounds.y + ((e.clientY - rect.top) / canvas.height) * imgBounds.height; + + viewer.viewport.panTo(new OpenSeadragon.Point(vpX, vpY), false); + } + + handleMouseDown = (e) => { this.isDragging = true; this.panTo(e); } + handleMouseMove = (e) => { if (this.isDragging) this.panTo(e); } + handleMouseUp = () => { this.isDragging = false; } + + computeSize = () => { + const { viewer, imageRatio, layout } = this.props; + const container = viewer?.container; + const cw = container?.clientWidth || 800; + const ch = container?.clientHeight || 600; + + const fit = (w, h, maxW, maxH) => { + if (h > maxH) { h = Math.round(maxH); w = Math.round(h / imageRatio); } + if (w > maxW) { w = Math.round(maxW); h = Math.round(w * imageRatio); } + return { width: w, height: h }; + }; + + if (layout === 'bottom-center') { + const w = Math.round(cw * 0.5); + return fit(w, Math.round(w * imageRatio), cw * 0.5, ch * 0.4); + } + if (layout === 'right-vertical') { + const h = Math.round(ch * 0.8); + return fit(Math.round(h / imageRatio), h, cw * 0.2, ch * 0.8); + } + const w = Math.round(cw * 0.25); + return fit(w, Math.round(w * imageRatio), cw * 0.25, ch * 0.3); + } + + render() { + const { layout } = this.props; + const { width, height } = this.computeSize(); + + return ( + + ); + } +} + +export default AdnoNavigator; + diff --git a/src/components/OpenView/OpenView.js b/src/components/OpenView/OpenView.js index f3bdddc..ca86aef 100644 --- a/src/components/OpenView/OpenView.js +++ b/src/components/OpenView/OpenView.js @@ -2,18 +2,15 @@ import { Component } from "react"; import { withRouter } from "react-router-dom"; import parse from 'html-react-parser'; -// Import FontAwesome import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - import { faMagnifyingGlassMinus, faPlay, faPause, faEye, faEyeSlash, faArrowRight, faArrowLeft, faExpand, faRotate, faQuestion, faVolumeOff, faVolumeHigh, faCircleInfo, faExternalLink } from "@fortawesome/free-solid-svg-icons"; - -// Import utils import { getEye } from "../../Utils/utils"; -// Import CSS import "./OpenView.css"; import { withTranslation } from "react-i18next"; +import AdnoNavigator from '../AdnoNavigator/AdnoNavigator' + class OpenView extends Component { constructor(props) { super(props); @@ -25,7 +22,11 @@ class OpenView extends Component { isAnnotationsVisible: true, currentTrack: undefined, soundMode: this.props.soundMode, - audioContexts: [] + audioContexts: [], + + imageRatio: null, + navigatorLayout: null, + viewerReady: false } } @@ -53,13 +54,31 @@ class OpenView extends Component { this.openSeadragon = OpenSeadragon({ id: 'adno-osd', homeButton: "home-button", - showNavigator: this.props.showNavigator, + // showNavigator: this.props.showNavigator, + showNavigator: false, tileSources: tileSources, prefixUrl: 'https://openseadragon.github.io/openseadragon/images/', crossOriginPolicy: 'Anonymous', ajaxWithCredentials: false }) + this.openSeadragon.addOnceHandler('open', () => { + const item = this.openSeadragon.world.getItemAt(0); + if (item) { + const size = item.getContentSize(); + const ratio = size.y / size.x; + let layout = 'bottom-right'; + if (ratio < 0.30) layout = 'bottom-center'; + else if (ratio > 3.33) layout = 'right-vertical'; + + this.setState({ + imageRatio: ratio, + navigatorLayout: layout, + viewerReady: true // 👈 maintenant OSD existe vraiment + }); + } + }); + OpenSeadragon.setString("Tooltips.FullPage", this.props.t('editor.fullpage')); OpenSeadragon.setString("Tooltips.Home", this.props.t('editor.home')); OpenSeadragon.setString("Tooltips.ZoomIn", this.props.t('editor.zoom_in')); @@ -624,15 +643,6 @@ class OpenView extends Component { if (prevProps.showEyes !== this.props.showEyes) setTimeout(this.freeMode, 1000) - // Check if the user toggled the navigator on/off - if (this.props.showNavigator !== prevProps.showNavigator && this.openSeadragon?.navigator) { - if (this.props.showNavigator) { - document.getElementById(this.openSeadragon.navigator.id).style.display = 'block'; - } else { - document.getElementById(this.openSeadragon.navigator.id).style.display = 'none'; - } - - } } } @@ -746,6 +756,14 @@ class OpenView extends Component { return (
+ {this.props.showNavigator && this.openSeadragon && this.state.viewerReady && ( + + )} { this.state.fullScreenEnabled && this.props.selectedAnno && this.props.selectedAnno.body && this.getAnnotationHTMLBody(this.props.selectedAnno) diff --git a/src/components/Project/Project.js b/src/components/Project/Project.js index 55fb53a..c7eac95 100644 --- a/src/components/Project/Project.js +++ b/src/components/Project/Project.js @@ -128,6 +128,8 @@ const Project = ({ editMode }) => { }) : annotations; + console.log(settings) + if (!state.selectedProject) return
{ }))} changeSelectedAnno={(anno) => setState(prev => ({ ...prev, selectedAnnotation: anno }))} rotation={settings.rotation} + showNavigator={settings.showNavigator} /> ) : (