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.
## v4.12.1
- **Feature**: Load full image on zoom ([#266](https://github.com/pulsejet/memories/issues/266))
## v4.12.0 (2023-03-10)
**This release drops support for Nextcloud 24.**

View File

@ -265,7 +265,7 @@ class ImageController extends ApiBase
* @PublicPage
*
* 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.
*/
public function decodable(string $id)
@ -285,7 +285,7 @@ class ImageController extends ApiBase
$blob = $file->getContent();
// 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->readImageBlob($blob);
$image->setImageFormat('jpeg');

View File

@ -55,6 +55,22 @@
>
{{ t("memories", "Show past photos on top of timeline") }}
</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
@ -204,6 +220,14 @@ export default defineComponent({
await this.updateSetting("squareThumbs");
},
async updateFullResOnZoom() {
await this.updateSetting("fullResOnZoom");
},
async updateFullResAlways() {
await this.updateSetting("fullResAlways");
},
async updateEnableTopMemories() {
await this.updateSetting("enableTopMemories");
},

View File

@ -1,42 +1,23 @@
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";
export function getXImgElem(
content: any,
onLoad: () => void
): HTMLImageElement {
const img = document.createElement("img");
img.classList.add("pswp__img");
img.style.visibility = "hidden";
// Fetch with Axios
fetchImage(content.data.src).then((blob) => {
// Check if destroyed already
if (!content.element) return;
// Insert image
const blobUrl = URL.createObjectURL(blob);
img.src = blobUrl;
img.onerror = img.onload = () => {
img.style.visibility = "visible";
onLoad();
URL.revokeObjectURL(blobUrl);
};
});
return img;
}
export default class ImageContentSetup {
constructor(lightbox: PhotoSwipe) {
this.initLightboxEvents(lightbox);
}
private loading = 0;
initLightboxEvents(lightbox: PhotoSwipe) {
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) {
@ -44,11 +25,88 @@ export default class ImageContentSetup {
// Insert image throgh XImgCache
e.preventDefault();
e.content.element = getXImgElem(e.content, () => e.content.onLoaded());
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");
img.classList.add("pswp__img", "ximg");
img.style.visibility = "hidden";
// Fetch with Axios
fetchImage(content.data.src).then((blob) => {
// Check if destroyed already
if (!content.element) return;
// Insert image
const blobUrl = URL.createObjectURL(blob);
img.src = blobUrl;
img.onerror = img.onload = () => {
img.style.visibility = "visible";
onLoad();
URL.revokeObjectURL(blobUrl);
img.onerror = img.onload = null;
this.slideActivate();
};
});
return img;
}
zoomPanUpdate({ slide }: { slide: Slide }) {
if (!slide.data.highSrc || slide.data.highSrcCond !== "zoom") return;
if (slide.currZoomLevel >= slide.zoomLevels.secondary) {
this.loadFullImage(slide);
}
}
slideActivate() {
const slide = this.lightbox.currSlide;
if (slide.data.highSrcCond === "always") {
this.loadFullImage(slide);
}
}
loadFullImage(slide: Slide) {
if (!slide.data.highSrc) return;
// 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 { getXImgElem } from "./PsImage";
import PsImage from "./PsImage";
import * as utils from "../../services/Utils";
export function isLiveContent(content): boolean {
@ -13,11 +13,7 @@ export function isLiveContent(content): boolean {
}
class LivePhotoContentSetup {
constructor(lightbox: PhotoSwipe, private options) {
this.initLightboxEvents(lightbox);
}
initLightboxEvents(lightbox: PhotoSwipe) {
constructor(lightbox: PhotoSwipe, private psImage: PsImage) {
lightbox.on("contentLoad", this.onContentLoad.bind(this));
lightbox.on("contentActivate", this.onContentActivate.bind(this));
lightbox.on("contentDeactivate", this.onContentDeactivate.bind(this));
@ -47,7 +43,7 @@ class LivePhotoContentSetup {
utils.setupLivePhotoHooks(video);
const img = getXImgElem(content, () => content.onLoaded());
const img = this.psImage.getXImgElem(content, () => content.onLoaded());
div.appendChild(img);
content.element = div;

View File

@ -180,6 +180,7 @@ import { defineComponent } from "vue";
import { IDay, IFileInfo, IPhoto, IRow, IRowType } from "../../types";
import UserConfig from "../../mixins/UserConfig";
import NcActions from "@nextcloud/vue/dist/Components/NcActions";
import NcActionButton from "@nextcloud/vue/dist/Components/NcActionButton";
import axios from "@nextcloud/axios";
@ -234,6 +235,8 @@ export default defineComponent({
LivePhotoIcon,
},
mixins: [UserConfig],
data: () => ({
isOpen: false,
originalTitle: null,
@ -251,6 +254,8 @@ export default defineComponent({
/** Base dialog */
photoswipe: null as PhotoSwipe | null,
psVideo: null as PsVideo | null,
psImage: null as PsImage | null,
psLivePhoto: null as PsLivePhoto | null,
list: [] as IPhoto[],
@ -582,17 +587,20 @@ export default defineComponent({
});
// Video support
new PsVideo(<any>this.photoswipe, {
this.psVideo = new PsVideo(<any>this.photoswipe, {
videoAttributes: { controls: "", playsinline: "", preload: "none" },
autoplay: true,
preventDragOffset: 40,
});
// Live Photo support
this.psLivePhoto = new PsLivePhoto(<any>this.photoswipe, {});
// 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
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 {
src: previewUrl,
highSrc: fullUrl,
highSrcCond: fullLoadCond,
width: w || undefined,
height: h || undefined,
thumbCropped: true,

View File

@ -5,7 +5,13 @@ import { API } from "../services/API";
import { defineComponent } from "vue";
const eventName = "memories:user-config-changed";
const localSettings = ["squareThumbs", "showFaceRect", "albumListSort"];
const localSettings = [
"squareThumbs",
"fullResOnZoom",
"fullResAlways",
"showFaceRect",
"albumListSort",
];
export default defineComponent({
name: "UserConfig",
@ -48,6 +54,10 @@ export default defineComponent({
config_placesGis: Number(loadState("memories", "places_gis", <string>"-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_albumListSort: Number(
localStorage.getItem("memories_albumListSort") || 1

View File

@ -27,6 +27,8 @@ declare module "vue" {
config_placesGis: number;
config_squareThumbs: boolean;
config_enableTopMemories: boolean;
config_fullResOnZoom: boolean;
config_fullResAlways: boolean;
config_showFaceRect: boolean;
config_albumListSort: 1 | 2;
config_eventName: string;