UX: Proof-of-concept for new video player #1307 #3372

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer 2023-06-19 14:53:15 +02:00
parent df0d93b1e4
commit baeba38acb
20 changed files with 1639 additions and 123 deletions

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.6 KiB

View file

@ -64,6 +64,7 @@
"mocha": "^10.2.0",
"node-storage-shim": "^2.0.1",
"photoswipe": "^4.1.3",
"plyr": "^3.7.8",
"postcss": "^8.4.20",
"postcss-import": "^15.1.0",
"postcss-loader": "^7.0.2",
@ -4656,6 +4657,11 @@
"resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz",
"integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg=="
},
"node_modules/custom-event-polyfill": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz",
"integrity": "sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w=="
},
"node_modules/date-format": {
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz",
@ -8390,6 +8396,11 @@
"json5": "lib/cli.js"
}
},
"node_modules/loadjs": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/loadjs/-/loadjs-4.2.0.tgz",
"integrity": "sha512-AgQGZisAlTPbTEzrHPb6q+NYBMD+DP9uvGSIjSUM5uG+0jG15cb8axWpxuOIqrmQjn6scaaH8JwloiP27b2KXA=="
},
"node_modules/loadware": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/loadware/-/loadware-2.0.0.tgz",
@ -9525,6 +9536,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/plyr": {
"version": "3.7.8",
"resolved": "https://registry.npmjs.org/plyr/-/plyr-3.7.8.tgz",
"integrity": "sha512-yG/EHDobwbB/uP+4Bm6eUpJ93f8xxHjjk2dYcD1Oqpe1EcuQl5tzzw9Oq+uVAzd2lkM11qZfydSiyIpiB8pgdA==",
"dependencies": {
"core-js": "^3.26.1",
"custom-event-polyfill": "^1.0.7",
"loadjs": "^4.2.0",
"rangetouch": "^2.0.1",
"url-polyfill": "^1.1.12"
}
},
"node_modules/pofile": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/pofile/-/pofile-1.1.4.tgz",
@ -10942,6 +10965,11 @@
"node": ">= 0.6"
}
},
"node_modules/rangetouch": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/rangetouch/-/rangetouch-2.0.1.tgz",
"integrity": "sha512-sln+pNSc8NGaHoLzwNBssFSf/rSYkqeBXzX1AtJlkJiUaVSJSbRAWJk+4omsXkN+EJalzkZhWQ3th1m0FpR5xA=="
},
"node_modules/raw-body": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
@ -12661,6 +12689,11 @@
"url": "https://opencollective.com/webpack"
}
},
"node_modules/url-polyfill": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/url-polyfill/-/url-polyfill-1.1.12.tgz",
"integrity": "sha512-mYFmBHCapZjtcNHW0MDq9967t+z4Dmg5CJ0KqysK3+ZbyoNOWQHksGCTWwDhxGXllkWlOc10Xfko6v4a3ucM6A=="
},
"node_modules/util": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",

View file

@ -75,6 +75,7 @@
"mocha": "^10.2.0",
"node-storage-shim": "^2.0.1",
"photoswipe": "^4.1.3",
"plyr": "^3.7.8",
"postcss": "^8.4.20",
"postcss-import": "^15.1.0",
"postcss-loader": "^7.0.2",

View file

