viewer: initial commit
parent
f86e255d60
commit
8a130c3efa
|
@ -17,6 +17,7 @@
|
|||
"justified-layout": "^4.1.0",
|
||||
"moment": "^2.29.4",
|
||||
"path-posix": "^1.0.0",
|
||||
"photoswipe": "^5.3.3",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"vue": "^2.7.10",
|
||||
"vue-class-component": "^7.2.6",
|
||||
|
@ -6673,6 +6674,14 @@
|
|||
"node": ">=0.12"
|
||||
}
|
||||
},
|
||||
"node_modules/photoswipe": {
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/photoswipe/-/photoswipe-5.3.3.tgz",
|
||||
"integrity": "sha512-BUuulwZwkYFKADSe5xf0dd+wf6dws34ZvqP8R3oYHepRauOXoQHvw600sw1HlWd8K0S3LRCS4jxyR5fTuI383Q==",
|
||||
"engines": {
|
||||
"node": ">= 0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
|
@ -14840,6 +14849,11 @@
|
|||
"sha.js": "^2.4.8"
|
||||
}
|
||||
},
|
||||
"photoswipe": {
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/photoswipe/-/photoswipe-5.3.3.tgz",
|
||||
"integrity": "sha512-BUuulwZwkYFKADSe5xf0dd+wf6dws34ZvqP8R3oYHepRauOXoQHvw600sw1HlWd8K0S3LRCS4jxyR5fTuI383Q=="
|
||||
},
|
||||
"picocolors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
"justified-layout": "^4.1.0",
|
||||
"moment": "^2.29.4",
|
||||
"path-posix": "^1.0.0",
|
||||
"photoswipe": "^5.3.3",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"vue": "^2.7.10",
|
||||
"vue-class-component": "^7.2.6",
|
||||
|
|
|
@ -213,11 +213,10 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
|||
private scrollerManager!: ScrollerManager & any;
|
||||
|
||||
/** Nextcloud viewer proxy */
|
||||
private viewerManager = new ViewerManager(
|
||||
this.deleteFromViewWithAnimation.bind(this),
|
||||
this.updateLoading.bind(this),
|
||||
this.$route
|
||||
);
|
||||
private viewerManager = new ViewerManager({
|
||||
ondelete: this.deleteFromViewWithAnimation,
|
||||
fetchDay: this.fetchDay,
|
||||
});
|
||||
|
||||
mounted() {
|
||||
this.selectionManager = this.$refs.selectionManager;
|
||||
|
@ -1141,7 +1140,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
|||
// selection mode
|
||||
this.selectionManager.selectPhoto(photo);
|
||||
} else {
|
||||
this.viewerManager.open(photo);
|
||||
this.viewerManager.open(photo, this.list);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<div
|
||||
:id="`memories-photo-${data.key || data.fileid}`"
|
||||
class="p-outer fill-block"
|
||||
:class="{
|
||||
selected: data.flag & c.FLAG_SELECTED,
|
||||
|
|
|
@ -200,7 +200,7 @@ export default class OnThisDay extends Mixins(GlobalMixin) {
|
|||
|
||||
click(year: IYear) {
|
||||
const allPhotos = this.years.flatMap((y) => y.photos);
|
||||
this.viewerManager.open(year.preview, allPhotos);
|
||||
// this.viewerManager.open(year.preview, allPhotos);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -94,6 +94,10 @@ export function hashCode(str: string): number {
|
|||
* @param key Key to use for comparison
|
||||
*/
|
||||
export function binarySearch(arr: any, elem: any, key?: string) {
|
||||
const desc = key
|
||||
? arr[0][key] > arr[arr.length - 1][key]
|
||||
: arr[0] > arr[arr.length - 1];
|
||||
|
||||
let minIndex = 0;
|
||||
let maxIndex = arr.length - 1;
|
||||
let currentIndex: number;
|
||||
|
@ -103,9 +107,12 @@ export function binarySearch(arr: any, elem: any, key?: string) {
|
|||
currentIndex = ((minIndex + maxIndex) / 2) | 0;
|
||||
currentElement = key ? arr[currentIndex][key] : arr[currentIndex];
|
||||
|
||||
if (currentElement < elem) {
|
||||
const e1 = desc ? elem : currentElement;
|
||||
const e2 = desc ? currentElement : elem;
|
||||
|
||||
if (e1 < e2) {
|
||||
minIndex = currentIndex + 1;
|
||||
} else if (currentElement > elem) {
|
||||
} else if (e1 > e2) {
|
||||
maxIndex = currentIndex - 1;
|
||||
} else {
|
||||
return currentIndex;
|
||||
|
|
|
@ -1,94 +1,228 @@
|
|||
import { IFileInfo, IPhoto } from "../types";
|
||||
import { IDay, IFileInfo, IPhoto, IRow, IRowType } from "../types";
|
||||
import { showError } from "@nextcloud/dialogs";
|
||||
import { subscribe } from "@nextcloud/event-bus";
|
||||
import { translate as t, translatePlural as n } from "@nextcloud/l10n";
|
||||
import { Route } from "vue-router";
|
||||
import * as dav from "./DavRequests";
|
||||
import PhotoSwipe from "photoswipe";
|
||||
import "photoswipe/style.css";
|
||||
import { getPreviewUrl } from "./FileUtils";
|
||||
import * as utils from "./Utils";
|
||||
|
||||
// Key to store sidebar state
|
||||
const SIDEBAR_KEY = "memories:sidebar-open";
|
||||
|
||||
// Options
|
||||
type opts_t = {
|
||||
ondelete: (photos: IPhoto[]) => void;
|
||||
fetchDay: (dayId: number) => void;
|
||||
};
|
||||
|
||||
export class ViewerManager {
|
||||
/** Map from fileid to Photo */
|
||||
private photoMap = new Map<number, IPhoto>();
|
||||
|
||||
constructor(
|
||||
ondelete: (photos: IPhoto[]) => void,
|
||||
private updateLoading: (delta: number) => void,
|
||||
private $route: Route
|
||||
) {
|
||||
constructor(private opts: opts_t) {
|
||||
subscribe("files:file:deleted", ({ fileid }: { fileid: number }) => {
|
||||
const photo = this.photoMap.get(fileid);
|
||||
ondelete([photo]);
|
||||
opts.ondelete([photo]);
|
||||
});
|
||||
}
|
||||
|
||||
public async open(photo: IPhoto, list?: IPhoto[]) {
|
||||
list = list || photo.d?.detail;
|
||||
if (!list?.length) return;
|
||||
public async open(anchorPhoto: IPhoto, rows?: IRow[]) {
|
||||
// list = list || photo.d?.detail;
|
||||
// if (!list?.length) return;
|
||||
// // Repopulate map
|
||||
// this.photoMap.clear();
|
||||
// for (const p of list) {
|
||||
// this.photoMap.set(p.fileid, p);
|
||||
// }
|
||||
// // Get file infos
|
||||
// let fileInfos: IFileInfo[];
|
||||
// try {
|
||||
// this.updateLoading(1);
|
||||
// fileInfos = await dav.getFiles(list);
|
||||
// } catch (e) {
|
||||
// console.error("Failed to load fileInfos", e);
|
||||
// showError("Failed to load fileInfos");
|
||||
// return;
|
||||
// } finally {
|
||||
// this.updateLoading(-1);
|
||||
// }
|
||||
// if (fileInfos.length === 0) {
|
||||
// return;
|
||||
// }
|
||||
// // Fix sorting of the fileInfos
|
||||
// const itemPositions = {};
|
||||
// for (const [index, p] of list.entries()) {
|
||||
// itemPositions[p.fileid] = index;
|
||||
// }
|
||||
// fileInfos.sort(function (a, b) {
|
||||
// return itemPositions[a.fileid] - itemPositions[b.fileid];
|
||||
// });
|
||||
// // Get this photo in the fileInfos
|
||||
// const fInfo = fileInfos.find((d) => Number(d.fileid) === photo.fileid);
|
||||
// if (!fInfo) {
|
||||
// showError(t("memories", "Cannot find this photo anymore!"));
|
||||
// return;
|
||||
// }
|
||||
// // Check viewer > 2.0.0
|
||||
// const viewerVersion: string = globalThis.OCA.Viewer.version;
|
||||
// const viewerMajor = Number(viewerVersion.split(".")[0]);
|
||||
// // Open Nextcloud viewer
|
||||
// globalThis.OCA.Viewer.open({
|
||||
// fileInfo: fInfo,
|
||||
// path: viewerMajor < 2 ? fInfo.filename : undefined, // Only specify path upto Nextcloud 24
|
||||
// list: fileInfos, // file list
|
||||
// canLoop: false, // don't loop
|
||||
// onClose: () => {
|
||||
// // on viewer close
|
||||
// if (globalThis.OCA.Files.Sidebar.file) {
|
||||
// localStorage.setItem(SIDEBAR_KEY, "1");
|
||||
// } else {
|
||||
// localStorage.removeItem(SIDEBAR_KEY);
|
||||
// }
|
||||
// globalThis.OCA.Files.Sidebar.close();
|
||||
// },
|
||||
// });
|
||||
// // Restore sidebar state
|
||||
// if (localStorage.getItem(SIDEBAR_KEY) === "1") {
|
||||
// globalThis.OCA.Files.Sidebar.open(fInfo.filename);
|
||||
// }
|
||||
|
||||
// Repopulate map
|
||||
this.photoMap.clear();
|
||||
for (const p of list) {
|
||||
this.photoMap.set(p.fileid, p);
|
||||
}
|
||||
const list = [...anchorPhoto.d.detail];
|
||||
const days = new Map<number, IDay>();
|
||||
const dayIds = [];
|
||||
|
||||
// Get file infos
|
||||
let fileInfos: IFileInfo[];
|
||||
try {
|
||||
this.updateLoading(1);
|
||||
fileInfos = await dav.getFiles(list);
|
||||
} catch (e) {
|
||||
console.error("Failed to load fileInfos", e);
|
||||
showError("Failed to load fileInfos");
|
||||
return;
|
||||
} finally {
|
||||
this.updateLoading(-1);
|
||||
}
|
||||
if (fileInfos.length === 0) {
|
||||
return;
|
||||
}
|
||||
let globalCount = 0;
|
||||
let globalAnchor = -1;
|
||||
let localAnchor = -1;
|
||||
|
||||
// Fix sorting of the fileInfos
|
||||
const itemPositions = {};
|
||||
for (const [index, p] of list.entries()) {
|
||||
itemPositions[p.fileid] = index;
|
||||
}
|
||||
fileInfos.sort(function (a, b) {
|
||||
return itemPositions[a.fileid] - itemPositions[b.fileid];
|
||||
});
|
||||
|
||||
// Get this photo in the fileInfos
|
||||
const fInfo = fileInfos.find((d) => Number(d.fileid) === photo.fileid);
|
||||
if (!fInfo) {
|
||||
showError(t("memories", "Cannot find this photo anymore!"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check viewer > 2.0.0
|
||||
const viewerVersion: string = globalThis.OCA.Viewer.version;
|
||||
const viewerMajor = Number(viewerVersion.split(".")[0]);
|
||||
|
||||
// Open Nextcloud viewer
|
||||
globalThis.OCA.Viewer.open({
|
||||
fileInfo: fInfo,
|
||||
path: viewerMajor < 2 ? fInfo.filename : undefined, // Only specify path upto Nextcloud 24
|
||||
list: fileInfos, // file list
|
||||
canLoop: false, // don't loop
|
||||
onClose: () => {
|
||||
// on viewer close
|
||||
if (globalThis.OCA.Files.Sidebar.file) {
|
||||
localStorage.setItem(SIDEBAR_KEY, "1");
|
||||
} else {
|
||||
localStorage.removeItem(SIDEBAR_KEY);
|
||||
for (const r of rows) {
|
||||
if (r.type === IRowType.HEAD) {
|
||||
if (r.day.dayid == anchorPhoto.d.dayid) {
|
||||
localAnchor = r.day.detail.findIndex(
|
||||
(p) => p.fileid === anchorPhoto.fileid
|
||||
);
|
||||
globalAnchor = globalCount + localAnchor;
|
||||
}
|
||||
globalThis.OCA.Files.Sidebar.close();
|
||||
},
|
||||
|
||||
globalCount += r.day.count;
|
||||
days.set(r.day.dayid, r.day);
|
||||
dayIds.push(r.day.dayid);
|
||||
}
|
||||
}
|
||||
|
||||
const lightbox = new PhotoSwipe({
|
||||
// arrowPrev: false,
|
||||
// arrowNext: false,
|
||||
// zoom: false,
|
||||
// close: false,
|
||||
counter: true,
|
||||
loop: false,
|
||||
index: globalAnchor,
|
||||
});
|
||||
|
||||
// Restore sidebar state
|
||||
if (localStorage.getItem(SIDEBAR_KEY) === "1") {
|
||||
globalThis.OCA.Files.Sidebar.open(fInfo.filename);
|
||||
}
|
||||
lightbox.addFilter("uiElement", (element, data) => {
|
||||
// add button-vue class if button
|
||||
if (element.classList.contains("pswp__button")) {
|
||||
element.classList.add("button-vue");
|
||||
}
|
||||
return element;
|
||||
});
|
||||
|
||||
lightbox.addFilter("numItems", (numItems) => {
|
||||
return globalCount;
|
||||
});
|
||||
|
||||
lightbox.addFilter("itemData", (itemData, index) => {
|
||||
console.log(index);
|
||||
|
||||
// Get photo object from list
|
||||
let idx = index - globalAnchor + localAnchor;
|
||||
if (idx < 0) {
|
||||
// Load previous day
|
||||
const firstDayId = list[0].d.dayid;
|
||||
const firstDayIdx = utils.binarySearch(dayIds, firstDayId);
|
||||
if (firstDayIdx === 0) {
|
||||
// No previous day
|
||||
return {};
|
||||
}
|
||||
const prevDayId = dayIds[firstDayIdx - 1];
|
||||
const prevDay = days.get(prevDayId);
|
||||
if (!prevDay.detail) {
|
||||
console.error("[BUG] No detail for previous day");
|
||||
return {};
|
||||
}
|
||||
list.unshift(...prevDay.detail);
|
||||
localAnchor += prevDay.count;
|
||||
} else if (idx >= list.length) {
|
||||
// Load next day
|
||||
const lastDayId = list[list.length - 1].d.dayid;
|
||||
const lastDayIdx = utils.binarySearch(dayIds, lastDayId);
|
||||
if (lastDayIdx === dayIds.length - 1) {
|
||||
// No next day
|
||||
return {};
|
||||
}
|
||||
const nextDayId = dayIds[lastDayIdx + 1];
|
||||
const nextDay = days.get(nextDayId);
|
||||
if (!nextDay.detail) {
|
||||
console.error("[BUG] No detail for next day");
|
||||
return {};
|
||||
}
|
||||
list.push(...nextDay.detail);
|
||||
}
|
||||
|
||||
idx = index - globalAnchor + localAnchor;
|
||||
const photo = list[idx];
|
||||
|
||||
// Something went really wrong
|
||||
if (!photo) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Preload next and previous 3 days
|
||||
const dayIdx = utils.binarySearch(dayIds, photo.d.dayid);
|
||||
const preload = (idx: number) => {
|
||||
if (idx > 0 && idx < dayIds.length && !days.get(dayIds[idx]).detail) {
|
||||
this.opts.fetchDay(dayIds[idx]);
|
||||
console.log("Preload", dayIds[idx]);
|
||||
}
|
||||
};
|
||||
preload(dayIdx - 1);
|
||||
preload(dayIdx - 2);
|
||||
preload(dayIdx - 3);
|
||||
preload(dayIdx + 1);
|
||||
preload(dayIdx + 2);
|
||||
preload(dayIdx + 3);
|
||||
|
||||
// Get thumb image
|
||||
const thumbSrc: string =
|
||||
this.thumbElem(photo)?.querySelector("img")?.getAttribute("src") ||
|
||||
getPreviewUrl(photo, false, 256);
|
||||
|
||||
// Get full image
|
||||
return {
|
||||
src: getPreviewUrl(photo, false, 256),
|
||||
msrc: thumbSrc,
|
||||
width: photo.w || undefined,
|
||||
height: photo.h || undefined,
|
||||
thumbCropped: true,
|
||||
};
|
||||
});
|
||||
|
||||
lightbox.addFilter("thumbEl", (thumbEl, data, index) => {
|
||||
const photo = list[index - globalAnchor + localAnchor];
|
||||
return this.thumbElem(photo) || thumbEl;
|
||||
});
|
||||
|
||||
lightbox.init();
|
||||
}
|
||||
|
||||
private thumbElem(photo: IPhoto) {
|
||||
if (!photo) return;
|
||||
return document.getElementById(
|
||||
`memories-photo-${photo.key || photo.fileid}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue