From c6478a195da906e9bc0107a4ebf9e3f1ff67d2dd Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Sun, 19 Mar 2023 03:09:28 -0700 Subject: [PATCH] ps: add typings Signed-off-by: Varun Patil --- package-lock.json | 31 +++++++++ package.json | 1 + src/components/viewer/PsImage.ts | 16 ++--- src/components/viewer/PsLivePhoto.ts | 15 +++-- src/components/viewer/PsVideo.ts | 96 +++++++++++++++------------- src/components/viewer/Viewer.vue | 13 ++-- src/components/viewer/types.ts | 15 +++++ 7 files changed, 121 insertions(+), 66 deletions(-) create mode 100644 src/components/viewer/types.ts diff --git a/package-lock.json b/package-lock.json index 86f803c1..45cf86fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "@nextcloud/webpack-vue-config": "^5.4.0", "@playwright/test": "^1.31.2", "@types/url-parse": "^1.4.8", + "@types/videojs-contrib-quality-levels": "^2.0.1", "playwright": "^1.31.2", "ts-loader": "^9.4.2", "typescript": "^4.9.5", @@ -2534,6 +2535,21 @@ "integrity": "sha512-zqqcGKyNWgTLFBxmaexGUKQyWqeG7HjXj20EuQJSJWwXe54BjX0ihIo5cJB9yAQzH8dNugJ9GvkBYMjPXs/PJw==", "dev": true }, + "node_modules/@types/video.js": { + "version": "7.3.51", + "resolved": "https://registry.npmjs.org/@types/video.js/-/video.js-7.3.51.tgz", + "integrity": "sha512-xLlt/ZfCuWYBvG2MRn018RvaEplcK6dI63aOiVUeeAWFyjx3Br1hL749ndFgbrvNdY4m9FoHG1FQ/PB6IpfSAQ==", + "dev": true + }, + "node_modules/@types/videojs-contrib-quality-levels": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-2.0.1.tgz", + "integrity": "sha512-a7vNjolI9zG269Ks8y6JC/Eut+BXex0/1haB8c2J7RCIn365EdTxgiT0udG2ObaCkQqZsa2+4KS7ZE1HMo113w==", + "dev": true, + "dependencies": { + "@types/video.js": "*" + } + }, "node_modules/@types/ws": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", @@ -13244,6 +13260,21 @@ "integrity": "sha512-zqqcGKyNWgTLFBxmaexGUKQyWqeG7HjXj20EuQJSJWwXe54BjX0ihIo5cJB9yAQzH8dNugJ9GvkBYMjPXs/PJw==", "dev": true }, + "@types/video.js": { + "version": "7.3.51", + "resolved": "https://registry.npmjs.org/@types/video.js/-/video.js-7.3.51.tgz", + "integrity": "sha512-xLlt/ZfCuWYBvG2MRn018RvaEplcK6dI63aOiVUeeAWFyjx3Br1hL749ndFgbrvNdY4m9FoHG1FQ/PB6IpfSAQ==", + "dev": true + }, + "@types/videojs-contrib-quality-levels": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-2.0.1.tgz", + "integrity": "sha512-a7vNjolI9zG269Ks8y6JC/Eut+BXex0/1haB8c2J7RCIn365EdTxgiT0udG2ObaCkQqZsa2+4KS7ZE1HMo113w==", + "dev": true, + "requires": { + "@types/video.js": "*" + } + }, "@types/ws": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", diff --git a/package.json b/package.json index 3e39045a..4b415f73 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "@nextcloud/webpack-vue-config": "^5.4.0", "@playwright/test": "^1.31.2", "@types/url-parse": "^1.4.8", + "@types/videojs-contrib-quality-levels": "^2.0.1", "playwright": "^1.31.2", "ts-loader": "^9.4.2", "typescript": "^4.9.5", diff --git a/src/components/viewer/PsImage.ts b/src/components/viewer/PsImage.ts index f167d028..7ba4168b 100644 --- a/src/components/viewer/PsImage.ts +++ b/src/components/viewer/PsImage.ts @@ -1,9 +1,9 @@ import PhotoSwipe from "photoswipe"; -import Slide from "photoswipe/dist/types/slide/slide"; import { isVideoContent } from "./PsVideo"; import { isLiveContent } from "./PsLivePhoto"; import { fetchImage } from "../frame/XImgCache"; +import { PsContent, PsEvent, PsSlide } from "./types"; export default class ImageContentSetup { private loading = 0; @@ -17,11 +17,11 @@ export default class ImageContentSetup { lightbox.addFilter("placeholderSrc", this.placeholderSrc.bind(this)); } - isContentLoading(isLoading: boolean, content: any) { + isContentLoading(isLoading: boolean, content: PsContent) { return isLoading || this.loading > 0; } - onContentLoad(e) { + onContentLoad(e: PsEvent) { if (isVideoContent(e.content) || isLiveContent(e.content)) return; // Insert image throgh XImgCache @@ -29,16 +29,16 @@ export default class ImageContentSetup { e.content.element = this.getXImgElem(e.content, () => e.content.onLoaded()); } - onContentLoadImage(e) { + onContentLoadImage(e: PsEvent) { if (isVideoContent(e.content) || isLiveContent(e.content)) return; e.preventDefault(); } - placeholderSrc(placeholderSrc: string, content: any) { + placeholderSrc(placeholderSrc: string, content: PsContent) { return content.data.msrc || placeholderSrc; } - getXImgElem(content: any, onLoad: () => void): HTMLImageElement { + getXImgElem(content: PsContent, onLoad: () => void): HTMLImageElement { const img = document.createElement("img"); img.classList.add("pswp__img", "ximg"); img.style.visibility = "hidden"; @@ -63,7 +63,7 @@ export default class ImageContentSetup { return img; } - zoomPanUpdate({ slide }: { slide: Slide }) { + zoomPanUpdate({ slide }: { slide: PsSlide }) { if (!slide.data.highSrc || slide.data.highSrcCond !== "zoom") return; if (slide.currZoomLevel >= slide.zoomLevels.secondary) { @@ -78,7 +78,7 @@ export default class ImageContentSetup { } } - loadFullImage(slide: Slide) { + loadFullImage(slide: PsSlide) { if (!slide.data.highSrc) return; // Get ximg element diff --git a/src/components/viewer/PsLivePhoto.ts b/src/components/viewer/PsLivePhoto.ts index 72dc250f..f6ccc04e 100644 --- a/src/components/viewer/PsLivePhoto.ts +++ b/src/components/viewer/PsLivePhoto.ts @@ -1,8 +1,9 @@ import PhotoSwipe from "photoswipe"; import PsImage from "./PsImage"; import * as utils from "../../services/Utils"; +import { PsContent, PsEvent } from "./types"; -export function isLiveContent(content): boolean { +export function isLiveContent(content: PsContent): boolean { // Do not play Live Photo if the slideshow is // playing in full screen mode. if (document.fullscreenElement) { @@ -21,7 +22,7 @@ class LivePhotoContentSetup { } onContentLoad(e) { - const content = e.content; + const content: PsContent = e.content; if (!isLiveContent(content)) return; e.preventDefault(); @@ -50,7 +51,7 @@ class LivePhotoContentSetup { content.element = div; } - onContentActivate({ content }) { + onContentActivate({ content }: { content: PsContent }) { if (isLiveContent(content)) { const video = content.element?.querySelector("video"); if (video) { @@ -60,15 +61,15 @@ class LivePhotoContentSetup { } } - onContentDeactivate({ content }) { + onContentDeactivate({ content }: PsEvent) { if (isLiveContent(content)) { content.element?.querySelector("video")?.pause(); } } - onContentDestroy(e) { - if (isLiveContent(e.content)) { - e.content.element?.remove(); + onContentDestroy({ content }: PsEvent) { + if (isLiveContent(content)) { + content.element?.remove(); } } } diff --git a/src/components/viewer/PsVideo.ts b/src/components/viewer/PsVideo.ts index c76365a3..319e1309 100644 --- a/src/components/viewer/PsVideo.ts +++ b/src/components/viewer/PsVideo.ts @@ -1,12 +1,27 @@ import PhotoSwipe from "photoswipe"; import { loadState } from "@nextcloud/initial-state"; -import axios from "@nextcloud/axios"; import { showError } from "@nextcloud/dialogs"; import { translate as t } from "@nextcloud/l10n"; import { getCurrentUser } from "@nextcloud/auth"; +import axios from "@nextcloud/axios"; import { API } from "../../services/API"; -import { IPhoto } from "../../types"; +import { PsContent, PsEvent, PsSlide } from "./types"; + +import Player from "video.js/dist/types/player"; +import { QualityLevelList } from "videojs-contrib-quality-levels"; + +type VideoContent = PsContent & { + videoElement: HTMLVideoElement; + videojs: Player & { + qualityLevels?: () => QualityLevelList; + }; + plyr: globalThis.Plyr; +}; + +type PsVideoEvent = PsEvent & { + content: VideoContent; +}; const config_noTranscode = loadState( "memories", @@ -21,16 +36,18 @@ const config_video_default_quality = Number( /** * Check if slide has video content - * - * @param {Slide|Content} content Slide or Content object - * @returns Boolean */ -export function isVideoContent(content): boolean { +export function isVideoContent(content: PsSlide | PsContent): boolean { return content?.data?.type === "video"; } class VideoContentSetup { - constructor(lightbox: PhotoSwipe, private options) { + constructor( + lightbox: PhotoSwipe, + private options: { + preventDragOffset: number; + } + ) { this.initLightboxEvents(lightbox); lightbox.on("init", () => { this.initPswpEvents(lightbox); @@ -53,10 +70,6 @@ class VideoContentSetup { "useContentPlaceholder", this.useContentPlaceholder.bind(this) ); - - lightbox.addFilter("domItemData", (itemData, element, linkEl) => { - return itemData; - }); } initPswpEvents(pswp: PhotoSwipe) { @@ -101,22 +114,19 @@ class VideoContentSetup { }); pswp.on("close", () => { - if (isVideoContent(pswp.currSlide.content)) { - // prevent more requests - this.destroyVideo(pswp.currSlide.content); - } + this.destroyVideo(pswp.currSlide.content as VideoContent); }); // Prevent closing when video fullscreen is active pswp.on("pointerMove", (e) => { - const plyr: Plyr = (pswp.currSlide.content)?.plyr; + const plyr = (pswp.currSlide.content)?.plyr; if (plyr?.fullscreen.active) { e.preventDefault(); } }); } - getHLSsrc(content: any) { + getHLSsrc(content: VideoContent) { // Get base URL const fileid = content.data.photo.fileid; return { @@ -125,13 +135,13 @@ class VideoContentSetup { }; } - async initVideo(content: any) { + async initVideo(content: VideoContent) { if (!isVideoContent(content) || content.videojs || !config_videoIsSetup) { return; } // Prevent double loading - content.videojs = {}; + content.videojs = {} as any; // Load videojs scripts if (!globalThis.vidjs) { @@ -142,20 +152,18 @@ class VideoContentSetup { content.videoElement = document.createElement("video"); content.videoElement.className = "video-js"; content.videoElement.setAttribute("poster", content.data.msrc); - if (this.options.videoAttributes) { - for (let key in this.options.videoAttributes) { - content.videoElement.setAttribute( - key, - this.options.videoAttributes[key] || "" - ); - } - } + content.videoElement.setAttribute("preload", "none"); + content.videoElement.setAttribute("controls", ""); + content.videoElement.setAttribute("playsinline", ""); // Add the video element to the actual container content.element.appendChild(content.videoElement); // Create hls sources if enabled - let sources: any[] = []; + const sources: { + src: string; + type: string; + }[] = []; if (!config_noTranscode) { sources.push(this.getHLSsrc(content)); @@ -239,7 +247,7 @@ class VideoContentSetup { } } - destroyVideo(content: any) { + destroyVideo(content: VideoContent) { if (isVideoContent(content)) { content.videojs?.dispose?.(); content.videojs = null; @@ -256,7 +264,7 @@ class VideoContentSetup { } } - initPlyr(content: any) { + initPlyr(content: VideoContent) { if (content.plyr) return; content.videoElement = content.videojs?.el()?.querySelector("video"); @@ -323,7 +331,7 @@ class VideoContentSetup { } // Set the source to the original video - if (content.videojs.src().includes("m3u8")) { + if (content.videojs.src(undefined).includes("m3u8")) { content.videojs.src({ src: content.data.src, type: "video/mp4", @@ -332,7 +340,7 @@ class VideoContentSetup { return; } else { // Set source to HLS - if (!content.videojs.src().includes("m3u8")) { + if (!content.videojs.src(undefined).includes("m3u8")) { content.videojs.src(this.getHLSsrc(content)); } } @@ -402,16 +410,16 @@ class VideoContentSetup { } } - updateRotation(content: any, val?: number): boolean { + updateRotation(content: VideoContent, val?: number): boolean { if (!content.videojs) return; content.videoElement = content.videojs.el()?.querySelector("video"); if (!content.videoElement) return; - const photo: IPhoto = content.data.photo; + const photo = content.data.photo; const exif = photo.imageInfo?.exif; const rotation = val ?? Number(exif?.Rotation || 0); - const shouldRotate = content.videojs?.src().includes("m3u8"); + const shouldRotate = content.videojs?.src(undefined).includes("m3u8"); if (rotation && shouldRotate) { let transform = `rotate(${rotation}deg)`; @@ -437,7 +445,7 @@ class VideoContentSetup { return false; } - onContentDestroy({ content }) { + onContentDestroy({ content }: PsVideoEvent) { this.destroyVideo(content); } @@ -466,25 +474,25 @@ class VideoContentSetup { } } - isContentZoomable(isZoomable: boolean, content) { + isContentZoomable(isZoomable: boolean, content: PsContent) { return !isVideoContent(content) && isZoomable; } - isKeepingPlaceholder(keep: boolean, content) { + isKeepingPlaceholder(keep: boolean, content: PsContent) { return isVideoContent(content) || keep; } - onContentActivate({ content }) { + onContentActivate({ content }: PsVideoEvent) { this.initVideo(content); } - onContentDeactivate({ content }) { + onContentDeactivate({ content }: PsVideoEvent) { this.destroyVideo(content); } - onContentLoad(e) { - const content = e.content; - if (!isVideoContent(e.content)) return; + onContentLoad(e: PsVideoEvent) { + const content: PsContent = e.content; + if (!isVideoContent(content)) return; // Stop default content load e.preventDefault(); @@ -508,7 +516,7 @@ class VideoContentSetup { content.onLoaded(); } - useContentPlaceholder(usePlaceholder: boolean, content: any) { + useContentPlaceholder(usePlaceholder: boolean, content: PsContent) { return isVideoContent(content) || usePlaceholder; } } diff --git a/src/components/viewer/Viewer.vue b/src/components/viewer/Viewer.vue index 187bdc15..f2e36ca0 100644 --- a/src/components/viewer/Viewer.vue +++ b/src/components/viewer/Viewer.vue @@ -179,6 +179,7 @@ import { defineComponent } from "vue"; import { IDay, IPhoto, IRow, IRowType } from "../../types"; +import { PsSlide } from "./types"; import UserConfig from "../../mixins/UserConfig"; import NcActions from "@nextcloud/vue/dist/Components/NcActions"; @@ -584,19 +585,17 @@ export default defineComponent({ }); // Video support - this.psVideo = new PsVideo(this.photoswipe, { - videoAttributes: { controls: "", playsinline: "", preload: "none" }, - autoplay: true, + this.psVideo = new PsVideo(this.photoswipe, { preventDragOffset: 40, }); // Image support - this.psImage = new PsImage(this.photoswipe); + this.psImage = new PsImage(this.photoswipe); // Live Photo support this.psLivePhoto = new PsLivePhoto( - this.photoswipe, - this.psImage + this.photoswipe, + this.psImage ); // Patch the close button to stop the slideshow @@ -979,7 +978,7 @@ export default defineComponent({ /** Play the current live photo */ playLivePhoto() { - this.psLivePhoto.onContentActivate(this.photoswipe.currSlide); + this.psLivePhoto.onContentActivate(this.photoswipe.currSlide as PsSlide); }, /** Is the current photo a favorite */ diff --git a/src/components/viewer/types.ts b/src/components/viewer/types.ts new file mode 100644 index 00000000..831ca7a8 --- /dev/null +++ b/src/components/viewer/types.ts @@ -0,0 +1,15 @@ +import Content from "photoswipe/dist/types/slide/content"; +import Slide, { _SlideData } from "photoswipe/dist/types/slide/slide"; +import { IPhoto } from "../../types"; + +type PsAugment = { + data: _SlideData & { + photo?: IPhoto; + }; +}; +export type PsSlide = Slide & PsAugment; +export type PsContent = Content & PsAugment; +export type PsEvent = { + content: PsContent; + preventDefault: () => void; +};