viewer: allow loading full image (fix #266)
Signed-off-by: Varun Patil <varunpatil@ucla.edu>pull/504/head
parent
41a37df454
commit
6698d58135
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
This file is manually updated. Please file an issue if something is missing.
|
This file is manually updated. Please file an issue if something is missing.
|
||||||
|
|
||||||
|
## v4.12.1
|
||||||
|
|
||||||
|
- **Feature**: Load full image on zoom ([#266](https://github.com/pulsejet/memories/issues/266))
|
||||||
|
|
||||||
## v4.12.0 (2023-03-10)
|
## v4.12.0 (2023-03-10)
|
||||||
|
|
||||||
**This release drops support for Nextcloud 24.**
|
**This release drops support for Nextcloud 24.**
|
||||||
|
|
|
@ -265,7 +265,7 @@ class ImageController extends ApiBase
|
||||||
* @PublicPage
|
* @PublicPage
|
||||||
*
|
*
|
||||||
* Get a full resolution decodable image for editing from a file.
|
* Get a full resolution decodable image for editing from a file.
|
||||||
* The returned image may be png / webp / jpeg.
|
* The returned image may be png / webp / jpeg / gif.
|
||||||
* These formats are supported by all browsers.
|
* These formats are supported by all browsers.
|
||||||
*/
|
*/
|
||||||
public function decodable(string $id)
|
public function decodable(string $id)
|
||||||
|
@ -285,7 +285,7 @@ class ImageController extends ApiBase
|
||||||
$blob = $file->getContent();
|
$blob = $file->getContent();
|
||||||
|
|
||||||
// Convert image to JPEG if required
|
// Convert image to JPEG if required
|
||||||
if (!\in_array($mimetype, ['image/png', 'image/webp', 'image/jpeg'], true)) {
|
if (!\in_array($mimetype, ['image/png', 'image/webp', 'image/jpeg', 'image/gif'], true)) {
|
||||||
$image = new \Imagick();
|
$image = new \Imagick();
|
||||||
$image->readImageBlob($blob);
|
$image->readImageBlob($blob);
|
||||||
$image->setImageFormat('jpeg');
|
$image->setImageFormat('jpeg');
|
||||||
|
|
|
@ -55,6 +55,22 @@
|
||||||
>
|
>
|
||||||
{{ t("memories", "Show past photos on top of timeline") }}
|
{{ t("memories", "Show past photos on top of timeline") }}
|
||||||
</NcCheckboxRadioSwitch>
|
</NcCheckboxRadioSwitch>
|
||||||
|
|
||||||
|
<NcCheckboxRadioSwitch
|
||||||
|
:checked.sync="config_fullResOnZoom"
|
||||||
|
@update:checked="updateFullResOnZoom"
|
||||||
|
type="switch"
|
||||||
|
>
|
||||||
|
{{ t("memories", "Load full size image on zoom") }}
|
||||||
|
</NcCheckboxRadioSwitch>
|
||||||
|
|
||||||
|
<NcCheckboxRadioSwitch
|
||||||
|
:checked.sync="config_fullResAlways"
|
||||||
|
@update:checked="updateFullResAlways"
|
||||||
|
type="switch"
|
||||||
|
>
|
||||||
|
{{ t("memories", "Always load full size image (not recommended)") }}
|
||||||
|
</NcCheckboxRadioSwitch>
|
||||||
</NcAppSettingsSection>
|
</NcAppSettingsSection>
|
||||||
|
|
||||||
<NcAppSettingsSection
|
<NcAppSettingsSection
|
||||||
|
@ -204,6 +220,14 @@ export default defineComponent({
|
||||||
await this.updateSetting("squareThumbs");
|
await this.updateSetting("squareThumbs");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async updateFullResOnZoom() {
|
||||||
|
await this.updateSetting("fullResOnZoom");
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateFullResAlways() {
|
||||||
|
await this.updateSetting("fullResAlways");
|
||||||
|
},
|
||||||
|
|
||||||
async updateEnableTopMemories() {
|
async updateEnableTopMemories() {
|
||||||
await this.updateSetting("enableTopMemories");
|
await this.updateSetting("enableTopMemories");
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,14 +1,41 @@
|
||||||
import PhotoSwipe from "photoswipe";
|
import PhotoSwipe from "photoswipe";
|
||||||
|
import Slide from "photoswipe/dist/types/slide/slide";
|
||||||
|
|
||||||
import { isVideoContent } from "./PsVideo";
|
import { isVideoContent } from "./PsVideo";
|
||||||
import { isLiveContent } from "./PsLivePhoto";
|
import { isLiveContent } from "./PsLivePhoto";
|
||||||
import { fetchImage } from "../frame/XImgCache";
|
import { fetchImage } from "../frame/XImgCache";
|
||||||
|
|
||||||
export function getXImgElem(
|
export default class ImageContentSetup {
|
||||||
content: any,
|
private loading = 0;
|
||||||
onLoad: () => void
|
|
||||||
): HTMLImageElement {
|
constructor(private lightbox: PhotoSwipe) {
|
||||||
|
lightbox.on("contentLoad", this.onContentLoad.bind(this));
|
||||||
|
lightbox.on("contentLoadImage", this.onContentLoadImage.bind(this));
|
||||||
|
lightbox.on("zoomPanUpdate", this.zoomPanUpdate.bind(this));
|
||||||
|
lightbox.on("slideActivate", this.slideActivate.bind(this));
|
||||||
|
lightbox.addFilter("isContentLoading", this.isContentLoading.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
isContentLoading(isLoading: boolean, content: any) {
|
||||||
|
return isLoading || this.loading > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
onContentLoad(e) {
|
||||||
|
if (isVideoContent(e.content) || isLiveContent(e.content)) return;
|
||||||
|
|
||||||
|
// Insert image throgh XImgCache
|
||||||
|
e.preventDefault();
|
||||||
|
e.content.element = this.getXImgElem(e.content, () => e.content.onLoaded());
|
||||||
|
}
|
||||||
|
|
||||||
|
onContentLoadImage(e) {
|
||||||
|
if (isVideoContent(e.content) || isLiveContent(e.content)) return;
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
getXImgElem(content: any, onLoad: () => void): HTMLImageElement {
|
||||||
const img = document.createElement("img");
|
const img = document.createElement("img");
|
||||||
img.classList.add("pswp__img");
|
img.classList.add("pswp__img", "ximg");
|
||||||
img.style.visibility = "hidden";
|
img.style.visibility = "hidden";
|
||||||
|
|
||||||
// Fetch with Axios
|
// Fetch with Axios
|
||||||
|
@ -23,32 +50,63 @@ export function getXImgElem(
|
||||||
img.style.visibility = "visible";
|
img.style.visibility = "visible";
|
||||||
onLoad();
|
onLoad();
|
||||||
URL.revokeObjectURL(blobUrl);
|
URL.revokeObjectURL(blobUrl);
|
||||||
|
img.onerror = img.onload = null;
|
||||||
|
this.slideActivate();
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return img;
|
return img;
|
||||||
}
|
|
||||||
|
|
||||||
export default class ImageContentSetup {
|
|
||||||
constructor(lightbox: PhotoSwipe) {
|
|
||||||
this.initLightboxEvents(lightbox);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initLightboxEvents(lightbox: PhotoSwipe) {
|
zoomPanUpdate({ slide }: { slide: Slide }) {
|
||||||
lightbox.on("contentLoad", this.onContentLoad.bind(this));
|
if (!slide.data.highSrc || slide.data.highSrcCond !== "zoom") return;
|
||||||
lightbox.on("contentLoadImage", this.onContentLoadImage.bind(this));
|
|
||||||
|
if (slide.currZoomLevel >= slide.zoomLevels.secondary) {
|
||||||
|
this.loadFullImage(slide);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onContentLoad(e) {
|
slideActivate() {
|
||||||
if (isVideoContent(e.content) || isLiveContent(e.content)) return;
|
const slide = this.lightbox.currSlide;
|
||||||
|
if (slide.data.highSrcCond === "always") {
|
||||||
// Insert image throgh XImgCache
|
this.loadFullImage(slide);
|
||||||
e.preventDefault();
|
}
|
||||||
e.content.element = getXImgElem(e.content, () => e.content.onLoaded());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onContentLoadImage(e) {
|
loadFullImage(slide: Slide) {
|
||||||
if (isVideoContent(e.content) || isLiveContent(e.content)) return;
|
if (!slide.data.highSrc) return;
|
||||||
e.preventDefault();
|
|
||||||
|
// Get ximg element
|
||||||
|
const img = slide.holderElement?.querySelector(
|
||||||
|
".ximg:not(.ximg--full)"
|
||||||
|
) as HTMLImageElement;
|
||||||
|
if (!img) return;
|
||||||
|
|
||||||
|
// Load full image at secondary zoom level
|
||||||
|
img.classList.add("ximg--full");
|
||||||
|
|
||||||
|
this.loading++;
|
||||||
|
this.lightbox.ui.updatePreloaderVisibility();
|
||||||
|
|
||||||
|
fetchImage(slide.data.highSrc)
|
||||||
|
.then((blob) => {
|
||||||
|
// Check if destroyed already
|
||||||
|
if (!slide.content.element) return;
|
||||||
|
|
||||||
|
// Insert image
|
||||||
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
|
img.onerror = img.onload = () => {
|
||||||
|
URL.revokeObjectURL(blobUrl);
|
||||||
|
img.onerror = img.onload = null;
|
||||||
|
};
|
||||||
|
img.src = blobUrl;
|
||||||
|
|
||||||
|
// Don't load again
|
||||||
|
slide.data.highSrcCond = "never";
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading--;
|
||||||
|
this.lightbox.ui.updatePreloaderVisibility();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import PhotoSwipe from "photoswipe";
|
import PhotoSwipe from "photoswipe";
|
||||||
import { getXImgElem } from "./PsImage";
|
import PsImage from "./PsImage";
|
||||||
import * as utils from "../../services/Utils";
|
import * as utils from "../../services/Utils";
|
||||||
|
|
||||||
export function isLiveContent(content): boolean {
|
export function isLiveContent(content): boolean {
|
||||||
|
@ -13,11 +13,7 @@ export function isLiveContent(content): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
class LivePhotoContentSetup {
|
class LivePhotoContentSetup {
|
||||||
constructor(lightbox: PhotoSwipe, private options) {
|
constructor(lightbox: PhotoSwipe, private psImage: PsImage) {
|
||||||
this.initLightboxEvents(lightbox);
|
|
||||||
}
|
|
||||||
|
|
||||||
initLightboxEvents(lightbox: PhotoSwipe) {
|
|
||||||
lightbox.on("contentLoad", this.onContentLoad.bind(this));
|
lightbox.on("contentLoad", this.onContentLoad.bind(this));
|
||||||
lightbox.on("contentActivate", this.onContentActivate.bind(this));
|
lightbox.on("contentActivate", this.onContentActivate.bind(this));
|
||||||
lightbox.on("contentDeactivate", this.onContentDeactivate.bind(this));
|
lightbox.on("contentDeactivate", this.onContentDeactivate.bind(this));
|
||||||
|
@ -47,7 +43,7 @@ class LivePhotoContentSetup {
|
||||||
|
|
||||||
utils.setupLivePhotoHooks(video);
|
utils.setupLivePhotoHooks(video);
|
||||||
|
|
||||||
const img = getXImgElem(content, () => content.onLoaded());
|
const img = this.psImage.getXImgElem(content, () => content.onLoaded());
|
||||||
div.appendChild(img);
|
div.appendChild(img);
|
||||||
|
|
||||||
content.element = div;
|
content.element = div;
|
||||||
|
|
|
@ -180,6 +180,7 @@ import { defineComponent } from "vue";
|
||||||
|
|
||||||
import { IDay, IFileInfo, IPhoto, IRow, IRowType } from "../../types";
|
import { IDay, IFileInfo, IPhoto, IRow, IRowType } from "../../types";
|
||||||
|
|
||||||
|
import UserConfig from "../../mixins/UserConfig";
|
||||||
import NcActions from "@nextcloud/vue/dist/Components/NcActions";
|
import NcActions from "@nextcloud/vue/dist/Components/NcActions";
|
||||||
import NcActionButton from "@nextcloud/vue/dist/Components/NcActionButton";
|
import NcActionButton from "@nextcloud/vue/dist/Components/NcActionButton";
|
||||||
import axios from "@nextcloud/axios";
|
import axios from "@nextcloud/axios";
|
||||||
|
@ -234,6 +235,8 @@ export default defineComponent({
|
||||||
LivePhotoIcon,
|
LivePhotoIcon,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
mixins: [UserConfig],
|
||||||
|
|
||||||
data: () => ({
|
data: () => ({
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
originalTitle: null,
|
originalTitle: null,
|
||||||
|
@ -251,6 +254,8 @@ export default defineComponent({
|
||||||
|
|
||||||
/** Base dialog */
|
/** Base dialog */
|
||||||
photoswipe: null as PhotoSwipe | null,
|
photoswipe: null as PhotoSwipe | null,
|
||||||
|
psVideo: null as PsVideo | null,
|
||||||
|
psImage: null as PsImage | null,
|
||||||
psLivePhoto: null as PsLivePhoto | null,
|
psLivePhoto: null as PsLivePhoto | null,
|
||||||
|
|
||||||
list: [] as IPhoto[],
|
list: [] as IPhoto[],
|
||||||
|
@ -582,17 +587,20 @@ export default defineComponent({
|
||||||
});
|
});
|
||||||
|
|
||||||
// Video support
|
// Video support
|
||||||
new PsVideo(<any>this.photoswipe, {
|
this.psVideo = new PsVideo(<any>this.photoswipe, {
|
||||||
videoAttributes: { controls: "", playsinline: "", preload: "none" },
|
videoAttributes: { controls: "", playsinline: "", preload: "none" },
|
||||||
autoplay: true,
|
autoplay: true,
|
||||||
preventDragOffset: 40,
|
preventDragOffset: 40,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Live Photo support
|
|
||||||
this.psLivePhoto = new PsLivePhoto(<any>this.photoswipe, {});
|
|
||||||
|
|
||||||
// Image support
|
// Image support
|
||||||
new PsImage(<any>this.photoswipe);
|
this.psImage = new PsImage(<any>this.photoswipe);
|
||||||
|
|
||||||
|
// Live Photo support
|
||||||
|
this.psLivePhoto = new PsLivePhoto(
|
||||||
|
<any>this.photoswipe,
|
||||||
|
<any>this.psImage
|
||||||
|
);
|
||||||
|
|
||||||
// Patch the close button to stop the slideshow
|
// Patch the close button to stop the slideshow
|
||||||
const _close = this.photoswipe.close.bind(this.photoswipe);
|
const _close = this.photoswipe.close.bind(this.photoswipe);
|
||||||
|
@ -808,8 +816,20 @@ export default defineComponent({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get full image URL
|
||||||
|
const fullUrl = isvideo
|
||||||
|
? null
|
||||||
|
: API.IMAGE_DECODABLE(photo.fileid, photo.etag);
|
||||||
|
const fullLoadCond = this.config_fullResAlways
|
||||||
|
? "always"
|
||||||
|
: this.config_fullResOnZoom
|
||||||
|
? "zoom"
|
||||||
|
: "never";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
src: previewUrl,
|
src: previewUrl,
|
||||||
|
highSrc: fullUrl,
|
||||||
|
highSrcCond: fullLoadCond,
|
||||||
width: w || undefined,
|
width: w || undefined,
|
||||||
height: h || undefined,
|
height: h || undefined,
|
||||||
thumbCropped: true,
|
thumbCropped: true,
|
||||||
|
|
|
@ -5,7 +5,13 @@ import { API } from "../services/API";
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent } from "vue";
|
||||||
|
|
||||||
const eventName = "memories:user-config-changed";
|
const eventName = "memories:user-config-changed";
|
||||||
const localSettings = ["squareThumbs", "showFaceRect", "albumListSort"];
|
const localSettings = [
|
||||||
|
"squareThumbs",
|
||||||
|
"fullResOnZoom",
|
||||||
|
"fullResAlways",
|
||||||
|
"showFaceRect",
|
||||||
|
"albumListSort",
|
||||||
|
];
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "UserConfig",
|
name: "UserConfig",
|
||||||
|
@ -48,6 +54,10 @@ export default defineComponent({
|
||||||
config_placesGis: Number(loadState("memories", "places_gis", <string>"-1")),
|
config_placesGis: Number(loadState("memories", "places_gis", <string>"-1")),
|
||||||
|
|
||||||
config_squareThumbs: localStorage.getItem("memories_squareThumbs") === "1",
|
config_squareThumbs: localStorage.getItem("memories_squareThumbs") === "1",
|
||||||
|
config_fullResOnZoom:
|
||||||
|
localStorage.getItem("memories_fullResOnZoom") !== "0",
|
||||||
|
config_fullResAlways:
|
||||||
|
localStorage.getItem("memories_fullResAlways") === "1",
|
||||||
config_showFaceRect: localStorage.getItem("memories_showFaceRect") === "1",
|
config_showFaceRect: localStorage.getItem("memories_showFaceRect") === "1",
|
||||||
config_albumListSort: Number(
|
config_albumListSort: Number(
|
||||||
localStorage.getItem("memories_albumListSort") || 1
|
localStorage.getItem("memories_albumListSort") || 1
|
||||||
|
|
|
@ -27,6 +27,8 @@ declare module "vue" {
|
||||||
config_placesGis: number;
|
config_placesGis: number;
|
||||||
config_squareThumbs: boolean;
|
config_squareThumbs: boolean;
|
||||||
config_enableTopMemories: boolean;
|
config_enableTopMemories: boolean;
|
||||||
|
config_fullResOnZoom: boolean;
|
||||||
|
config_fullResAlways: boolean;
|
||||||
config_showFaceRect: boolean;
|
config_showFaceRect: boolean;
|
||||||
config_albumListSort: 1 | 2;
|
config_albumListSort: 1 | 2;
|
||||||
config_eventName: string;
|
config_eventName: string;
|
||||||
|
|
Loading…
Reference in New Issue