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.
|
||||
|
||||
## 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.**
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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");
|
||||
},
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue