diff --git a/NOTICE b/NOTICE index d1fb5d644..ef61c7b0b 100644 --- a/NOTICE +++ b/NOTICE @@ -6138,8 +6138,8 @@ License: Apache-2.0 (https://github.com/prometheus/client_model/blob/v0.6.2/LICE -------------------------------------------------------------------------------- Package: github.com/prometheus/common -Version: v0.68.1 -License: Apache-2.0 (https://github.com/prometheus/common/blob/v0.68.1/LICENSE) +Version: v0.67.5 +License: Apache-2.0 (https://github.com/prometheus/common/blob/v0.67.5/LICENSE) Apache License Version 2.0, January 2004 @@ -7217,8 +7217,8 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- Package: github.com/yalue/onnxruntime_go -Version: v1.31.0 -License: MIT (https://github.com/yalue/onnxruntime_go/blob/v1.31.0/LICENSE) +Version: v1.30.1 +License: MIT (https://github.com/yalue/onnxruntime_go/blob/v1.30.1/LICENSE) Copyright (c) 2023 Nathan Otterness @@ -8851,6 +8851,214 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- +Package: go.yaml.in/yaml/v2 +Version: v2.4.4 +License: Apache-2.0 (https://github.com/yaml/go-yaml/blob/v2.4.4/LICENSE) + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +-------------------------------------------------------------------------------- + Package: go4.org/media/heif Version: v0.0.0-20260112195520-a5071408f32f License: Apache-2.0 (https://github.com/go4org/go4/blob/a5071408f32f/LICENSE) @@ -9061,8 +9269,8 @@ License: Apache-2.0 (https://github.com/go4org/go4/blob/a5071408f32f/LICENSE) -------------------------------------------------------------------------------- Package: golang.org/x/crypto -Version: v0.53.0 -License: BSD-3-Clause (https://cs.opensource.google/go/x/crypto/+/v0.53.0:LICENSE) +Version: v0.52.0 +License: BSD-3-Clause (https://cs.opensource.google/go/x/crypto/+/v0.52.0:LICENSE) Copyright 2009 The Go Authors. @@ -9095,8 +9303,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- Package: golang.org/x/image -Version: v0.42.0 -License: BSD-3-Clause (https://cs.opensource.google/go/x/image/+/v0.42.0:LICENSE) +Version: v0.41.0 +License: BSD-3-Clause (https://cs.opensource.google/go/x/image/+/v0.41.0:LICENSE) Copyright 2009 The Go Authors. @@ -9129,8 +9337,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- Package: golang.org/x/mod/semver -Version: v0.37.0 -License: BSD-3-Clause (https://cs.opensource.google/go/x/mod/+/v0.37.0:LICENSE) +Version: v0.36.0 +License: BSD-3-Clause (https://cs.opensource.google/go/x/mod/+/v0.36.0:LICENSE) Copyright 2009 The Go Authors. @@ -9163,8 +9371,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- Package: golang.org/x/net -Version: v0.56.0 -License: BSD-3-Clause (https://cs.opensource.google/go/x/net/+/v0.56.0:LICENSE) +Version: v0.55.0 +License: BSD-3-Clause (https://cs.opensource.google/go/x/net/+/v0.55.0:LICENSE) Copyright 2009 The Go Authors. @@ -9231,8 +9439,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- Package: golang.org/x/sync/errgroup -Version: v0.21.0 -License: BSD-3-Clause (https://cs.opensource.google/go/x/sync/+/v0.21.0:LICENSE) +Version: v0.20.0 +License: BSD-3-Clause (https://cs.opensource.google/go/x/sync/+/v0.20.0:LICENSE) Copyright 2009 The Go Authors. @@ -9265,8 +9473,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- Package: golang.org/x/sys -Version: v0.46.0 -License: BSD-3-Clause (https://cs.opensource.google/go/x/sys/+/v0.46.0:LICENSE) +Version: v0.45.0 +License: BSD-3-Clause (https://cs.opensource.google/go/x/sys/+/v0.45.0:LICENSE) Copyright 2009 The Go Authors. @@ -9299,8 +9507,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- Package: golang.org/x/text -Version: v0.38.0 -License: BSD-3-Clause (https://cs.opensource.google/go/x/text/+/v0.38.0:LICENSE) +Version: v0.37.0 +License: BSD-3-Clause (https://cs.opensource.google/go/x/text/+/v0.37.0:LICENSE) Copyright 2009 The Go Authors. diff --git a/frontend/CODEMAP.md b/frontend/CODEMAP.md index 64f6d34e0..d0b079f18 100644 --- a/frontend/CODEMAP.md +++ b/frontend/CODEMAP.md @@ -18,7 +18,7 @@ Directory Map (src) - `src/app.js` — app bootstrap: creates Vue app, installs Vuetify + plugins, configures router, mounts to `#app` - `src/app/routes.js` — all route definitions (guards, titles, meta) - `src/app/session.js` — `$config` and `$session` singletons wired from server-provided `window.__CONFIG__` and storage -- `src/common/*` — framework-agnostic helpers: `$api` (Axios), `$notify`, `$view`, `$event` (PubSub), i18n (`gettext`), util, fullscreen, map utils, websocket +- `src/common/*` — framework-agnostic helpers: `$api` (Axios), `$notify`, `$view`, `$event` (PubSub), i18n (`gettext`), util, fullscreen, map utils, websocket, `sphere.js` (lazy-loaded 360° viewer wrapper) - `src/component/*` — Vue components; `src/component/components.js` registers global components - `src/page/*` — route views (Albums, Photos, Places, Settings, Admin, Discover, Help, Login, etc.) - `src/model/*` — REST models; base `Rest` class (`model/rest.js`) wraps Axios CRUD for collections and entities diff --git a/frontend/package.json b/frontend/package.json index 77943f3be..bab6ec1b0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -87,6 +87,9 @@ "minimist": "^1.2.8", "node-storage-shim": "^2.0.1", "passive-events-support": "^1.1.0", + "@photo-sphere-viewer/core": "^5.14.1", + "@photo-sphere-viewer/equirectangular-video-adapter": "^5.14.1", + "@photo-sphere-viewer/video-plugin": "^5.14.1", "photoswipe": "^5.4.4", "playwright": "^1.61.0", "postcss": "^8.5.15", diff --git a/frontend/src/common/sphere.js b/frontend/src/common/sphere.js new file mode 100644 index 000000000..96d601ff6 --- /dev/null +++ b/frontend/src/common/sphere.js @@ -0,0 +1,110 @@ +/* + +Copyright (c) 2018 - 2026 PhotoPrism UG. All rights reserved. + + This program is free software: you can redistribute it and/or modify + it under Version 3 of the GNU Affero General Public License (the "AGPL"): + + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + The AGPL is supplemented by our Trademark and Brand Guidelines, + which describe how our Brand Assets may be used: + + +Feel free to send an email to hello@photoprism.app if you have questions, +want to support our work, or just want to say hello. + +Additional information can be found in our Developer Guide: + + +*/ + +import * as media from "common/media"; + +// EQUIRECTANGULAR_RATIO_TOLERANCE bounds how far a video frame may deviate from the +// 2:1 equirectangular aspect ratio and still be treated as 360° media. +const EQUIRECTANGULAR_RATIO_TOLERANCE = 0.15; + +// is360Equirectangular reports whether a slide/thumb model is equirectangular 360° +// content that should open in the sphere viewer. An explicit "equirectangular" +// projection is authoritative and any other non-empty projection (cubemap, …) is +// never 360°. Many 360° videos carry no projection tag PhotoPrism can read, so a +// panorama-flagged video with no projection falls back to equirectangular's defining +// 2:1 frame — this keeps genuine spherical videos opening while cubemap (4:3, 6:1) +// and ultrawide (~2.35:1) clips stay in the flat player. +export function is360Equirectangular(model) { + const projection = (model?.Projection || "").toLowerCase(); + if (projection === "equirectangular") { + return true; + } + if (projection) { + return false; + } + const isVideo = model?.Type === media.Video || model?.Type === media.Animated; + if (!isVideo || model?.Panorama !== true) { + return false; + } + const w = Number(model?.Width); + const h = Number(model?.Height); + return w > 0 && h > 0 && Math.abs(w / h - 2) <= EQUIRECTANGULAR_RATIO_TOLERANCE; +} + +// createSphereViewer mounts a Photo Sphere Viewer instance for an equirectangular photo or video. +// The renderer (and its ThreeJS dependency) is dynamic-imported on first call so the base bundle +// is unaffected when no 360° media is opened. +export async function createSphereViewer(container, src, opts = {}) { + const [coreMod, videoMod, videoAdapterMod] = await Promise.all([ + import(/* webpackChunkName: "sphere-viewer" */ "@photo-sphere-viewer/core"), + opts.isVideo + ? import(/* webpackChunkName: "sphere-viewer" */ "@photo-sphere-viewer/video-plugin") + : Promise.resolve(null), + opts.isVideo + ? import(/* webpackChunkName: "sphere-viewer" */ "@photo-sphere-viewer/equirectangular-video-adapter") + : Promise.resolve(null), + ]); + + await import(/* webpackChunkName: "sphere-viewer" */ "@photo-sphere-viewer/core/index.css"); + + if (opts.isVideo) { + await import(/* webpackChunkName: "sphere-viewer" */ "@photo-sphere-viewer/video-plugin/index.css"); + return new coreMod.Viewer({ + container, + adapter: [videoAdapterMod.EquirectangularVideoAdapter, { muted: !!opts.muted, autoplay: false }], + panorama: { source: src }, + plugins: [videoMod.VideoPlugin], + navbar: false, + keyboard: "always", + defaultYaw: 0, + defaultPitch: 0, + }); + } + + return new coreMod.Viewer({ + container, + panorama: src, + navbar: false, + keyboard: "always", + defaultYaw: 0, + defaultPitch: 0, + }); +} + +// findSphereVideoElement returns the underlying HTMLVideoElement that PSV's +// EquirectangularVideoAdapter uses as a WebGL texture source. The adapter does +// not insert the element into the DOM, so we read it off the adapter instance +// the viewer exposes after the panorama has finished loading. +export function findSphereVideoElement(viewer) { + return viewer?.adapter?.video || null; +} + +// destroySphereViewer tears down a viewer instance, releasing its WebGL context and textures. +// Safe to call on null or an already-destroyed viewer. +export function destroySphereViewer(viewer) { + if (viewer && typeof viewer.destroy === "function") { + viewer.destroy(); + } +} diff --git a/frontend/src/component/lightbox.vue b/frontend/src/component/lightbox.vue index 274fde9ab..cf69629be 100644 --- a/frontend/src/component/lightbox.vue +++ b/frontend/src/component/lightbox.vue @@ -128,6 +128,7 @@ import * as media from "common/media"; import { getAppSessionStorage, getAppStorage } from "common/storage"; import * as contexts from "options/contexts"; import { $faceMarkers } from "common/face-markers"; +import { createSphereViewer, destroySphereViewer, findSphereVideoElement, is360Equirectangular } from "common/sphere"; const VIDEO_EVENT_TYPES = [ "loadstart", @@ -150,6 +151,10 @@ const VIDEO_EVENT_TYPES = [ const VIDEO_REMOTE_EVENT_TYPES = ["connect", "connecting", "disconnect"]; +// Max pointer travel (px) for a press on a 360° sphere to still count as a tap +// rather than a pan, used to toggle the lightbox controls on touch devices. +const SPHERE_TAP_SLOP = 10; + import PLightboxMenu from "component/lightbox/menu.vue"; import PLightboxSidebar from "component/lightbox/sidebar.vue"; import { Marker } from "model/marker"; @@ -659,6 +664,27 @@ export default { loading: false, }; + // Route equirectangular 360° media to the lazy-loaded sphere viewer before the video branch, + // since panoramic videos are also Playable and would otherwise fall through. is360Equirectangular + // routes on the equirectangular projection when present, and falls back to the 2:1 frame for + // panorama-flagged videos that carry no readable projection tag — cubemap and ultrawide clips + // stay flat. See common/sphere.js for the discriminator. + const isEquirect = is360Equirectangular(model); + if (isEquirect && model?.Hash) { + const isVideo = model?.Type === media.Video || model?.Type === media.Animated; + const src = isVideo ? this.$util.videoUrl(model.Hash, model?.Codec, model?.Mime) : this.$util.thumb(model.Thumbs, 8192, 4096).src; + return { + type: "html", + html: `
`, + model: model, + isSphere: true, + isVideo: isVideo, + src: src, + msrc: img.src, + loading: true, + }; + } + // Check if content is playable and return the data needed to render it in "contentLoad". if (model?.Playable && model?.Hash) { /* @@ -694,14 +720,157 @@ export default { return img; }, isContentZoomable(isContentZoomable, content) { + if (content.data?.isSphere) { + return false; + } if (content.data?.model?.Type === media.Live) { isContentZoomable = true; } return isContentZoomable; }, + // slideZoomable reports whether a slide supports the flat zoom button and + // pinch/click-to-zoom. 360° sphere slides own their zoom/pan, and videos and + // animations are never flatly zoomable; the result drives the `.is-zoomable` + // class that shows or hides the PhotoSwipe zoom button. + slideZoomable(data) { + if (data?.isSphere) { + return false; + } + return data?.model?.Type !== media.Video && data?.model?.Type !== media.Animated; + }, + // trapSphereGestures stops PhotoSwipe's swipe/zoom gesture detection from running + // while the user interacts with a 360° sphere. It swallows pointer + wheel events + // on the sphere container during the bubble phase, before they reach PhotoSwipe's + // listeners on the scroll wrapper above it. + // + // Touch events are deliberately NOT trapped: PhotoSwipe binds pointer events on our + // browser baseline (it only falls back to touch when PointerEvent is unavailable), + // while Photo Sphere Viewer drives panning from touchmove/touchend listeners bound on + // `window`. A bubble-phase stopPropagation here would keep those events from reaching + // PSV and break 360° panning on touch devices. UI buttons (close, prev/next, sidebar) + // live on the PhotoSwipe wrapper outside this container and stay clickable. + // + // Trapping the pointer events also swallows PhotoSwipe's tap detection, which is what + // reveals the prev/next arrows and top bar on touch devices. To restore that, a short + // press without a pan toggles the controls here. Only touch/pen taps do so — mouse + // users still get the controls via mousemove, so desktop behavior is unchanged. + trapSphereGestures(el) { + const stop = (e) => e.stopPropagation(); + let tapX = 0; + let tapY = 0; + let tapping = false; + el.addEventListener( + "pointerdown", + (e) => { + e.stopPropagation(); + tapping = e.pointerType !== "mouse"; + tapX = e.clientX; + tapY = e.clientY; + }, + { capture: false } + ); + el.addEventListener( + "pointerup", + (e) => { + e.stopPropagation(); + if (tapping && Math.abs(e.clientX - tapX) < SPHERE_TAP_SLOP && Math.abs(e.clientY - tapY) < SPHERE_TAP_SLOP) { + this.toggleControls(); + } + tapping = false; + }, + { capture: false } + ); + el.addEventListener( + "pointercancel", + (e) => { + e.stopPropagation(); + tapping = false; + }, + { capture: false } + ); + ["pointermove", "wheel"].forEach((type) => { + el.addEventListener(type, stop, { capture: false }); + }); + }, + // setSphereClass toggles the `pswp--sphere` marker on the PhotoSwipe root element so + // CSS can keep the prev/next arrows reachable on touch devices for 360° slides, where + // swipe is captured for panning instead of navigation. PhotoSwipe hides the arrows on + // touch by default, leaving such slides with no way to switch photos otherwise. + setSphereClass(enabled) { + const el = this.pswp()?.element; + if (el?.classList) { + el.classList.toggle("pswp--sphere", enabled); + } + }, onContentLoad(ev) { const { content } = ev; + if (content.data?.isSphere) { + ev.preventDefault(); + + try { + const sphereEl = document.createElement("div"); + sphereEl.setAttribute("class", "pswp__media pswp__media--sphere"); + sphereEl.style.width = "100%"; + sphereEl.style.height = "100%"; + sphereEl.style.touchAction = "none"; + this.trapSphereGestures(sphereEl); + + content.element = sphereEl; + content.state = "loading"; + content.data.loading = false; + content.onLoaded(); + + createSphereViewer(sphereEl, content.data.src, { isVideo: content.data.isVideo, muted: this.muted }) + .then((viewer) => { + content.data.sphereViewer = viewer; + // For 360° videos, expose the underlying