@ -76,9 +76,7 @@
</div>
</div>
<div v-if="player.show" class="video-viewer" @click.stop.prevent="closePlayer" @keydown.esc.stop.prevent="closePlayer">
<p-video-player ref="player" :source="player.source" :poster="player.poster"
:height="player.height" :width="player.width" :autoplay="player.autoplay" :loop="player.loop" @close="closePlayer">
</p-video-player>
<p-video-player ref="player" :videos="videos" :index="0" @close="closePlayer"></p-video-player>
</div>
</div>
</template>
@ -101,13 +99,14 @@ export default {
canDownload: this.$config.allow("photos", "download") && this.$config.feature("download"),
selection: this.$clipboard.selection,
config: this.$config.values,
item: new Thumb(),
item: new Thumb(false),
subscriptions: [],
interval: false,
slideshow: {
active: false,
next: 0,
},
videos: [],
player: {
show: false,
loop: false,
@ -175,28 +174,23 @@ export default {
},
onPlay() {
if (this.item && this.item.Playable) {
new Photo().find(this.item.UID).then((video) => this.openPlayer(video));
new Photo().find(this.item.UID).then((photo) => this.openPlayer(photo));
}
},
openPlayer(video) {
if (!video) {
openPlayer(photo) {
if (!photo) {
this.$notify.error(this.$gettext("No video selected"));
return;
}
const params = video.videoParams();
const video = photo.video();
if (params.error) {
this.$notify.error(params.error);
if (!video || video.Error) {
this.$notify.error(this.$gettext("Not Found"));
return;
}
// Set video parameters.
this.player.loop = params.loop;
this.player.width = params.width;
this.player.height = params.height;
this.player.poster = params.poster;
this.player.source = params.uri;
this.videos = [video];
// Play video.
this.player.show = true;

View file

@ -1,14 +1,17 @@
<template>
<div class="video-wrapper" :style="style">
<video :key="source" ref="video" class="video-player" :height="height" :width="width" :autoplay="autoplay"
:style="style" :poster="poster" :loop="loop" preload="auto" controls playsinline @click.stop
<div class="video-wrapper" :style="style" @click.stop.prevent>
<video :key="source" ref="video" class="video-player"
preload="auto" controls autoplay playsinline @click.stop
@keydown.esc.stop.prevent="$emit('close')">
<source :src="source">
<source :src="video.url()">
</video>
</div>
</template>
<script>
import Video from "model/video";
import Plyr from 'plyr';
export default {
name: "PVideoPlayer",
props: {
@ -19,13 +22,13 @@ export default {
},
poster: {
type: String,
required: true,
required: false,
default: ""
},
source: {
type: String,
required: true,
default: ""
index: {
type: Number,
required: false,
default: 0
},
width: {
type: Number,
@ -59,38 +62,127 @@ export default {
error: {
type: Function,
default: () => {},
}
},
videos: {
type: Array,
required: false,
default: () => [],
},
album: {
type: Object,
required: false,
default: () => {},
},
},
data() {
const c = this.$config;
return {
refresh: false,
style: `width: 90vw; height: 90vh`,
source: "",
video: new Video(false),
player: false,
current: this.index,
options: {
iconUrl: `${c.staticUri}/video/plyr.svg`,
controls: ['play-large', 'play', 'progress', 'current-time', 'duration', 'mute', 'volume', 'download', 'settings', 'airplay', 'fullscreen'],
settings: ['loop'],
captions: { active: false, language: 'auto', update: false },
hideControls: true,
enabled: true,
autoplay: true,
clickToPlay: true,
disableContextMenu: false,
resetOnEnd: true,
toggleInvert: true,
blankVideo: `${c.staticUri}/video/404.mp4`,
loop: {
active: false,
},
},
};
},
data: () => ({
refresh: false,
style: `width: 90vw; height: 90vh`,
}),
watch: {
source: function (src) {
if (src) {
this.setSrc(src);
}
},
videos: function () {
this.onVideos();
},
},
mounted() {
document.body.classList.add("player");
this.render();
window.addEventListener('keyup', this.onKeyUp);
},
beforeDestroy() {
document.body.classList.remove("player");
this.stop();
},
beforeUnmount() {
try {
if (this.player) {
this.player.destroy();
}
} catch (e) {
console.log(e);
}
window.removeEventListener('keyup', this.onKeyUp);
},
methods: {
videoEl() {
return this.$el.getElementsByTagName('video')[0];
},
updateStyle() {
// this.style = `width: ${this.width.toString()}px; height: ${this.height.toString()}px`;
this.style = `width:100%; height: 100%`;
if (!this.video || !this.video.Width) {
return;
}
const size = this.video.playerSize();
this.style = `width: ${size.width.toString()}px; height: ${size.height.toString()}px`;
const plyrEl = this.$el.getElementsByClassName('plyr')[0];
if (plyrEl) {
plyrEl.style.cssText = this.style;
}
this.$el.style.cssText = this.style;
},
currentVideo() {
if(typeof this.videos[this.current] === 'undefined') {
return Video.notFound();
}
return this.videos[this.current];
},
onVideos() {
this.current = this.play;
/*
const video = this.currentVideo();
if (!video || video.Error) {
this.$notify.error(this.$gettext("Not Found"));
return;
}
// Set video parameters.
const size = video.playerSize();
this.loop = video.loop();
this.width = size.width;
this.height = size.height;
this.poster = video.posterUrl();
this.source = video.url(); */
},
render() {
this.updateStyle();
const el = this.videoEl();
if (!el) return;
this.player = new Plyr(el, this.options);
// this.player.on("ended", (ev) => { console.log("event.ended", ev); });
this.play();
},
fullscreen() {
const el = this.videoEl();
@ -98,12 +190,65 @@ export default {
el.requestFullscreen();
},
setSrc(src) {
if (!src) {
play() {
this.video = this.currentVideo();
if (!this.video) {
console.log("render: No current video");
return;
}
this.updateStyle();
this.player.source = {
type: 'video',
title: this.video.Title,
sources: [
{
src: this.video.url(),
// type: this.video.Mime,
},
],
poster: this.video.posterUrl("fit_720"),
};
this.player.loop = this.videos.length === 0 && this.video.loop();
this.player.play();
},
onPrev(ev) {
if(this.videos.length < 2) {
this.current = 0;
return;
} else if(this.current <= 0) {
return;
}
this.player.stop();
this.current--;
this.play();
},
onNext(ev) {
if(this.videos.length < 2) {
this.current = 0;
return;
} else if(this.current >= this.videos.length - 1) {
return;
}
this.player.stop();
this.current++;
this.play();
},
onKeyUp(ev) {
switch(ev.key) {
case "Escape": this.$emit('close'); break;
case "ArrowLeft": this.onPrev(ev); break;
case "ArrowRight": this.onNext(ev); break;
}
},
setSrc(src) {
// console.log("setSrc", src);
if (!src) {
return;
}
const el = this.videoEl();
if (!el) return;
@ -111,6 +256,17 @@ export default {
el.src = src;
el.poster = this.poster;
el.play();
this.updateStyle();
// console.log("el", el);
/* this.player = new Plyr(el);
console.log("this.player", this.player);
this.player.play();
this.player.source = src;
this.player.poster = this.poster;
*/
},
pause() {
const el = this.videoEl();

View file

@ -26,6 +26,8 @@ Additional information can be found in our Developer Guide:
@import url("vendor/icons/material-design-icons.css");
@import url("../../node_modules/vuetify/dist/vuetify.min.css");
@import url("../../node_modules/maplibre-gl/dist/maplibre-gl.css");
@import url("../../node_modules/plyr/dist/plyr.css");
@import url("variables.css");
@import url("typography.css");
@import url("wallpapers.css");
@import url("scrollbar.css");

View file

@ -0,0 +1,4 @@
:root {
--plyr-color-main: #2f303164; /* #d3cbfc66; */
--plyr-video-control-background-hover: #2f303190;
}

View file

@ -10,7 +10,7 @@
top: 0;
bottom: 0;
right: 0;
background-color: rgba(0,0,0,1);
background-color: rgba(0,0,0,0.74);
}
#photoprism .video-wrapper {

View file

@ -1,7 +1,6 @@
<template>
<div v-if="show" class="video-viewer" role="dialog" @click.stop.prevent="onClose" @keydown.esc.stop.prevent="onClose">
<p-video-player v-show="show" ref="player" :source="source" :poster="poster" :height="height"
:width="width" :autoplay="true" :loop="loop" @close="onClose"></p-video-player>
<p-video-player v-show="show" ref="player" :videos="videos" :index="index" :album="album" @close="onClose"></p-video-player>
</div>
</template>
<script>
@ -18,7 +17,8 @@ export default {
defaultHeight: 480,
width: 640,
height: 480,
video: null,
index: 0,
videos: [],
album: null,
loop: false,
subscriptions: [],
@ -39,9 +39,16 @@ export default {
methods: {
onOpen(ev, params) {
const fullscreen = !!params.fullscreen;
const hasQueue = params.videos && params.videos.length > 0;
this.video = params.video;
this.album = params.album;
this.videos = hasQueue ? params.videos : [];
this.album = params.album ? params.album : null;
if(params.index && params.index < this.videos.length) {
this.index = params.index;
} else {
this.index = 0;
}
this.play(fullscreen);
},
@ -58,25 +65,11 @@ export default {
this.show = false;
},
play(fullscreen) {
if (!this.video) {
this.$notify.error(this.$gettext("No video selected"));
if (!this.videos) {
this.$notify.error(this.$gettext("No videos found to play"));
return;
}
const params = this.video.videoParams();
if (params.error) {
this.$notify.error(params.error);
return;
}
// Set video parameters.
this.loop = params.loop;
this.width = params.width;
this.height = params.height;
this.poster = params.poster;
this.source = params.uri;
// Play video.
this.show = true;

View file

@ -24,13 +24,14 @@ Additional information can be found in our Developer Guide:
*/
import RestModel from "model/rest";
import Video from "model/video";
import { MediaAnimated, MediaImage } from "model/photo";
import Api from "common/api";
import { DateTime } from "luxon";
import Util from "common/util";
import { config } from "app/session";
import { $gettext } from "common/vm";
import download from "common/download";
import { MediaImage } from "./photo";
export class File extends RestModel {
getDefaults() {
@ -128,7 +129,7 @@ export class File extends RestModel {
return `${config.contentUri}/t/${this.Hash}/${config.previewToken}/${size}`;
}
getDownloadUrl() {
downloadUrl() {
return `${config.apiUri}/dl/${this.Hash}?t=${config.downloadToken}`;
}
@ -137,7 +138,7 @@ export class File extends RestModel {
return;
}
download(this.getDownloadUrl(), this.baseName(this.Name));
download(this.downloadUrl(), this.baseName(this.Name));
}
calculateSize(width, height) {
@ -287,6 +288,48 @@ export class File extends RestModel {
return Api.delete(this.getPhotoResource() + "/like");
}
isPlayable() {
if (this.MediaType === MediaAnimated) {
return true;
}
return this.Video;
}
video() {
let width = this.Width;
let height = this.Height;
if (width <= 0 || height <= 0) {
width = 640;
height = 480;
}
return new Video({
UID: this.UID,
PhotoUID: this.PhotoUID,
Hash: this.Hash,
PosterHash: this.Hash,
Title: this.Name,
Description: "",
TakenAt: this.TakenAt,
Favorite: false,
Playable: this.isPlayable(),
HDR: this.HDR,
Mime: this.Mime,
Type: this.Video ? "video" : "animated",
Codec: this.Codec,
Width: width,
Height: height,
Duration: this.Duration,
FPS: this.FPS,
Frames: this.Frames,
Projection: this.Projection,
ColorProfile: this.ColorProfile,
Error: this.Error,
});
}
static getCollectionResource() {
return "files";
}

View file

@ -27,6 +27,7 @@ import memoizeOne from "memoize-one";
import RestModel from "model/rest";
import File from "model/file";
import Video from "model/video";
import Marker from "model/marker";
import Api from "common/api";
import { DateTime } from "luxon";
@ -396,13 +397,7 @@ export class Photo extends RestModel {
return files.some((f) => f.Video);
});
videoParams() {
const uri = this.videoUrl();
if (!uri) {
return { error: "no video selected" };
}
video() {
let main = this.mainFile();
let file = this.videoFile();
@ -410,44 +405,37 @@ export class Photo extends RestModel {
file = main;
}
const vw = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
const vh = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
let width = file.Width > 0 ? file.Width : main.Width;
let height = file.Height > 0 ? file.Height : main.Height;
let actualWidth = 640;
let actualHeight = 480;
if (file.Width > 0) {
actualWidth = file.Width;
} else if (main && main.Width > 0) {
actualWidth = main.Width;
if (width <= 0 || height <= 0) {
width = 640;
height = 480;
}
if (file.Height > 0) {
actualHeight = file.Height;
} else if (main && main.Height > 0) {
actualHeight = main.Height;
}
let width = actualWidth;
let height = actualHeight;
if (vw < width + 80) {
let newWidth = vw - 90;
height = Math.round(newWidth * (actualHeight / actualWidth));
width = newWidth;
}
if (vh < height + 100) {
let newHeight = vh - 160;
width = Math.round(newHeight * (actualWidth / actualHeight));
height = newHeight;
}
const loop = this.Type === MediaAnimated || (file.Duration >= 0 && file.Duration <= 5000000000);
const poster = this.thumbnailUrl("fit_720");
const error = false;
return { width, height, loop, poster, uri, error };
return new Video({
UID: file.UID ? file.UID : this.FileUID,
PhotoUID: this.UID,
Hash: file.Hash ? file.Hash : this.Hash,
PosterHash: this.mainFileHash(),
Title: this.Title,
TakenAtLocal: this.TakenAtLocal,
Description: this.Description,
Favorite: this.Favorite,
Playable: this.isPlayable(),
HDR: file.HDR,
Mime: file.Mime,
Type: this.Type,
Codec: file.Codec,
Width: width,
Height: height,
Duration: file.Duration,
FPS: file.FPS,
Frames: file.Frames,
Projection: file.Projection,
ColorProfile: file.ColorProfile,
Error: file.Error,
});
}
videoFile() {
@ -600,7 +588,7 @@ export class Photo extends RestModel {
return `${contentUri}/t/${hash}/${previewToken}/${size}`;
});
getDownloadUrl() {
downloadUrl() {
return `${config.apiUri}/dl/${this.mainFileHash()}?t=${config.downloadToken}`;
}

View file

@ -68,7 +68,7 @@ export class Thumb extends Model {
}
}
static thumbNotFound() {
static notFound() {
const result = {
UID: "",
Title: $gettext("Not Found"),
@ -112,7 +112,7 @@ export class Thumb extends Model {
}
if (!photo || !photo.Hash) {
return this.thumbNotFound();
return this.notFound();
}
const result = {
@ -144,7 +144,7 @@ export class Thumb extends Model {
static fromFile(photo, file) {
if (!photo || !file || !file.Hash) {
return this.thumbNotFound();
return this.notFound();
}
const result = {

274
frontend/src/model/video.js Normal file
View file

@ -0,0 +1,274 @@
/*
Copyright (c) 2018 - 2023 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"):
<https://docs.photoprism.app/license/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:
<https://www.photoprism.app/trademark>
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:
<https://docs.photoprism.app/developer-guide/>
*/
import Model from "model.js";
import Api from "common/api";
import { $gettext } from "common/vm";
import File from "model/file";
import Photo from "model/photo";
import {
CodecAv1,
CodecHvc1,
CodecOGV,
CodecVP8,
CodecVP9,
FormatAv1,
FormatAvc,
FormatHevc,
FormatWebM,
MediaAnimated,
} from "model/photo";
import { config } from "app/session";
import { canUseOGV, canUseVP8, canUseVP9, canUseAv1, canUseWebM, canUseHevc } from "common/caniuse";
export class Video extends Model {
getDefaults() {
return {
UID: "",
PhotoUID: "",
Hash: "",
PosterHash: "",
Title: "",
Description: "",
TakenAt: "",
Favorite: false,
Playable: false,
HDR: false,
Mime: "",
Type: "",
Codec: "",
Width: 640,
Height: 480,
Duration: 0,
FPS: 0,
Frames: 0,
Projection: "",
ColorProfile: "",
Error: "",
};
}
getId() {
return this.UID ? this.UID : this.PhotoUID;
}
hasId() {
return !!this.getId();
}
toggleLike() {
this.Favorite = !this.Favorite;
if (this.Favorite) {
return Api.post("photos/" + this.PhotoUID + "/like");
} else {
return Api.delete("photos/" + this.PhotoUID + "/like");
}
}
url() {
let hash = this.Hash ? this.Hash : this.PosterHash;
if (!hash) {
return `${config.staticUri}/video/404.mp4`;
}
if (this.Hash && (this.Codec || this.FileType)) {
let videoFormat = FormatAvc;
if (canUseHevc && this.Codec === CodecHvc1) {
videoFormat = FormatHevc;
} else if (canUseOGV && this.Codec === CodecOGV) {
videoFormat = CodecOGV;
} else if (canUseVP8 && this.Codec === CodecVP8) {
videoFormat = CodecVP8;
} else if (canUseVP9 && this.Codec === CodecVP9) {
videoFormat = CodecVP9;
} else if (canUseAv1 && this.Codec === CodecAv1) {
videoFormat = FormatAv1;
} else if (canUseWebM && this.FileType === FormatWebM) {
videoFormat = FormatWebM;
}
return `${config.videoUri}/videos/${hash}/${config.previewToken}/${videoFormat}`;
}
return `${config.videoUri}/videos/${hash}/${config.previewToken}/${FormatAvc}`;
}
downloadUrl() {
return `${config.apiUri}/dl/${this.Hash}?t=${config.downloadToken}`;
}
posterUrl(size) {
let hash = this.PosterHash ? this.PosterHash : this.Hash;
if (!size) {
size = "fit_720";
}
if (!hash) {
return `${config.contentUri}/svg/video`;
}
return `${config.contentUri}/t/${hash}/${config.previewToken}/${size}`;
}
loop() {
return this.Type === MediaAnimated || (this.Duration >= 0 && this.Duration <= 5000000000);
}
playerSize() {
const vw = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
const vh = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
let actualWidth = this.Width;
let actualHeight = this.Height;
let width = actualWidth;
let height = actualHeight;
if (vw < width + 70) {
let newWidth = vw - 80;
height = Math.round(newWidth * (actualHeight / actualWidth));
width = newWidth;
}
if (vh < height + 100) {
let newHeight = vh - 140;
width = Math.round(newHeight * (actualWidth / actualHeight));
height = newHeight;
}
if (!width || !height) {
width = 640;
height = 480;
}
return { width, height };
}
static notFound() {
const result = new this();
result.Title = $gettext("Not Found");
result.Error = "not found";
return result;
}
static fromPhotos(photos, photosIndex) {
let videos = [];
let index = 0;
if (!photosIndex) {
photosIndex = 0;
}
const n = photos.length;
for (let i = 0; i < n; i++) {
const video = this.fromPhoto(photos[i]);
if (video && !video.Error) {
videos.push(video);
if (photosIndex > i) {
index++;
}
}
}
return { videos, index };
}
static fromPhoto(photo) {
if (!photo || !photo.Hash) {
return this.notFound();
}
if (!(photo instanceof Photo)) {
photo = new Photo(photo);
}
return photo.video();
}
static fromFile(photo, file) {
if (!file || !file.Hash) {
return false;
}
if (!(file instanceof File)) {
file = new File(file);
}
if (!file.isPlayable()) {
return false;
}
const video = file.video();
if (photo) {
video.Title = photo.Title;
video.Description = photo.Description;
video.Favorite = photo.Favorite;
}
return video;
}
static wrap(data) {
return data.map((values) => new this(values));
}
static fromFiles(photos) {
let result = [];
if (!photos || !photos.length) {
return result;
}
const n = photos.length;
for (let i = 0; i < n; i++) {
let p = photos[i];
if (!p.Files || !p.Files.length) {
continue;
}
for (let j = 0; j < p.Files.length; j++) {
let f = p.Files[j];
let video = this.fromFile(p, f);
if (video && !video.Error) {
result.push(video);
}
}
}
return result;
}
}
export default Video;

View file

@ -52,8 +52,9 @@
import {Photo, MediaLive, MediaRaw, MediaVideo, MediaAnimated} from "model/photo";
import Album from "model/album";
import Thumb from "model/thumb";
import Event from "pubsub-js";
import Video from "model/video";
import Viewer from "common/viewer";
import Event from "pubsub-js";
export default {
name: 'PPageAlbumPhotos',
@ -241,7 +242,8 @@ export default {
*/
if (preferVideo && selected.Type === MediaLive || selected.Type === MediaVideo || selected.Type === MediaAnimated) {
if (selected.isPlayable()) {
this.$viewer.play({video: selected, album: this.album});
const play = Video.fromPhotos(this.results, index);
this.$viewer.play({videos: play.videos, index: play.index, album: this.albums});
} else {
this.$viewer.show(Thumb.fromPhotos(this.results), index);
}

View file

@ -47,6 +47,7 @@
<script>
import {MediaAnimated, MediaLive, MediaRaw, MediaVideo, Photo} from "model/photo";
import Thumb from "model/thumb";
import Video from "model/video";
import Viewer from "common/viewer";
import Event from "pubsub-js";
@ -323,7 +324,7 @@ export default {
*/
if (preferVideo && selected.Type === MediaLive || selected.Type === MediaVideo || selected.Type === MediaAnimated) {
if (selected.isPlayable()) {
this.$viewer.play({video: selected});
this.$viewer.play(Video.fromPhotos(this.results, index));
} else {
this.$viewer.show(Thumb.fromPhotos(this.results), index);
}

View file

@ -310,6 +310,8 @@ Mock.onDelete("api/v1/link/5").reply(200, "delete success", mockHeaders);
Mock.onPost("api/v1/photos/55/like").reply(200, { status: "ok" }, mockHeaders);
Mock.onDelete("api/v1/photos/55/like").reply(200, { status: "ok" }, mockHeaders);
Mock.onPost("api/v1/photos/prqjmzr1jlmr4mpb/like").reply(200, { status: "ok" }, mockHeaders);
Mock.onDelete("api/v1/photos/prqjmzr1jlmr4mpb/like").reply(200, { status: "ok" }, mockHeaders);
Mock.onGet("api/v1/albums/5").reply(200, { UID: "5" }, mockHeaders);
Mock.onPut("api/v1/photos/5").reply(200, { UID: "5" }, mockHeaders);
Mock.onDelete("api/v1/photos/abc123/like").reply(200, { status: "ok" }, mockHeaders);

View file

@ -120,7 +120,7 @@ describe("model/file", () => {
Name: "1/2/IMG123.jpg",
};
const file = new File(values);
assert.equal(file.getDownloadUrl("abc"), "/api/v1/dl/54ghtfd?t=2lbh9x09");
assert.equal(file.downloadUrl("abc"), "/api/v1/dl/54ghtfd?t=2lbh9x09");
});
it("should not download as hash is missing", () => {

View file

@ -120,7 +120,7 @@ describe("model/photo", () => {
it("should get photo download url", () => {
const values = { ID: 5, Title: "Crazy Cat", Hash: 345982 };
const photo = new Photo(values);
const result = photo.getDownloadUrl();
const result = photo.downloadUrl();
assert.equal(result, "/api/v1/dl/345982?t=2lbh9x09");
});
@ -636,11 +636,17 @@ describe("model/photo", () => {
],
};
const photo3 = new Photo(values3);
const result = photo3.videoParams();
assert.equal(result.height, "463");
assert.equal(result.width, "695");
assert.equal(result.loop, false);
assert.equal(result.uri, "/api/v1/videos/1xxbgdt55/public/avc");
const video = photo3.video();
assert.equal(video.Height, 600);
assert.equal(video.Width, 900);
assert.equal(video.loop(), false);
assert.equal(video.url(), "/api/v1/videos/1xxbgdt55/public/avc");
const playerSize = video.playerSize();
assert.equal(playerSize.height, 470);
assert.equal(playerSize.width, 705);
const values = {
ID: 11,
UID: "ABC127",
@ -667,12 +673,17 @@ describe("model/photo", () => {
},
],
};
const photo = new Photo(values);
const result2 = photo.videoParams();
assert.equal(result2.height, "440");
assert.equal(result2.width, "440");
assert.equal(result2.loop, false);
assert.equal(result2.uri, "/api/v1/videos/1xxbgdt55/public/avc");
const video2 = photo.video();
assert.equal(video2.Height, 5000);
assert.equal(video2.Width, 5000);
assert.equal(video2.loop(), false);
assert.equal(video2.url(), "/api/v1/videos/1xxbgdt55/public/avc");
const playerSize2 = video2.playerSize();
assert.equal(playerSize2.height, 460);
assert.equal(playerSize2.width, 460);
});
it("should return videofile", () => {

View file

@ -66,8 +66,8 @@ describe("model/thumb", () => {
assert.equal(thumb.Favorite, true);
});
it("should return thumb not found", () => {
const result = Thumb.thumbNotFound();
it("should return not placeholder", () => {
const result = Thumb.notFound();
assert.equal(result.UID, "");
assert.equal(result.Favorite, false);
});

File diff suppressed because it is too large Load diff