viewer: allow loading full image (fix #266)

Signed-off-by: Varun Patil <varunpatil@ucla.edu>
pull/504/head
Varun Patil 2023-03-15 14:06:30 -07:00
parent 41a37df454
commit 6698d58135
8 changed files with 160 additions and 46 deletions

View File

@ -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.**

View File

@ -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');

View File

@ -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");
}, },

View File

@ -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();
});
} }
} }

View File

@ -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;

View File

@ -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,

View File

@ -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

View File

@ -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;