refactor: enable strict null checking
Signed-off-by: Varun Patil <radialapps@gmail.com>pull/602/head
parent
516917a0e0
commit
65448ed4c0
|
@ -641,7 +641,7 @@ export default defineComponent({
|
|||
|
||||
loading: 0,
|
||||
|
||||
status: null as IStatus,
|
||||
status: null as IStatus | null,
|
||||
}),
|
||||
|
||||
mounted() {
|
||||
|
@ -680,7 +680,7 @@ export default defineComponent({
|
|||
}
|
||||
},
|
||||
|
||||
async update(key: string, value = null) {
|
||||
async update(key: string, value: any = null) {
|
||||
value = value ?? this[key];
|
||||
const setting = settings[key];
|
||||
|
||||
|
@ -727,7 +727,7 @@ export default defineComponent({
|
|||
"This may also cause all photos to be re-indexed!"
|
||||
);
|
||||
const msg =
|
||||
(this.status.gis_count ? warnSetup : warnLong) + " " + warnReindex;
|
||||
(this.status?.gis_count ? warnSetup : warnLong) + " " + warnReindex;
|
||||
if (!confirm(msg)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
@ -805,6 +805,8 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
gisStatus() {
|
||||
if (!this.status) return "";
|
||||
|
||||
if (typeof this.status.gis_type !== "number") {
|
||||
return this.status.gis_type;
|
||||
}
|
||||
|
@ -825,7 +827,7 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
gisStatusType() {
|
||||
return typeof this.status.gis_type !== "number" ||
|
||||
return typeof this.status?.gis_type !== "number" ||
|
||||
this.status.gis_type <= 0
|
||||
? "error"
|
||||
: "success";
|
||||
|
@ -836,6 +838,8 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
vaapiStatusText(): string {
|
||||
if (!this.status) return "";
|
||||
|
||||
const dev = "/dev/dri/renderD128";
|
||||
if (this.status.vaapi_dev === "ok") {
|
||||
return this.t("memories", "VA-API device ({dev}) is readable", { dev });
|
||||
|
@ -855,7 +859,7 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
vaapiStatusType(): string {
|
||||
return this.status.vaapi_dev === "ok" ? "success" : "error";
|
||||
return this.status?.vaapi_dev === "ok" ? "success" : "error";
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
23
src/App.vue
23
src/App.vue
|
@ -89,6 +89,13 @@ import TagsIcon from "vue-material-design-icons/Tag.vue";
|
|||
import MapIcon from "vue-material-design-icons/Map.vue";
|
||||
import CogIcon from "vue-material-design-icons/Cog.vue";
|
||||
|
||||
type NavItem = {
|
||||
name: string;
|
||||
title: string;
|
||||
icon: any;
|
||||
if?: any;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: "App",
|
||||
components: {
|
||||
|
@ -122,7 +129,7 @@ export default defineComponent({
|
|||
mixins: [UserConfig],
|
||||
|
||||
data: () => ({
|
||||
navItems: [],
|
||||
navItems: [] as NavItem[],
|
||||
metadataComponent: null as any,
|
||||
settingsOpen: false,
|
||||
}),
|
||||
|
@ -133,7 +140,7 @@ export default defineComponent({
|
|||
return Number(version[0]);
|
||||
},
|
||||
|
||||
recognize(): string | boolean {
|
||||
recognize(): string | false {
|
||||
if (!this.config_recognizeEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
@ -145,7 +152,7 @@ export default defineComponent({
|
|||
return t("memories", "People");
|
||||
},
|
||||
|
||||
facerecognition(): string | boolean {
|
||||
facerecognition(): string | false {
|
||||
if (!this.config_facerecognitionInstalled) {
|
||||
return false;
|
||||
}
|
||||
|
@ -189,7 +196,7 @@ export default defineComponent({
|
|||
const onResize = () => {
|
||||
globalThis.windowInnerWidth = window.innerWidth;
|
||||
globalThis.windowInnerHeight = window.innerHeight;
|
||||
emit("memories:window:resize", null);
|
||||
emit("memories:window:resize", {});
|
||||
};
|
||||
window.addEventListener("resize", () => {
|
||||
utils.setRenewingTimeout(this, "resizeTimer", onResize, 100);
|
||||
|
@ -258,7 +265,7 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
methods: {
|
||||
navItemsAll() {
|
||||
navItemsAll(): NavItem[] {
|
||||
return [
|
||||
{
|
||||
name: "timeline",
|
||||
|
@ -289,13 +296,13 @@ export default defineComponent({
|
|||
{
|
||||
name: "recognize",
|
||||
icon: PeopleIcon,
|
||||
title: this.recognize,
|
||||
title: this.recognize || "",
|
||||
if: this.recognize,
|
||||
},
|
||||
{
|
||||
name: "facerecognition",
|
||||
icon: PeopleIcon,
|
||||
title: this.facerecognition,
|
||||
title: this.facerecognition || "",
|
||||
if: this.facerecognition,
|
||||
},
|
||||
{
|
||||
|
@ -334,7 +341,7 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
doRouteChecks() {
|
||||
if (this.$route.name.endsWith("-share")) {
|
||||
if (this.$route.name?.endsWith("-share")) {
|
||||
this.putShareToken(<string>this.$route.params.token);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import Cluster from "./frame/Cluster.vue";
|
||||
import { ICluster } from "../types";
|
||||
import type { ICluster } from "../types";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ClusterGrid",
|
||||
|
|
|
@ -21,7 +21,7 @@ import EmptyContent from "./top-matter/EmptyContent.vue";
|
|||
|
||||
import * as dav from "../services/DavRequests";
|
||||
|
||||
import { ICluster } from "../types";
|
||||
import type { ICluster } from "../types";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ClusterView",
|
||||
|
|
|
@ -59,7 +59,7 @@ import { getCurrentUser } from "@nextcloud/auth";
|
|||
import axios from "@nextcloud/axios";
|
||||
|
||||
import banner from "../assets/banner.svg";
|
||||
import { IDay } from "../types";
|
||||
import type { IDay } from "../types";
|
||||
import { API } from "../services/API";
|
||||
|
||||
export default defineComponent({
|
||||
|
|
|
@ -12,7 +12,7 @@ import { defineComponent } from "vue";
|
|||
import UserConfig from "../mixins/UserConfig";
|
||||
import Folder from "./frame/Folder.vue";
|
||||
|
||||
import { IFolder } from "../types";
|
||||
import type { IFolder } from "../types";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ClusterGrid",
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
<NcActions :inline="1">
|
||||
<NcActionButton
|
||||
:aria-label="t('memories', 'Edit')"
|
||||
@click="field.edit()"
|
||||
@click="field.edit?.()"
|
||||
>
|
||||
{{ t("memories", "Edit") }}
|
||||
<template #icon> <EditIcon :size="20" /> </template>
|
||||
|
@ -72,7 +72,7 @@ import InfoIcon from "vue-material-design-icons/InformationOutline.vue";
|
|||
import LocationIcon from "vue-material-design-icons/MapMarker.vue";
|
||||
import TagIcon from "vue-material-design-icons/Tag.vue";
|
||||
import { API } from "../services/API";
|
||||
import { IImageInfo } from "../types";
|
||||
import type { IImageInfo } from "../types";
|
||||
|
||||
interface TopField {
|
||||
title: string;
|
||||
|
@ -112,8 +112,8 @@ export default defineComponent({
|
|||
|
||||
if (this.dateOriginal) {
|
||||
list.push({
|
||||
title: this.dateOriginalStr,
|
||||
subtitle: this.dateOriginalTime,
|
||||
title: this.dateOriginalStr!,
|
||||
subtitle: this.dateOriginalTime!,
|
||||
icon: CalendarIcon,
|
||||
edit: () =>
|
||||
globalThis.editMetadata([globalThis.currentViewerPhoto], [1]),
|
||||
|
@ -228,13 +228,13 @@ export default defineComponent({
|
|||
return `${make} ${model}`;
|
||||
},
|
||||
|
||||
cameraSub(): string[] | null {
|
||||
cameraSub(): string[] {
|
||||
const f = this.exif["FNumber"] || this.exif["Aperture"];
|
||||
const s = this.shutterSpeed;
|
||||
const len = this.exif["FocalLength"];
|
||||
const iso = this.exif["ISO"];
|
||||
|
||||
const parts = [];
|
||||
const parts: string[] = [];
|
||||
if (f) parts.push(`f/${f}`);
|
||||
if (s) parts.push(`${s}`);
|
||||
if (len) parts.push(`${len}mm`);
|
||||
|
@ -263,8 +263,8 @@ export default defineComponent({
|
|||
return this.baseInfo.basename;
|
||||
},
|
||||
|
||||
imageInfoSub(): string[] | null {
|
||||
let parts = [];
|
||||
imageInfoSub(): string[] {
|
||||
let parts: string[] = [];
|
||||
let mp = Number(this.exif["Megapixels"]);
|
||||
|
||||
if (this.baseInfo.w && this.baseInfo.h) {
|
||||
|
@ -282,7 +282,7 @@ export default defineComponent({
|
|||
return parts;
|
||||
},
|
||||
|
||||
address(): string | null {
|
||||
address(): string | undefined {
|
||||
return this.baseInfo.address;
|
||||
},
|
||||
|
||||
|
@ -298,11 +298,11 @@ export default defineComponent({
|
|||
return Object.values(this.baseInfo?.tags || {});
|
||||
},
|
||||
|
||||
tagNamesStr(): string {
|
||||
tagNamesStr(): string | null {
|
||||
return this.tagNames.length > 0 ? this.tagNames.join(", ") : null;
|
||||
},
|
||||
|
||||
mapUrl(): string | null {
|
||||
mapUrl(): string {
|
||||
const boxSize = 0.0075;
|
||||
const bbox = [
|
||||
this.lon - boxSize,
|
||||
|
@ -314,7 +314,7 @@ export default defineComponent({
|
|||
return `https://www.openstreetmap.org/export/embed.html?bbox=${bbox.join()}&marker=${m}`;
|
||||
},
|
||||
|
||||
mapFullUrl(): string | null {
|
||||
mapFullUrl(): string {
|
||||
return `https://www.openstreetmap.org/?mlat=${this.lat}&mlon=${this.lon}#map=18/${this.lat}/${this.lon}`;
|
||||
},
|
||||
},
|
||||
|
@ -336,7 +336,7 @@ export default defineComponent({
|
|||
return this.baseInfo;
|
||||
},
|
||||
|
||||
handleFileUpdated({ fileid }) {
|
||||
handleFileUpdated({ fileid }: { fileid: number }) {
|
||||
if (fileid && this.fileid === fileid) {
|
||||
this.update(this.fileid);
|
||||
}
|
||||
|
|
|
@ -66,13 +66,25 @@ export default defineComponent({
|
|||
|
||||
props: {
|
||||
/** Rows from Timeline */
|
||||
rows: Array as PropType<IRow[]>,
|
||||
rows: {
|
||||
type: Array as PropType<IRow[]>,
|
||||
required: true,
|
||||
},
|
||||
/** Total height */
|
||||
height: Number,
|
||||
height: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
/** Actual recycler component */
|
||||
recycler: Object,
|
||||
recycler: {
|
||||
type: Object,
|
||||
required: false,
|
||||
},
|
||||
/** Recycler before slot component */
|
||||
recyclerBefore: HTMLDivElement,
|
||||
recyclerBefore: {
|
||||
type: HTMLDivElement,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
|
@ -81,7 +93,7 @@ export default defineComponent({
|
|||
/** Height of the entire photo view */
|
||||
recyclerHeight: 100,
|
||||
/** Rect of scroller */
|
||||
scrollerRect: null as DOMRect,
|
||||
scrollerRect: null as DOMRect | null,
|
||||
/** Computed ticks */
|
||||
ticks: [] as ITick[],
|
||||
/** Computed cursor top */
|
||||
|
@ -273,8 +285,8 @@ export default defineComponent({
|
|||
/** Do adjustment synchronously */
|
||||
adjustNow() {
|
||||
// Refresh height of recycler
|
||||
this.recyclerHeight = this.recycler.$refs.wrapper.clientHeight;
|
||||
const extraY = this.recyclerBefore?.clientHeight || 0;
|
||||
this.recyclerHeight = this.recycler?.$refs.wrapper.clientHeight ?? 0;
|
||||
const extraY = this.recyclerBefore?.clientHeight ?? 0;
|
||||
|
||||
// Start with the first tick. Walk over all rows counting the
|
||||
// y position. When you hit a row with the tick, update y and
|
||||
|
@ -417,7 +429,7 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
const date = utils.dayIdToDate(dayId);
|
||||
this.hoverCursorText = utils.getShortDateStr(date);
|
||||
this.hoverCursorText = utils.getShortDateStr(date) ?? "";
|
||||
},
|
||||
|
||||
/** Handle mouse hover */
|
||||
|
@ -480,7 +492,7 @@ export default defineComponent({
|
|||
|
||||
if (this.lastRequestedRecyclerY !== targetY) {
|
||||
this.lastRequestedRecyclerY = targetY;
|
||||
this.recycler.scrollToPosition(targetY);
|
||||
this.recycler?.scrollToPosition(targetY);
|
||||
}
|
||||
|
||||
this.handleScroll();
|
||||
|
@ -494,6 +506,7 @@ export default defineComponent({
|
|||
|
||||
/** Handle touch */
|
||||
touchmove(event: any) {
|
||||
if (!this.scrollerRect) return;
|
||||
let y = event.targetTouches[0].pageY - this.scrollerRect.top;
|
||||
y = Math.max(0, y - 20); // middle of touch finger
|
||||
this.moveto(y, true);
|
||||
|
|
|
@ -103,27 +103,39 @@ export default defineComponent({
|
|||
mixins: [UserConfig],
|
||||
|
||||
props: {
|
||||
heads: Object as PropType<{ [dayid: number]: IHeadRow }>,
|
||||
heads: {
|
||||
type: Object as PropType<{ [dayid: number]: IHeadRow }>,
|
||||
required: true,
|
||||
},
|
||||
/** List of rows for multi selection */
|
||||
rows: Array as PropType<IRow[]>,
|
||||
rows: {
|
||||
type: Array as PropType<IRow[]>,
|
||||
required: true,
|
||||
},
|
||||
/** Rows are in ascending order (desc is normal) */
|
||||
isreverse: Boolean,
|
||||
isreverse: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
/** Recycler element to scroll during touch multi-select */
|
||||
recycler: Object,
|
||||
recycler: {
|
||||
type: HTMLDivElement,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
show: false,
|
||||
size: 0,
|
||||
selection: new Map<number, IPhoto>(),
|
||||
defaultActions: null as ISelectionAction[],
|
||||
defaultActions: null! as ISelectionAction[],
|
||||
|
||||
touchAnchor: null as IPhoto,
|
||||
prevTouch: null as Touch,
|
||||
touchAnchor: null as IPhoto | null,
|
||||
prevTouch: null as Touch | null,
|
||||
touchTimer: 0,
|
||||
touchMoved: false,
|
||||
touchPrevSel: null as Selection,
|
||||
prevOver: null as IPhoto,
|
||||
touchPrevSel: null as Selection | null,
|
||||
prevOver: null as IPhoto | null,
|
||||
touchScrollInterval: 0,
|
||||
touchScrollDelta: 0,
|
||||
}),
|
||||
|
@ -225,6 +237,14 @@ export default defineComponent({
|
|||
this.$emit("delete", photos);
|
||||
},
|
||||
|
||||
deleteSelectedPhotosById(delIds: number[], selection: Selection) {
|
||||
return this.deletePhotos(
|
||||
delIds
|
||||
.map((id) => selection.get(id))
|
||||
.filter((p): p is IPhoto => p !== undefined)
|
||||
);
|
||||
},
|
||||
|
||||
updateLoading(delta: number) {
|
||||
this.$emit("updateLoading", delta);
|
||||
},
|
||||
|
@ -351,7 +371,7 @@ export default defineComponent({
|
|||
window.clearTimeout(this.touchTimer);
|
||||
this.touchTimer = 0;
|
||||
this.touchMoved = false;
|
||||
this.prevOver = undefined;
|
||||
this.prevOver = null;
|
||||
|
||||
window.cancelAnimationFrame(this.touchScrollInterval);
|
||||
this.touchScrollInterval = 0;
|
||||
|
@ -419,7 +439,8 @@ export default defineComponent({
|
|||
let frameCount = 3;
|
||||
|
||||
const fun = () => {
|
||||
this.recycler.$el.scrollTop += this.touchScrollDelta;
|
||||
if (!this.prevTouch) return;
|
||||
this.recycler!.scrollTop += this.touchScrollDelta;
|
||||
|
||||
if (frameCount++ >= 3) {
|
||||
this.touchMoveSelect(this.prevTouch, rowIdx);
|
||||
|
@ -442,11 +463,14 @@ export default defineComponent({
|
|||
|
||||
/** Multi-select triggered by touchmove */
|
||||
touchMoveSelect(touch: Touch, rowIdx: number) {
|
||||
// Assertions
|
||||
if (!this.touchAnchor) return;
|
||||
|
||||
// Which photo is the cursor over, if any
|
||||
const elem: any = document
|
||||
.elementFromPoint(touch.clientX, touch.clientY)
|
||||
?.closest(".p-outer-super");
|
||||
let overPhoto: IPhoto = elem?.__vue__?.data;
|
||||
let overPhoto: IPhoto | null = elem?.__vue__?.data;
|
||||
if (overPhoto && overPhoto.flag & this.c.FLAG_PLACEHOLDER)
|
||||
overPhoto = null;
|
||||
|
||||
|
@ -460,7 +484,8 @@ export default defineComponent({
|
|||
// days reverse XOR rows reverse
|
||||
let reverse: boolean;
|
||||
if (overPhoto.dayid === this.touchAnchor.dayid) {
|
||||
const l = overPhoto.d.detail;
|
||||
const l = overPhoto?.d?.detail;
|
||||
if (!l) return; // Shouldn't happen
|
||||
const ai = l.indexOf(this.touchAnchor);
|
||||
const oi = l.indexOf(overPhoto);
|
||||
if (ai === -1 || oi === -1) return; // Shouldn't happen
|
||||
|
@ -474,14 +499,16 @@ export default defineComponent({
|
|||
|
||||
// Walk over rows
|
||||
let i = rowIdx;
|
||||
let j = this.rows[i].photos.indexOf(this.touchAnchor);
|
||||
let j = this.rows[i].photos?.indexOf(this.touchAnchor) ?? -2;
|
||||
if (j === -2) return; // row is not initialized yet?!
|
||||
while (true) {
|
||||
if (j < 0) {
|
||||
while (i > 0 && !this.rows[--i].photos);
|
||||
if (!this.rows[i].photos) break;
|
||||
j = this.rows[i].photos.length - 1;
|
||||
const plen = this.rows[i].photos?.length;
|
||||
if (!plen) break;
|
||||
j = plen - 1;
|
||||
continue;
|
||||
} else if (j >= this.rows[i].photos.length) {
|
||||
} else if (j >= this.rows[i].photos!.length) {
|
||||
while (i < this.rows.length - 1 && !this.rows[++i].photos);
|
||||
if (!this.rows[i].photos) break;
|
||||
j = 0;
|
||||
|
@ -549,7 +576,7 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
if (!noUpdate) {
|
||||
this.updateHeadSelected(this.heads[photo.d.dayid]);
|
||||
this.updateHeadSelected(this.heads[photo.dayid]);
|
||||
this.$forceUpdate();
|
||||
}
|
||||
},
|
||||
|
@ -557,11 +584,11 @@ export default defineComponent({
|
|||
/** Multi-select */
|
||||
selectMulti(photo: IPhoto, rows: IRow[], rowIdx: number) {
|
||||
const pRow = rows[rowIdx];
|
||||
const pIdx = pRow.photos.indexOf(photo);
|
||||
const pIdx = pRow.photos?.indexOf(photo) ?? -1;
|
||||
if (pIdx === -1) return;
|
||||
|
||||
const updateDaySet = new Set<number>();
|
||||
let behind = [];
|
||||
let behind: IPhoto[] = [];
|
||||
let behindFound = false;
|
||||
|
||||
// Look behind
|
||||
|
@ -570,16 +597,16 @@ export default defineComponent({
|
|||
if (rows[i].type !== IRowType.PHOTOS) continue;
|
||||
if (!rows[i].photos?.length) break;
|
||||
|
||||
const sj = i === rowIdx ? pIdx : rows[i].photos.length - 1;
|
||||
const sj = i === rowIdx ? pIdx : rows[i].photos!.length - 1;
|
||||
for (let j = sj; j >= 0; j--) {
|
||||
const p = rows[i].photos[j];
|
||||
const p = rows[i].photos![j];
|
||||
if (p.flag & this.c.FLAG_PLACEHOLDER || !p.fileid) continue;
|
||||
if (p.flag & this.c.FLAG_SELECTED) {
|
||||
behindFound = true;
|
||||
break;
|
||||
}
|
||||
behind.push(p);
|
||||
updateDaySet.add(p.d.dayid);
|
||||
updateDaySet.add(p.dayid);
|
||||
}
|
||||
|
||||
if (behindFound) break;
|
||||
|
@ -587,23 +614,25 @@ export default defineComponent({
|
|||
|
||||
// Select everything behind
|
||||
if (behindFound) {
|
||||
const detail = photo.d!.detail!;
|
||||
|
||||
// Clear everything in front in this day
|
||||
const pdIdx = photo.d.detail.indexOf(photo);
|
||||
for (let i = pdIdx + 1; i < photo.d.detail.length; i++) {
|
||||
const p = photo.d.detail[i];
|
||||
if (p.flag & this.c.FLAG_SELECTED) this.selectPhoto(p, false, true);
|
||||
const pdIdx = detail.indexOf(photo);
|
||||
for (let i = pdIdx + 1; i < detail.length; i++) {
|
||||
if (detail[i].flag & this.c.FLAG_SELECTED)
|
||||
this.selectPhoto(detail[i], false, true);
|
||||
}
|
||||
|
||||
// Clear everything else in front
|
||||
Array.from(this.selection.values())
|
||||
.filter((p: IPhoto) => {
|
||||
return this.isreverse
|
||||
? p.d.dayid > photo.d.dayid
|
||||
: p.d.dayid < photo.d.dayid;
|
||||
? p.dayid > photo.dayid
|
||||
: p.dayid < photo.dayid;
|
||||
})
|
||||
.forEach((photo: IPhoto) => {
|
||||
this.selectPhoto(photo, false, true);
|
||||
updateDaySet.add(photo.d.dayid);
|
||||
updateDaySet.add(photo.dayid);
|
||||
});
|
||||
|
||||
behind.forEach((p) => this.selectPhoto(p, true, true));
|
||||
|
@ -615,8 +644,8 @@ export default defineComponent({
|
|||
/** Select or deselect all photos in a head */
|
||||
selectHead(head: IHeadRow) {
|
||||
head.selected = !head.selected;
|
||||
for (const row of head.day.rows) {
|
||||
for (const photo of row.photos) {
|
||||
for (const row of head.day.rows ?? []) {
|
||||
for (const photo of row.photos ?? []) {
|
||||
this.selectPhoto(photo, head.selected, true);
|
||||
}
|
||||
}
|
||||
|
@ -628,8 +657,8 @@ export default defineComponent({
|
|||
let selected = true;
|
||||
|
||||
// Check if all photos are selected
|
||||
for (const row of head.day.rows) {
|
||||
for (const photo of row.photos) {
|
||||
for (const row of head.day.rows ?? []) {
|
||||
for (const photo of row.photos ?? []) {
|
||||
if (!(photo.flag & this.c.FLAG_SELECTED)) {
|
||||
selected = false;
|
||||
break;
|
||||
|
@ -647,7 +676,7 @@ export default defineComponent({
|
|||
const toClear = only || this.selection.values();
|
||||
Array.from(toClear).forEach((photo: IPhoto) => {
|
||||
photo.flag &= ~this.c.FLAG_SELECTED;
|
||||
heads.add(this.heads[photo.d.dayid]);
|
||||
heads.add(this.heads[photo.dayid]);
|
||||
this.selection.delete(photo.fileid);
|
||||
this.selectionChanged();
|
||||
});
|
||||
|
@ -663,7 +692,7 @@ export default defineComponent({
|
|||
|
||||
// FileID => Photo for new day
|
||||
const dayMap = new Map<number, IPhoto>();
|
||||
day.detail.forEach((photo) => {
|
||||
day.detail?.forEach((photo) => {
|
||||
dayMap.set(photo.fileid, photo);
|
||||
});
|
||||
|
||||
|
@ -674,13 +703,13 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
// Remove all selections that are not in the new day
|
||||
if (!dayMap.has(fileid)) {
|
||||
const newPhoto = dayMap.get(fileid);
|
||||
if (!newPhoto) {
|
||||
this.selection.delete(fileid);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the photo object
|
||||
const newPhoto = dayMap.get(fileid);
|
||||
this.selection.set(fileid, newPhoto);
|
||||
newPhoto.flag |= this.c.FLAG_SELECTED;
|
||||
});
|
||||
|
@ -749,10 +778,7 @@ export default defineComponent({
|
|||
for await (const delIds of dav.deletePhotos(
|
||||
Array.from(selection.values())
|
||||
)) {
|
||||
const delPhotos = delIds
|
||||
.filter((id) => id)
|
||||
.map((id) => selection.get(id));
|
||||
this.deletePhotos(delPhotos);
|
||||
this.deleteSelectedPhotosById(delIds, selection);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -793,12 +819,7 @@ export default defineComponent({
|
|||
Array.from(selection.keys()),
|
||||
!this.routeIsArchive()
|
||||
)) {
|
||||
delIds = delIds.filter((x) => x);
|
||||
if (delIds.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const delPhotos = delIds.map((id) => selection.get(id));
|
||||
this.deletePhotos(delPhotos);
|
||||
this.deleteSelectedPhotosById(delIds, selection);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -858,10 +879,7 @@ export default defineComponent({
|
|||
<string>name,
|
||||
Array.from(selection.values())
|
||||
)) {
|
||||
const delPhotos = delIds
|
||||
.filter((x) => x)
|
||||
.map((id) => selection.get(id));
|
||||
this.deletePhotos(delPhotos);
|
||||
this.deleteSelectedPhotosById(delIds, selection);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -110,7 +110,7 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
handleClose() {
|
||||
emit("memories:sidebar:closed", null);
|
||||
emit("memories:sidebar:closed", {});
|
||||
},
|
||||
|
||||
handleOpen() {
|
||||
|
@ -120,7 +120,7 @@ export default defineComponent({
|
|||
if (e.key.length === 1) e.stopPropagation();
|
||||
});
|
||||
|
||||
emit("memories:sidebar:opened", null);
|
||||
emit("memories:sidebar:opened", {});
|
||||
},
|
||||
|
||||
handleNativeOpen() {
|
||||
|
|
|
@ -46,7 +46,7 @@ export default defineComponent({
|
|||
primaryPos: 0,
|
||||
containerSize: 0,
|
||||
mobileOpen: 1,
|
||||
hammer: null as HammerManager,
|
||||
hammer: null as HammerManager | null,
|
||||
photoCount: 0,
|
||||
}),
|
||||
|
||||
|
@ -85,7 +85,7 @@ export default defineComponent({
|
|||
|
||||
beforeDestroy() {
|
||||
this.pointerUp();
|
||||
this.hammer.destroy();
|
||||
this.hammer?.destroy();
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -133,7 +133,7 @@ export default defineComponent({
|
|||
this.pointerDown = false;
|
||||
document.removeEventListener("pointermove", this.documentPointerMove);
|
||||
document.removeEventListener("pointerup", this.pointerUp);
|
||||
emit("memories:window:resize", null);
|
||||
emit("memories:window:resize", {});
|
||||
},
|
||||
|
||||
setFlexBasis(pos: { clientX: number; clientY: number }) {
|
||||
|
@ -154,7 +154,7 @@ export default defineComponent({
|
|||
// so that we can prepare in advance for showing more photos
|
||||
// on the timeline
|
||||
await this.$nextTick();
|
||||
emit("memories:window:resize", null);
|
||||
emit("memories:window:resize", {});
|
||||
},
|
||||
|
||||
async mobileSwipeDown() {
|
||||
|
@ -165,7 +165,7 @@ export default defineComponent({
|
|||
// ends. Note that this is necesary: the height of the timeline inner
|
||||
// div is also animated to the smaller size.
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
emit("memories:window:resize", null);
|
||||
emit("memories:window:resize", {});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -88,7 +88,7 @@
|
|||
:heads="heads"
|
||||
:rows="list"
|
||||
:isreverse="isMonthView"
|
||||
:recycler="$refs.recycler"
|
||||
:recycler="$refs.recycler?.$el"
|
||||
@refresh="softRefresh"
|
||||
@delete="deleteFromViewWithAnimation"
|
||||
@updateLoading="updateLoading"
|
||||
|
@ -374,8 +374,8 @@ export default defineComponent({
|
|||
this.loadedDays.clear();
|
||||
this.sizedDays.clear();
|
||||
this.fetchDayQueue = [];
|
||||
window.clearTimeout(this.fetchDayTimer);
|
||||
window.clearTimeout(this.resizeTimer);
|
||||
window.clearTimeout(this.fetchDayTimer ?? 0);
|
||||
window.clearTimeout(this.resizeTimer ?? 0);
|
||||
},
|
||||
|
||||
/** Recreate everything */
|
||||
|
@ -492,7 +492,7 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
// Initialize photos and add placeholders
|
||||
if (row.pct && !row.photos.length) {
|
||||
if (row.pct && !row.photos?.length) {
|
||||
row.photos = new Array(row.pct);
|
||||
for (let j = 0; j < row.pct; j++) {
|
||||
// Any row that has placeholders has ONLY placeholders
|
||||
|
@ -500,6 +500,7 @@ export default defineComponent({
|
|||
row.photos[j] = {
|
||||
flag: this.c.FLAG_PLACEHOLDER,
|
||||
fileid: Math.random(),
|
||||
dayid: row.dayId,
|
||||
dispW: utils.roundHalf(this.rowWidth / this.numCols),
|
||||
dispX: utils.roundHalf((j * this.rowWidth) / this.numCols),
|
||||
dispH: this.rowHeight,
|
||||
|
@ -568,7 +569,7 @@ export default defineComponent({
|
|||
if (this.loadedDays.has(item.dayId)) {
|
||||
if (!this.sizedDays.has(item.dayId)) {
|
||||
// Just quietly reflow without refetching
|
||||
this.processDay(item.dayId, item.day.detail);
|
||||
this.processDay(item.dayId, item.day.detail!);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
@ -692,7 +693,7 @@ export default defineComponent({
|
|||
// Filter out hidden folders
|
||||
if (!this.config_showHidden) {
|
||||
this.folders = this.folders.filter(
|
||||
(f) => !f.name.startsWith(".") && f.previews.length
|
||||
(f) => !f.name.startsWith(".") && f.previews?.length
|
||||
);
|
||||
}
|
||||
},
|
||||
|
@ -716,7 +717,7 @@ export default defineComponent({
|
|||
const cacheUrl = <string>this.$route.name + url;
|
||||
|
||||
// Try cache first
|
||||
let cache: IDay[];
|
||||
let cache: IDay[] | null = null;
|
||||
|
||||
// Make sure to refresh scroll later
|
||||
this.currentEnd = -1;
|
||||
|
@ -730,18 +731,19 @@ export default defineComponent({
|
|||
data = await dav.getOnThisDayData();
|
||||
} else if (dav.isSingleItem()) {
|
||||
data = await dav.getSingleItemData();
|
||||
this.$router.replace(utils.getViewerRoute(data[0]!.detail[0]));
|
||||
this.$router.replace(utils.getViewerRoute(data[0]!.detail![0]));
|
||||
} else {
|
||||
// Try the cache
|
||||
try {
|
||||
cache = noCache ? null : await utils.getCachedData(cacheUrl);
|
||||
if (cache) {
|
||||
await this.processDays(cache);
|
||||
this.loading--;
|
||||
if (!noCache) {
|
||||
try {
|
||||
if ((cache = await utils.getCachedData(cacheUrl))) {
|
||||
await this.processDays(cache);
|
||||
this.loading--;
|
||||
}
|
||||
} catch {
|
||||
console.warn(`Failed to process days cache: ${cacheUrl}`);
|
||||
cache = null;
|
||||
}
|
||||
} catch {
|
||||
console.warn(`Failed to process days cache: ${cacheUrl}`);
|
||||
cache = null;
|
||||
}
|
||||
|
||||
// Get from network
|
||||
|
@ -760,6 +762,7 @@ export default defineComponent({
|
|||
console.error(err);
|
||||
showError(err?.response?.data?.message || err.message);
|
||||
} finally {
|
||||
// If cache is set here, loading was already decremented
|
||||
if (!cache) this.loading--;
|
||||
}
|
||||
},
|
||||
|
@ -891,7 +894,8 @@ export default defineComponent({
|
|||
// Look for cache
|
||||
const cacheUrl = this.getDayUrl(dayId);
|
||||
try {
|
||||
this.processDay(dayId, await utils.getCachedData(cacheUrl));
|
||||
const cache = await utils.getCachedData<IPhoto[]>(cacheUrl);
|
||||
if (cache) this.processDay(dayId, cache);
|
||||
} catch {
|
||||
console.warn(`Failed to process day cache: ${cacheUrl}`);
|
||||
}
|
||||
|
@ -936,8 +940,8 @@ export default defineComponent({
|
|||
// It is already sorted in dayid DESC
|
||||
const dayMap = new Map<number, IPhoto[]>();
|
||||
for (const photo of data) {
|
||||
if (!dayMap.get(photo.dayid)) dayMap.set(photo.dayid, []);
|
||||
dayMap.get(photo.dayid).push(photo);
|
||||
if (!dayMap.has(photo.dayid)) dayMap.set(photo.dayid, []);
|
||||
dayMap.get(photo.dayid)!.push(photo);
|
||||
}
|
||||
|
||||
// Store cache asynchronously
|
||||
|
@ -997,9 +1001,10 @@ export default defineComponent({
|
|||
// Set and make reactive
|
||||
day.count = data.length;
|
||||
day.detail = data;
|
||||
day.rows ??= [];
|
||||
|
||||
// Reset rows including placeholders
|
||||
for (const row of head.day.rows || []) {
|
||||
for (const row of day.rows) {
|
||||
row.photos = [];
|
||||
}
|
||||
|
||||
|
@ -1136,8 +1141,8 @@ export default defineComponent({
|
|||
// These may be valid, e.g. in face rects. All we need to have
|
||||
// is a unique Vue key for the v-for loop.
|
||||
const key = photo.faceid || photo.fileid;
|
||||
if (seen.has(key)) {
|
||||
const val = seen.get(key);
|
||||
const val = seen.get(key);
|
||||
if (val) {
|
||||
photo.key = `${key}-${val}`;
|
||||
seen.set(key, val + 1);
|
||||
} else {
|
||||
|
@ -1146,7 +1151,7 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
// Add photo to row
|
||||
row.photos.push(photo);
|
||||
row.photos!.push(photo);
|
||||
delete row.pct;
|
||||
}
|
||||
|
||||
|
@ -1187,8 +1192,8 @@ export default defineComponent({
|
|||
needAdjust = true;
|
||||
|
||||
// Remove from day
|
||||
const idx = head.day.rows.indexOf(row);
|
||||
if (idx >= 0) head.day.rows.splice(idx, 1);
|
||||
const idx = day.rows.indexOf(row);
|
||||
if (idx >= 0) day.rows.splice(idx, 1);
|
||||
}
|
||||
|
||||
// This will be true even if the head is being spliced
|
||||
|
@ -1210,6 +1215,9 @@ export default defineComponent({
|
|||
|
||||
/** Add and get a new blank photos row */
|
||||
addRow(day: IDay): IRow {
|
||||
// Make sure rows exists
|
||||
day.rows ??= [];
|
||||
|
||||
// Create new row
|
||||
const row = {
|
||||
id: `${day.dayid}-${day.rows.length}`,
|
||||
|
@ -1242,7 +1250,7 @@ export default defineComponent({
|
|||
if (delPhotos.length === 0) return;
|
||||
|
||||
// Get all days that need to be updatd
|
||||
const updatedDays = new Set<IDay>(delPhotos.map((p) => p.d));
|
||||
const updatedDays = new Set<IDay>(delPhotos.map((p) => p.d!));
|
||||
const delPhotosSet = new Set(delPhotos);
|
||||
|
||||
// Animate the deletion
|
||||
|
@ -1258,8 +1266,8 @@ export default defineComponent({
|
|||
|
||||
// Reflow all touched days
|
||||
for (const day of updatedDays) {
|
||||
const newDetail = day.detail.filter((p) => !delPhotosSet.has(p));
|
||||
this.processDay(day.dayid, newDetail);
|
||||
const newDetail = day.detail?.filter((p) => !delPhotosSet.has(p));
|
||||
this.processDay(day.dayid, newDetail!);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
@ -36,7 +36,7 @@ import { defineComponent, PropType } from "vue";
|
|||
import { getCurrentUser } from "@nextcloud/auth";
|
||||
import NcCounterBubble from "@nextcloud/vue/dist/Components/NcCounterBubble";
|
||||
|
||||
import { IAlbum, ICluster, IFace } from "../../types";
|
||||
import type { IAlbum, ICluster, IFace, IPhoto } from "../../types";
|
||||
import { getPreviewUrl } from "../../services/utils/helpers";
|
||||
import errorsvg from "../../assets/error.svg";
|
||||
|
||||
|
@ -66,7 +66,11 @@ export default defineComponent({
|
|||
if (this.error) return errorsvg;
|
||||
|
||||
if (this.album) {
|
||||
const mock = { fileid: this.album.last_added_photo, etag: "", flag: 0 };
|
||||
const mock = {
|
||||
fileid: this.album.last_added_photo,
|
||||
etag: this.album.album_id,
|
||||
flag: 0,
|
||||
} as unknown as IPhoto;
|
||||
return getPreviewUrl(mock, true, 512);
|
||||
}
|
||||
|
||||
|
|
|
@ -46,7 +46,10 @@ export default defineComponent({
|
|||
mixins: [UserConfig],
|
||||
|
||||
props: {
|
||||
data: Object as PropType<IFolder>,
|
||||
data: {
|
||||
type: Object as PropType<IFolder>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
|
|
|
@ -99,7 +99,7 @@ export default defineComponent({
|
|||
|
||||
data: () => ({
|
||||
touchTimer: 0,
|
||||
faceSrc: null,
|
||||
faceSrc: null as string | null,
|
||||
}),
|
||||
|
||||
watch: {
|
||||
|
@ -144,6 +144,7 @@ export default defineComponent({
|
|||
if (this.data.liveid) {
|
||||
return utils.getLivePhotoVideoUrl(this.data, true);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
src(): string | null {
|
||||
|
@ -171,7 +172,7 @@ export default defineComponent({
|
|||
let base = 256;
|
||||
|
||||
// Check if displayed size is larger than the image
|
||||
if (this.data.dispH > base * 0.9 && this.data.dispW > base * 0.9) {
|
||||
if (this.data.dispH! > base * 0.9 && this.data.dispW! > base * 0.9) {
|
||||
// Get a bigger image
|
||||
// 1. No trickery here, just get one size bigger. This is to
|
||||
// ensure that the images can be cached even after reflow.
|
||||
|
@ -208,6 +209,7 @@ export default defineComponent({
|
|||
|
||||
const canvas = document.createElement("canvas");
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) return; // failed to create canvas
|
||||
|
||||
canvas.width = img.naturalWidth;
|
||||
canvas.height = img.naturalHeight;
|
||||
|
@ -223,6 +225,7 @@ export default defineComponent({
|
|||
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
if (!blob) return;
|
||||
this.faceSrc = URL.createObjectURL(blob);
|
||||
},
|
||||
"image/jpeg",
|
||||
|
|
|
@ -49,7 +49,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
/** Change stickiness for a BLOB url */
|
||||
export async function sticky(url: string, delta: number) {
|
||||
if (!BLOB_STICKY.has(url)) BLOB_STICKY.set(url, 0);
|
||||
const val = BLOB_STICKY.get(url) + delta;
|
||||
const val = BLOB_STICKY.get(url)! + delta;
|
||||
if (val <= 0) {
|
||||
BLOB_STICKY.delete(url);
|
||||
} else {
|
||||
|
@ -62,15 +62,16 @@ export async function fetchImage(url: string) {
|
|||
startWorker();
|
||||
|
||||
// Check memcache entry
|
||||
if (BLOB_CACHE.has(url)) return BLOB_CACHE.get(url)[1];
|
||||
let entry = BLOB_CACHE.get(url);
|
||||
if (entry) return entry[1];
|
||||
|
||||
// Fetch image
|
||||
const blobUrl = await importer<typeof w.fetchImageSrc>("fetchImageSrc")(url);
|
||||
|
||||
// Check memcache entry again and revoke if it was added in the meantime
|
||||
if (BLOB_CACHE.has(url)) {
|
||||
if ((entry = BLOB_CACHE.get(url))) {
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
return BLOB_CACHE.get(url)[1];
|
||||
return entry[1];
|
||||
}
|
||||
|
||||
// Create new memecache entry
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
import { CacheExpiration } from "workbox-expiration";
|
||||
import { workerExport } from "../../worker";
|
||||
|
||||
type BlobCallback = {
|
||||
interface BlobCallback {
|
||||
resolve: (blob: Blob) => void;
|
||||
reject: (err: Error) => void;
|
||||
};
|
||||
}
|
||||
|
||||
// Queue of requests to fetch preview images
|
||||
type FetchPreviewObject = {
|
||||
interface FetchPreviewObject {
|
||||
origUrl: string;
|
||||
url: URL;
|
||||
fileid: number;
|
||||
reqid: number;
|
||||
done?: boolean;
|
||||
};
|
||||
}
|
||||
let fetchPreviewQueue: FetchPreviewObject[] = [];
|
||||
|
||||
// Pending requests
|
||||
|
@ -22,9 +22,12 @@ const pendingUrls = new Map<string, BlobCallback[]>();
|
|||
// Cache for preview images
|
||||
const cacheName = "images";
|
||||
let imageCache: Cache;
|
||||
(async () => {
|
||||
imageCache = await caches.open(cacheName);
|
||||
})();
|
||||
caches
|
||||
.open(cacheName)
|
||||
.then((c) => (imageCache = c))
|
||||
.catch(() => {
|
||||
/* ignore */
|
||||
});
|
||||
|
||||
// Expiration for cache
|
||||
const expirationManager = new CacheExpiration(cacheName, {
|
||||
|
@ -60,7 +63,9 @@ async function flushPreviewQueue() {
|
|||
// it came from a multipreview, so that we can try fetching
|
||||
// the single image instead
|
||||
const blob = await res.blob();
|
||||
pendingUrls.get(url)?.forEach((cb) => cb?.resolve?.(blob));
|
||||
pendingUrls.get(url)?.forEach((cb) => {
|
||||
cb?.resolve?.(blob);
|
||||
});
|
||||
pendingUrls.delete(url);
|
||||
|
||||
// Cache response
|
||||
|
@ -68,15 +73,17 @@ async function flushPreviewQueue() {
|
|||
};
|
||||
|
||||
// Throw error on URL
|
||||
const reject = (url: string, e: any) => {
|
||||
pendingUrls.get(url)?.forEach((cb) => cb?.reject?.(e));
|
||||
const reject = (url: string, e: any): void => {
|
||||
pendingUrls.get(url)?.forEach((cb) => {
|
||||
cb?.reject?.(e);
|
||||
});
|
||||
pendingUrls.delete(url);
|
||||
};
|
||||
|
||||
// Make a single-file request
|
||||
const fetchOneSafe = async (p: FetchPreviewObject) => {
|
||||
try {
|
||||
resolve(p.origUrl, await fetchOneImage(p.origUrl));
|
||||
await resolve(p.origUrl, await fetchOneImage(p.origUrl));
|
||||
} catch (e) {
|
||||
reject(p.origUrl, e);
|
||||
}
|
||||
|
@ -84,7 +91,8 @@ async function flushPreviewQueue() {
|
|||
|
||||
// Check if only one request, not worth a multipreview
|
||||
if (fetchPreviewQueueCopy.length === 1) {
|
||||
return fetchOneSafe(fetchPreviewQueueCopy[0]);
|
||||
await fetchOneSafe(fetchPreviewQueueCopy[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create aggregated request body
|
||||
|
@ -99,7 +107,8 @@ async function flushPreviewQueue() {
|
|||
try {
|
||||
// Fetch multipreview
|
||||
const res = await fetchMultipreview(files);
|
||||
if (res.status !== 200) throw new Error("Error fetching multi-preview");
|
||||
if (res.status !== 200 || !res.body)
|
||||
throw new Error("Error fetching multi-preview");
|
||||
|
||||
// Create fake headers for 7-day expiry
|
||||
const headers = {
|
||||
|
@ -119,7 +128,7 @@ async function flushPreviewQueue() {
|
|||
reqid: number;
|
||||
len: number;
|
||||
type: string;
|
||||
} = null;
|
||||
} | null = null;
|
||||
|
||||
// Index at which we are currently reading
|
||||
let idx = 0;
|
||||
|
@ -161,30 +170,31 @@ async function flushPreviewQueue() {
|
|||
if (bufSize - jsonStart < jsonLen) break;
|
||||
const jsonB = buffer.slice(jsonStart, jsonStart + jsonLen);
|
||||
const jsonT = new TextDecoder().decode(jsonB);
|
||||
params = JSON.parse(jsonT);
|
||||
idx = jsonStart + jsonLen;
|
||||
params = JSON.parse(jsonT);
|
||||
params = params!;
|
||||
}
|
||||
|
||||
// Read the image data
|
||||
if (bufSize - idx < params.len) break;
|
||||
const imgBlob = new Blob([buffer.slice(idx, idx + params.len)], {
|
||||
type: params.type,
|
||||
if (bufSize - idx < params!.len) break;
|
||||
const imgBlob = new Blob([buffer.slice(idx, idx + params!.len)], {
|
||||
type: params!.type,
|
||||
});
|
||||
idx += params.len;
|
||||
idx += params!.len;
|
||||
|
||||
// Initiate callbacks
|
||||
fetchPreviewQueueCopy
|
||||
.filter((p) => p.reqid === params.reqid && !p.done)
|
||||
.forEach((p) => {
|
||||
for (const p of fetchPreviewQueueCopy) {
|
||||
if (p.reqid === params.reqid && !p.done) {
|
||||
try {
|
||||
const dummy = getResponse(imgBlob, params.type, headers);
|
||||
resolve(p.origUrl, dummy);
|
||||
const dummy = getResponse(imgBlob, params!.type, headers);
|
||||
await resolve(p.origUrl, dummy);
|
||||
p.done = true;
|
||||
} catch (e) {
|
||||
// In case of error, we want to try fetching the single
|
||||
// image instead, so we don't reject here
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Reset for next iteration
|
||||
params = null;
|
||||
|
@ -272,7 +282,7 @@ function getResponse(blob: Blob, type: string | null, headers: any = {}) {
|
|||
"Content-Type": type || headers["content-type"],
|
||||
"Content-Length": blob.size.toString(),
|
||||
"Cache-Control": headers["cache-control"],
|
||||
Expires: headers["expires"],
|
||||
Expires: headers.expires,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -9,11 +9,11 @@
|
|||
const pathname = self.location.pathname;
|
||||
__webpack_public_path__ = pathname.substring(0, pathname.lastIndexOf("/") + 1);
|
||||
|
||||
const missedQueue = [];
|
||||
const missedQueue: any[] = [];
|
||||
self.onmessage = function (val: any) {
|
||||
missedQueue.push(val);
|
||||
};
|
||||
|
||||
import("./XImgWorker").then(function () {
|
||||
missedQueue.forEach((data: any) => self.onmessage(data));
|
||||
missedQueue.forEach((data: any) => self.onmessage?.(data));
|
||||
});
|
||||
|
|
|
@ -210,7 +210,7 @@ export default defineComponent({
|
|||
selectedCollaboratorsKeys: [] as string[],
|
||||
currentSearchResults: [] as Collaborator[],
|
||||
loadingAlbum: false,
|
||||
errorFetchingAlbum: null,
|
||||
errorFetchingAlbum: null as number | null,
|
||||
loadingCollaborators: false,
|
||||
errorFetchingCollaborators: null,
|
||||
randomId: Math.random().toString().substring(2, 10),
|
||||
|
@ -370,10 +370,9 @@ export default defineComponent({
|
|||
this.loadingAlbum = true;
|
||||
this.errorFetchingAlbum = null;
|
||||
|
||||
const album = await dav.getAlbum(
|
||||
getCurrentUser()?.uid.toString(),
|
||||
this.albumName
|
||||
);
|
||||
const uid = getCurrentUser()?.uid.toString();
|
||||
if (!uid) return;
|
||||
const album = await dav.getAlbum(uid, this.albumName);
|
||||
this.populateCollaborators(album.collaborators);
|
||||
} catch (error) {
|
||||
if (error.response?.status === 404) {
|
||||
|
@ -401,10 +400,9 @@ export default defineComponent({
|
|||
|
||||
async updateAlbumCollaborators() {
|
||||
try {
|
||||
const album = await dav.getAlbum(
|
||||
getCurrentUser()?.uid.toString(),
|
||||
this.albumName
|
||||
);
|
||||
const uid = getCurrentUser()?.uid?.toString();
|
||||
if (!uid) return;
|
||||
const album = await dav.getAlbum(uid, this.albumName);
|
||||
await dav.updateAlbum(album, {
|
||||
albumName: this.albumName,
|
||||
properties: {
|
||||
|
|
|
@ -176,16 +176,18 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
dateDiff() {
|
||||
return this.date.getTime() - this.dateLast.getTime();
|
||||
return this.date && this.dateLast
|
||||
? this.date.getTime() - this.dateLast.getTime()
|
||||
: 0;
|
||||
},
|
||||
|
||||
origDateNewest() {
|
||||
return new Date(this.sortedPhotos[0].datetaken);
|
||||
return new Date(this.sortedPhotos[0].datetaken!);
|
||||
},
|
||||
|
||||
origDateOldest() {
|
||||
return new Date(
|
||||
this.sortedPhotos[this.sortedPhotos.length - 1].datetaken
|
||||
this.sortedPhotos[this.sortedPhotos.length - 1].datetaken!
|
||||
);
|
||||
},
|
||||
|
||||
|
@ -212,13 +214,16 @@ export default defineComponent({
|
|||
|
||||
methods: {
|
||||
init() {
|
||||
const photos = (this.sortedPhotos = [...this.photos] as IPhoto[]);
|
||||
// Filter out only photos that have a datetaken
|
||||
const photos = (this.sortedPhotos = this.photos.filter(
|
||||
(photo) => photo.datetaken !== undefined
|
||||
));
|
||||
|
||||
// Sort photos by datetaken descending
|
||||
photos.sort((a, b) => b.datetaken - a.datetaken);
|
||||
photos.sort((a, b) => b.datetaken! - a.datetaken!);
|
||||
|
||||
// Get date of newest photo
|
||||
let date = new Date(photos[0].datetaken);
|
||||
let date = new Date(photos[0].datetaken!);
|
||||
this.year = date.getUTCFullYear().toString();
|
||||
this.month = (date.getUTCMonth() + 1).toString();
|
||||
this.day = date.getUTCDate().toString();
|
||||
|
@ -228,7 +233,7 @@ export default defineComponent({
|
|||
|
||||
// Get date of oldest photo
|
||||
if (photos.length > 1) {
|
||||
date = new Date(photos[photos.length - 1].datetaken);
|
||||
date = new Date(photos[photos.length - 1].datetaken!);
|
||||
this.yearLast = date.getUTCFullYear().toString();
|
||||
this.monthLast = (date.getUTCMonth() + 1).toString();
|
||||
this.dayLast = date.getUTCDate().toString();
|
||||
|
@ -262,7 +267,7 @@ export default defineComponent({
|
|||
return undefined;
|
||||
}
|
||||
|
||||
if (this.sortedPhotos.length === 0) {
|
||||
if (this.sortedPhotos.length === 0 || !this.date) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
@ -278,7 +283,7 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
newestChange(time = false) {
|
||||
if (this.sortedPhotos.length === 0) {
|
||||
if (this.sortedPhotos.length === 0 || !this.date) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -203,16 +203,17 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
result() {
|
||||
if (!this.dirty) {
|
||||
return null;
|
||||
}
|
||||
if (!this.dirty) return null;
|
||||
|
||||
const lat = (this.lat || 0).toFixed(6);
|
||||
const lon = (this.lon || 0).toFixed(6);
|
||||
|
||||
return {
|
||||
GPSLatitude: this.lat,
|
||||
GPSLongitude: this.lon,
|
||||
GPSLatitudeRef: this.lat,
|
||||
GPSLongitudeRef: this.lon,
|
||||
GPSCoordinates: `${this.lat.toFixed(6)}, ${this.lon.toFixed(6)}`,
|
||||
GPSLatitude: lat,
|
||||
GPSLongitude: lon,
|
||||
GPSLatitudeRef: lat,
|
||||
GPSLongitudeRef: lon,
|
||||
GPSCoordinates: `${lat}, ${lon}`,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
|
|
@ -92,7 +92,7 @@ export default defineComponent({
|
|||
mixins: [UserConfig],
|
||||
|
||||
data: () => ({
|
||||
photos: null as IPhoto[],
|
||||
photos: null as IPhoto[] | null,
|
||||
sections: [] as number[],
|
||||
show: false,
|
||||
processing: false,
|
||||
|
@ -183,7 +183,7 @@ export default defineComponent({
|
|||
this.processing = true;
|
||||
|
||||
// Update exif fields
|
||||
const calls = this.photos.map((p) => async () => {
|
||||
const calls = this.photos!.map((p) => async () => {
|
||||
try {
|
||||
let dirty = false;
|
||||
const fileid = p.fileid;
|
||||
|
@ -223,7 +223,7 @@ export default defineComponent({
|
|||
}
|
||||
} finally {
|
||||
done++;
|
||||
this.progress = Math.round((done * 100) / this.photos.length);
|
||||
this.progress = Math.round((done * 100) / this.photos!.length);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ export default defineComponent({
|
|||
|
||||
methods: {
|
||||
init() {
|
||||
let tagIds: number[] = null;
|
||||
let tagIds: number[] | null = null;
|
||||
|
||||
// Find common tags in all selected photos
|
||||
for (const photo of this.photos) {
|
||||
|
@ -53,7 +53,7 @@ export default defineComponent({
|
|||
tagIds = tagIds ? [...tagIds].filter((x) => s.has(x)) : [...s];
|
||||
}
|
||||
|
||||
this.tagSelection = tagIds;
|
||||
this.tagSelection = tagIds || [];
|
||||
this.origIds = new Set(this.tagSelection);
|
||||
},
|
||||
|
||||
|
|
|
@ -83,7 +83,7 @@ export default defineComponent({
|
|||
} else {
|
||||
await dav.setVisibilityPeopleFaceRecognition(this.name, false);
|
||||
}
|
||||
this.$router.push({ name: this.$route.name });
|
||||
this.$router.push({ name: this.$route.name as string });
|
||||
this.close();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
@ -96,4 +96,4 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
@ -97,7 +97,7 @@ export default defineComponent({
|
|||
await dav.renamePeopleFaceRecognition(this.oldName, this.name);
|
||||
}
|
||||
this.$router.push({
|
||||
name: this.$route.name,
|
||||
name: this.$route.name as string,
|
||||
params: { user: this.user, name: this.name },
|
||||
});
|
||||
this.close();
|
||||
|
|
|
@ -48,7 +48,7 @@ export default defineComponent({
|
|||
user: "",
|
||||
name: "",
|
||||
list: null as ICluster[] | null,
|
||||
fuse: null as Fuse<ICluster>,
|
||||
fuse: null as Fuse<ICluster> | null,
|
||||
search: "",
|
||||
}),
|
||||
|
||||
|
|
|
@ -130,7 +130,8 @@ export default defineComponent({
|
|||
}
|
||||
});
|
||||
for await (const resp of dav.runInParallel(calls, 10)) {
|
||||
this.moved(resp);
|
||||
const valid = resp.filter((r): r is IPhoto => r !== undefined);
|
||||
this.moved(valid);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
|
|
@ -46,8 +46,8 @@ export default defineComponent({
|
|||
data: () => ({
|
||||
isSidebarShown: false,
|
||||
sidebarWidth: 400,
|
||||
trapElements: [],
|
||||
_mutationObserver: null,
|
||||
trapElements: [] as HTMLElement[],
|
||||
_mutationObserver: null! as MutationObserver,
|
||||
}),
|
||||
|
||||
beforeMount() {
|
||||
|
@ -87,7 +87,8 @@ export default defineComponent({
|
|||
* That way we can adjust the focusTrap
|
||||
*/
|
||||
handleBodyMutation(mutations: MutationRecord[]) {
|
||||
const test = (node: HTMLElement) =>
|
||||
const test = (node: Node): node is HTMLElement =>
|
||||
node instanceof HTMLElement &&
|
||||
node?.classList?.contains("v-popper__popper");
|
||||
|
||||
mutations.forEach((mutation) => {
|
||||
|
|
|
@ -189,7 +189,7 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
getShareLabels(share: IShare): string {
|
||||
const labels = [];
|
||||
const labels: string[] = [];
|
||||
if (share.hasPassword) {
|
||||
labels.push(this.t("memories", "Password protected"));
|
||||
}
|
||||
|
|
|
@ -112,7 +112,7 @@ export default defineComponent({
|
|||
|
||||
data: () => {
|
||||
return {
|
||||
photo: null as IPhoto,
|
||||
photo: null as IPhoto | null,
|
||||
loading: 0,
|
||||
};
|
||||
},
|
||||
|
@ -127,7 +127,7 @@ export default defineComponent({
|
|||
isVideo() {
|
||||
return (
|
||||
this.photo &&
|
||||
(this.photo.mimetype.startsWith("video/") ||
|
||||
(this.photo.mimetype?.startsWith("video/") ||
|
||||
this.photo.flag & this.c.FLAG_IS_VIDEO)
|
||||
);
|
||||
},
|
||||
|
@ -160,32 +160,32 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
async sharePreview() {
|
||||
const src = utils.getPreviewUrl(this.photo, false, 2048);
|
||||
const src = utils.getPreviewUrl(this.photo!, false, 2048);
|
||||
this.shareWithHref(src, true);
|
||||
},
|
||||
|
||||
async shareHighRes() {
|
||||
const fileid = this.photo.fileid;
|
||||
const fileid = this.photo!.fileid;
|
||||
const src = this.isVideo
|
||||
? API.VIDEO_TRANSCODE(fileid, "max.mov")
|
||||
: API.IMAGE_DECODABLE(fileid, this.photo.etag);
|
||||
: API.IMAGE_DECODABLE(fileid, this.photo!.etag);
|
||||
this.shareWithHref(src, !this.isVideo);
|
||||
},
|
||||
|
||||
async shareOriginal() {
|
||||
this.shareWithHref(dav.getDownloadLink(this.photo));
|
||||
this.shareWithHref(dav.getDownloadLink(this.photo!));
|
||||
},
|
||||
|
||||
async shareLink() {
|
||||
this.l(async () => {
|
||||
const fileInfo = (await dav.getFiles([this.photo]))[0];
|
||||
const fileInfo = (await dav.getFiles([this.photo!]))[0];
|
||||
globalThis.shareNodeLink(fileInfo.filename, true);
|
||||
});
|
||||
this.close();
|
||||
},
|
||||
|
||||
async shareWithHref(href: string, replaceExt = false) {
|
||||
let blob: Blob;
|
||||
let blob: Blob | undefined;
|
||||
await this.l(async () => {
|
||||
const res = await axios.get(href, { responseType: "blob" });
|
||||
blob = res.data;
|
||||
|
@ -196,11 +196,11 @@ export default defineComponent({
|
|||
return;
|
||||
}
|
||||
|
||||
let basename = this.photo.basename;
|
||||
let basename = this.photo?.basename ?? "blank";
|
||||
|
||||
if (replaceExt) {
|
||||
// Fix basename extension
|
||||
let targetExts = [];
|
||||
let targetExts: string[] = [];
|
||||
if (blob.type === "image/png") {
|
||||
targetExts = ["png"];
|
||||
} else {
|
||||
|
@ -208,7 +208,8 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
// Append extension if not found
|
||||
if (!targetExts.includes(basename.split(".").pop().toLowerCase())) {
|
||||
const baseExt = basename.split(".").pop()?.toLowerCase() ?? "";
|
||||
if (!targetExts.includes(baseExt)) {
|
||||
basename += "." + targetExts[0];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ export default defineComponent({
|
|||
|
||||
methods: {
|
||||
back() {
|
||||
this.$router.push({ name: this.$route.name });
|
||||
this.$router.push({ name: this.$route.name as string });
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<NcActions :inline="1">
|
||||
<NcActionButton
|
||||
:aria-label="t('memories', 'Rename person')"
|
||||
@click="$refs.editModal.open()"
|
||||
@click="$refs.editModal?.open()"
|
||||
close-after-click
|
||||
>
|
||||
{{ t("memories", "Rename person") }}
|
||||
|
@ -21,7 +21,7 @@
|
|||
</NcActionButton>
|
||||
<NcActionButton
|
||||
:aria-label="t('memories', 'Merge with different person')"
|
||||
@click="$refs.mergeModal.open()"
|
||||
@click="$refs.mergeModal?.open()"
|
||||
close-after-click
|
||||
>
|
||||
{{ t("memories", "Merge with different person") }}
|
||||
|
@ -36,7 +36,7 @@
|
|||
</NcActionCheckbox>
|
||||
<NcActionButton
|
||||
:aria-label="t('memories', 'Remove person')"
|
||||
@click="$refs.deleteModal.open()"
|
||||
@click="$refs.deleteModal?.open()"
|
||||
close-after-click
|
||||
>
|
||||
{{ t("memories", "Remove person") }}
|
||||
|
@ -104,7 +104,7 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
back() {
|
||||
this.$router.push({ name: this.$route.name });
|
||||
this.$router.push({ name: this.$route.name as string });
|
||||
},
|
||||
|
||||
changeShowFaceRect() {
|
||||
|
|
|
@ -65,10 +65,10 @@ const OSM_ATTRIBUTION =
|
|||
const CLUSTER_TRANSITION_TIME = 300;
|
||||
|
||||
type IMarkerCluster = {
|
||||
id?: number;
|
||||
id: number;
|
||||
center: [number, number];
|
||||
count: number;
|
||||
preview?: IPhoto;
|
||||
preview: IPhoto;
|
||||
dummy?: boolean;
|
||||
};
|
||||
|
||||
|
|
|
@ -82,7 +82,7 @@ export default defineComponent({
|
|||
hasRight: false,
|
||||
hasLeft: false,
|
||||
scrollStack: [] as number[],
|
||||
resizeObserver: null as ResizeObserver,
|
||||
resizeObserver: null! as ResizeObserver,
|
||||
}),
|
||||
|
||||
mounted() {
|
||||
|
@ -146,7 +146,7 @@ export default defineComponent({
|
|||
year,
|
||||
text,
|
||||
url: "",
|
||||
preview: null,
|
||||
preview: null!,
|
||||
photos: [],
|
||||
});
|
||||
currentText = text;
|
||||
|
@ -167,7 +167,7 @@ export default defineComponent({
|
|||
for (const year of this.years) {
|
||||
// Try to prioritize landscape photos on desktop
|
||||
if (globalThis.windowInnerWidth <= 600) {
|
||||
const landscape = year.photos.filter((p) => p.w > p.h);
|
||||
const landscape = year.photos.filter((p) => (p.w ?? 0) > (p.h ?? 0));
|
||||
year.preview = utils.randomChoice(landscape);
|
||||
}
|
||||
|
||||
|
@ -214,7 +214,7 @@ export default defineComponent({
|
|||
|
||||
click(year: IYear) {
|
||||
const allPhotos = this.years.flatMap((y) => y.photos);
|
||||
this.viewer.openStatic(year.preview, allPhotos, 512);
|
||||
this.viewer?.openStatic(year.preview, allPhotos, 512);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -45,8 +45,8 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
data: () => ({
|
||||
exif: null as any,
|
||||
imageEditor: null as FilerobotImageEditor,
|
||||
exif: null as Object | null,
|
||||
imageEditor: null as FilerobotImageEditor | null,
|
||||
}),
|
||||
|
||||
computed: {
|
||||
|
@ -116,14 +116,14 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
defaultSavedImageName(): string {
|
||||
return this.photo.basename;
|
||||
return this.photo.basename || "";
|
||||
},
|
||||
|
||||
defaultSavedImageType(): "jpeg" | "png" | "webp" {
|
||||
if (
|
||||
["image/jpeg", "image/png", "image/webp"].includes(this.photo.mimetype)
|
||||
["image/jpeg", "image/png", "image/webp"].includes(this.photo.mimetype!)
|
||||
) {
|
||||
return this.photo.mimetype.split("/")[1] as any;
|
||||
return this.photo.mimetype!.split("/")[1] as any;
|
||||
}
|
||||
return "jpeg";
|
||||
},
|
||||
|
@ -169,7 +169,7 @@ export default defineComponent({
|
|||
methods: {
|
||||
async getImage(): Promise<HTMLImageElement> {
|
||||
const img = new Image();
|
||||
img.name = this.photo.basename;
|
||||
img.name = this.defaultSavedImageName;
|
||||
|
||||
await new Promise(async (resolve) => {
|
||||
img.onload = resolve;
|
||||
|
@ -200,11 +200,11 @@ export default defineComponent({
|
|||
*/
|
||||
async onSave(
|
||||
data: {
|
||||
name?: string;
|
||||
name: string;
|
||||
extension: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
quality?: number;
|
||||
extension?: string;
|
||||
fullName?: string;
|
||||
imageBase64?: string;
|
||||
},
|
||||
|
|
|
@ -79,8 +79,8 @@ export default class ImageContentSetup {
|
|||
|
||||
slideActivate() {
|
||||
const slide = this.lightbox.currSlide;
|
||||
if (slide.data.highSrcCond === "always") {
|
||||
this.loadFullImage(slide);
|
||||
if (slide?.data.highSrcCond === "always") {
|
||||
this.loadFullImage(slide as PsSlide);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -97,7 +97,7 @@ export default class ImageContentSetup {
|
|||
img.classList.add("ximg--full");
|
||||
|
||||
this.loading++;
|
||||
this.lightbox.ui.updatePreloaderVisibility();
|
||||
this.lightbox.ui?.updatePreloaderVisibility();
|
||||
|
||||
fetchImage(slide.data.highSrc)
|
||||
.then((blobSrc) => {
|
||||
|
@ -112,7 +112,7 @@ export default class ImageContentSetup {
|
|||
})
|
||||
.finally(() => {
|
||||
this.loading--;
|
||||
this.lightbox.ui.updatePreloaderVisibility();
|
||||
this.lightbox.ui?.updatePreloaderVisibility();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,17 +6,19 @@ import { getCurrentUser } from "@nextcloud/auth";
|
|||
import axios from "@nextcloud/axios";
|
||||
|
||||
import { API } from "../../services/API";
|
||||
import { PsContent, PsEvent, PsSlide } from "./types";
|
||||
import type { PsContent, PsEvent } from "./types";
|
||||
|
||||
import Player from "video.js/dist/types/player";
|
||||
import { QualityLevelList } from "videojs-contrib-quality-levels";
|
||||
|
||||
type VideoContent = PsContent & {
|
||||
videoElement: HTMLVideoElement;
|
||||
videojs: Player & {
|
||||
qualityLevels?: () => QualityLevelList;
|
||||
};
|
||||
plyr: globalThis.Plyr;
|
||||
videoElement: HTMLVideoElement | null;
|
||||
videojs:
|
||||
| (Player & {
|
||||
qualityLevels?: () => QualityLevelList;
|
||||
})
|
||||
| null;
|
||||
plyr: globalThis.Plyr | null;
|
||||
};
|
||||
|
||||
type PsVideoEvent = PsEvent & {
|
||||
|
@ -32,7 +34,7 @@ const config_video_default_quality = Number(
|
|||
/**
|
||||
* Check if slide has video content
|
||||
*/
|
||||
export function isVideoContent(content: PsSlide | PsContent): boolean {
|
||||
export function isVideoContent(content: any): content is VideoContent {
|
||||
return content?.data?.type === "video";
|
||||
}
|
||||
|
||||
|
@ -109,12 +111,12 @@ class VideoContentSetup {
|
|||
});
|
||||
|
||||
pswp.on("close", () => {
|
||||
this.destroyVideo(pswp.currSlide.content as VideoContent);
|
||||
this.destroyVideo(pswp.currSlide?.content as VideoContent);
|
||||
});
|
||||
|
||||
// Prevent closing when video fullscreen is active
|
||||
pswp.on("pointerMove", (e) => {
|
||||
const plyr = (<VideoContent>pswp.currSlide.content)?.plyr;
|
||||
const plyr = (<VideoContent>pswp.currSlide?.content)?.plyr;
|
||||
if (plyr?.fullscreen.active) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
@ -161,7 +163,7 @@ class VideoContentSetup {
|
|||
content.videoElement.setAttribute("playsinline", "");
|
||||
|
||||
// Add the video element to the actual container
|
||||
content.element.appendChild(content.videoElement);
|
||||
content.element?.appendChild(content.videoElement);
|
||||
|
||||
// Create hls sources if enabled
|
||||
const sources: {
|
||||
|
@ -202,7 +204,7 @@ class VideoContentSetup {
|
|||
let hlsFailed = false;
|
||||
|
||||
vjs.on("error", () => {
|
||||
if (vjs.src(undefined).includes("m3u8")) {
|
||||
if (vjs.src(undefined)?.includes("m3u8")) {
|
||||
hlsFailed = true;
|
||||
console.warn("PsVideo: HLS stream could not be opened.");
|
||||
|
||||
|
@ -245,7 +247,7 @@ class VideoContentSetup {
|
|||
playWithDelay();
|
||||
});
|
||||
|
||||
content.videojs.qualityLevels()?.on("addqualitylevel", (e) => {
|
||||
content.videojs.qualityLevels?.()?.on("addqualitylevel", (e) => {
|
||||
if (e.qualityLevel?.label?.includes("max.m3u8")) {
|
||||
// This is the highest quality level
|
||||
// and guaranteed to be the last one
|
||||
|
@ -274,6 +276,7 @@ class VideoContentSetup {
|
|||
destroyVideo(content: VideoContent) {
|
||||
if (isVideoContent(content)) {
|
||||
// Destroy videojs
|
||||
content.videojs?.pause?.();
|
||||
content.videojs?.dispose?.();
|
||||
content.videojs = null;
|
||||
|
||||
|
@ -283,9 +286,11 @@ class VideoContentSetup {
|
|||
content.plyr = null;
|
||||
|
||||
// Clear the video element
|
||||
const elem: HTMLDivElement = content.element;
|
||||
while (elem.lastElementChild) {
|
||||
elem.removeChild(elem.lastElementChild);
|
||||
if (content.element instanceof HTMLDivElement) {
|
||||
const elem = content.element;
|
||||
while (elem.lastElementChild) {
|
||||
elem.removeChild(elem.lastElementChild);
|
||||
}
|
||||
}
|
||||
content.videoElement = null;
|
||||
|
||||
|
@ -301,17 +306,17 @@ class VideoContentSetup {
|
|||
if (!content.videoElement) return;
|
||||
|
||||
// Retain original parent for video element
|
||||
const origParent = content.videoElement.parentElement;
|
||||
const origParent = content.videoElement.parentElement!;
|
||||
|
||||
// Populate quality list
|
||||
let qualityList = content.videojs?.qualityLevels();
|
||||
let qualityNums: number[];
|
||||
let qualityList = content.videojs?.qualityLevels?.();
|
||||
let qualityNums: number[] | undefined;
|
||||
if (qualityList && qualityList.length >= 1) {
|
||||
const s = new Set<number>();
|
||||
let hasMax = false;
|
||||
for (let i = 0; i < qualityList?.length; i++) {
|
||||
const { width, height, label } = qualityList[i];
|
||||
s.add(Math.min(width, height));
|
||||
s.add(Math.min(width!, height!));
|
||||
|
||||
if (label?.includes("max.m3u8")) {
|
||||
hasMax = true;
|
||||
|
@ -351,10 +356,10 @@ class VideoContentSetup {
|
|||
options: qualityNums,
|
||||
forced: true,
|
||||
onChange: (quality: number) => {
|
||||
qualityList = content.videojs?.qualityLevels();
|
||||
qualityList = content.videojs?.qualityLevels?.();
|
||||
if (!qualityList || !content.videojs) return;
|
||||
|
||||
const isHLS = content.videojs.src(undefined).includes("m3u8");
|
||||
const isHLS = content.videojs.src(undefined)?.includes("m3u8");
|
||||
|
||||
if (quality === -2) {
|
||||
// Direct playback
|
||||
|
@ -378,7 +383,7 @@ class VideoContentSetup {
|
|||
// Enable only the selected quality
|
||||
for (let i = 0; i < qualityList.length; ++i) {
|
||||
const { width, height, label } = qualityList[i];
|
||||
const pixels = Math.min(width, height);
|
||||
const pixels = Math.min(width!, height!);
|
||||
qualityList[i].enabled =
|
||||
!quality || // auto
|
||||
pixels === quality || // exact match
|
||||
|
@ -390,40 +395,42 @@ class VideoContentSetup {
|
|||
|
||||
// Initialize Plyr and custom CSS
|
||||
const plyr = new Plyr(content.videoElement, opts);
|
||||
plyr.elements.container.style.height = "100%";
|
||||
plyr.elements.container.style.width = "100%";
|
||||
plyr.elements.container
|
||||
const container = plyr.elements.container!;
|
||||
|
||||
container.style.height = "100%";
|
||||
container.style.width = "100%";
|
||||
container
|
||||
.querySelectorAll("button")
|
||||
.forEach((el) => el.classList.add("button-vue"));
|
||||
plyr.elements.container
|
||||
container
|
||||
.querySelectorAll("progress")
|
||||
.forEach((el) => el.classList.add("vue"));
|
||||
plyr.elements.container.style.backgroundColor = "transparent";
|
||||
plyr.elements.wrapper.style.backgroundColor = "transparent";
|
||||
container.style.backgroundColor = "transparent";
|
||||
plyr.elements.wrapper!.style.backgroundColor = "transparent";
|
||||
|
||||
// Set the fullscreen element to the container
|
||||
plyr.elements.fullscreen = content.slide.holderElement;
|
||||
plyr.elements.fullscreen = content.slide?.holderElement || null;
|
||||
|
||||
// Done with init
|
||||
content.plyr = plyr;
|
||||
|
||||
// Wait for animation to end before showing Plyr
|
||||
plyr.elements.container.style.opacity = "0";
|
||||
container.style.opacity = "0";
|
||||
setTimeout(() => {
|
||||
plyr.elements.container.style.opacity = "1";
|
||||
container.style.opacity = "1";
|
||||
}, 250);
|
||||
|
||||
// Restore original parent of video element
|
||||
origParent.appendChild(content.videoElement);
|
||||
// Move plyr to the slide container
|
||||
content.slide.holderElement.appendChild(plyr.elements.container);
|
||||
content.slide?.holderElement?.appendChild(container);
|
||||
|
||||
// Add fullscreen orientation hooks
|
||||
if (screen.orientation?.lock) {
|
||||
// Store the previous orientation
|
||||
// This is because unlocking (at least on Chrome) does
|
||||
// not restore the previous orientation
|
||||
let previousOrientation: OrientationLockType;
|
||||
let previousOrientation: OrientationLockType | undefined;
|
||||
|
||||
// Lock orientation when entering fullscreen
|
||||
plyr.on("enterfullscreen", async (event) => {
|
||||
|
@ -461,25 +468,25 @@ class VideoContentSetup {
|
|||
}
|
||||
|
||||
updateRotation(content: VideoContent, val?: number): boolean {
|
||||
if (!content.videojs) return;
|
||||
if (!content.videojs) return false;
|
||||
|
||||
content.videoElement = content.videojs.el()?.querySelector("video");
|
||||
if (!content.videoElement) return;
|
||||
if (!content.videoElement) return false;
|
||||
|
||||
const photo = content.data.photo;
|
||||
const exif = photo.imageInfo?.exif;
|
||||
const rotation = val ?? Number(exif?.Rotation || 0);
|
||||
const shouldRotate = content.videojs?.src(undefined).includes("m3u8");
|
||||
const shouldRotate = content.videojs?.src(undefined)?.includes("m3u8");
|
||||
|
||||
if (rotation && shouldRotate) {
|
||||
let transform = `rotate(${rotation}deg)`;
|
||||
const hasRotation = rotation === 90 || rotation === 270;
|
||||
|
||||
if (hasRotation) {
|
||||
content.videoElement.style.width = content.element.style.height;
|
||||
content.videoElement.style.height = content.element.style.width;
|
||||
content.videoElement.style.width = content.element!.style.height;
|
||||
content.videoElement.style.height = content.element!.style.width;
|
||||
|
||||
transform = `translateY(-${content.element.style.width}) ${transform}`;
|
||||
transform = `translateY(-${content.element!.style.width}) ${transform}`;
|
||||
content.videoElement.style.transformOrigin = "bottom left";
|
||||
}
|
||||
|
||||
|
|
|
@ -237,7 +237,7 @@ export default defineComponent({
|
|||
|
||||
data: () => ({
|
||||
isOpen: false,
|
||||
originalTitle: null,
|
||||
originalTitle: null as string | null,
|
||||
editorOpen: false,
|
||||
editorSrc: "",
|
||||
|
||||
|
@ -301,7 +301,7 @@ export default defineComponent({
|
|||
|
||||
/** Route is public */
|
||||
routeIsPublic(): boolean {
|
||||
return this.$route.name?.endsWith("-share");
|
||||
return this.$route.name?.endsWith("-share") ?? false;
|
||||
},
|
||||
|
||||
/** Route is album */
|
||||
|
@ -323,7 +323,7 @@ export default defineComponent({
|
|||
|
||||
/** Is the current slide a video */
|
||||
isVideo(): boolean {
|
||||
return Boolean(this.currentPhoto?.flag & this.c.FLAG_IS_VIDEO);
|
||||
return Boolean((this.currentPhoto?.flag ?? 0) & this.c.FLAG_IS_VIDEO);
|
||||
},
|
||||
|
||||
/** Is the current slide a live photo */
|
||||
|
@ -354,12 +354,12 @@ export default defineComponent({
|
|||
|
||||
/** Show edit buttons */
|
||||
canEdit(): boolean {
|
||||
return this.currentPhoto?.imageInfo?.permissions?.includes("U");
|
||||
return this.currentPhoto?.imageInfo?.permissions?.includes("U") ?? false;
|
||||
},
|
||||
|
||||
/** Show delete button */
|
||||
canDelete(): boolean {
|
||||
return this.currentPhoto?.imageInfo?.permissions?.includes("D");
|
||||
return this.currentPhoto?.imageInfo?.permissions?.includes("D") ?? false;
|
||||
},
|
||||
|
||||
/** Show share button */
|
||||
|
@ -399,7 +399,7 @@ export default defineComponent({
|
|||
const photo = this.currentPhoto;
|
||||
const isvideo = photo && photo.flag & this.c.FLAG_IS_VIDEO;
|
||||
if (photo && !isvideo && photo.fileid === fileid) {
|
||||
this.photoswipe.refreshSlideContent(this.currIndex);
|
||||
this.photoswipe?.refreshSlideContent(this.currIndex);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -487,7 +487,7 @@ export default defineComponent({
|
|||
) {
|
||||
return;
|
||||
}
|
||||
_onFocusIn.call(this.photoswipe.keyboard, e);
|
||||
_onFocusIn.call(this.photoswipe!.keyboard, e);
|
||||
};
|
||||
|
||||
// Refresh sidebar on change
|
||||
|
@ -556,7 +556,7 @@ export default defineComponent({
|
|||
|
||||
// Update vue route for deep linking
|
||||
this.photoswipe.on("slideActivate", (e) => {
|
||||
this.currIndex = this.photoswipe.currIndex;
|
||||
this.currIndex = this.photoswipe!.currIndex;
|
||||
const photo = e.slide?.data?.photo;
|
||||
this.setRouteHash(photo);
|
||||
this.updateTitle(photo);
|
||||
|
@ -619,15 +619,20 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
/** Open using start photo and rows list */
|
||||
async open(anchorPhoto: IPhoto, rows?: IRow[]) {
|
||||
this.list = [...anchorPhoto.d.detail];
|
||||
let startIndex = -1;
|
||||
async open(anchorPhoto: IPhoto, rows: IRow[]) {
|
||||
const detail = anchorPhoto.d?.detail;
|
||||
if (!detail) {
|
||||
console.error("Attempted to open viewer with no detail list!");
|
||||
return;
|
||||
}
|
||||
|
||||
this.list = [...detail];
|
||||
const startIndex = detail.indexOf(anchorPhoto);
|
||||
|
||||
// Get days list and map
|
||||
for (const r of rows) {
|
||||
if (r.type === IRowType.HEAD) {
|
||||
if (r.day.dayid == anchorPhoto.d.dayid) {
|
||||
startIndex = r.day.detail.indexOf(anchorPhoto);
|
||||
if (r.day.dayid == anchorPhoto.dayid) {
|
||||
this.globalAnchor = this.globalCount;
|
||||
}
|
||||
|
||||
|
@ -638,18 +643,18 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
// Create basic viewer
|
||||
await this.createBase({
|
||||
const photoswipe = await this.createBase({
|
||||
index: this.globalAnchor + startIndex,
|
||||
});
|
||||
|
||||
// Lazy-generate item data.
|
||||
// Load the next two days in the timeline.
|
||||
this.photoswipe.addFilter("itemData", (itemData, index) => {
|
||||
photoswipe.addFilter("itemData", (itemData, index) => {
|
||||
// Get photo object from list
|
||||
let idx = index - this.globalAnchor;
|
||||
if (idx < 0) {
|
||||
// Load previous day
|
||||
const firstDayId = this.list[0].d.dayid;
|
||||
const firstDayId = this.list[0].dayid;
|
||||
const firstDayIdx = utils.binarySearch(this.dayIds, firstDayId);
|
||||
if (firstDayIdx === 0) {
|
||||
// No previous day
|
||||
|
@ -657,7 +662,7 @@ export default defineComponent({
|
|||
}
|
||||
const prevDayId = this.dayIds[firstDayIdx - 1];
|
||||
const prevDay = this.days.get(prevDayId);
|
||||
if (!prevDay.detail) {
|
||||
if (!prevDay?.detail) {
|
||||
console.error("[BUG] No detail for previous day");
|
||||
return {};
|
||||
}
|
||||
|
@ -665,7 +670,7 @@ export default defineComponent({
|
|||
this.globalAnchor -= prevDay.count;
|
||||
} else if (idx >= this.list.length) {
|
||||
// Load next day
|
||||
const lastDayId = this.list[this.list.length - 1].d.dayid;
|
||||
const lastDayId = this.list[this.list.length - 1].dayid;
|
||||
const lastDayIdx = utils.binarySearch(this.dayIds, lastDayId);
|
||||
if (lastDayIdx === this.dayIds.length - 1) {
|
||||
// No next day
|
||||
|
@ -673,7 +678,7 @@ export default defineComponent({
|
|||
}
|
||||
const nextDayId = this.dayIds[lastDayIdx + 1];
|
||||
const nextDay = this.days.get(nextDayId);
|
||||
if (!nextDay.detail) {
|
||||
if (!nextDay?.detail) {
|
||||
console.error("[BUG] No detail for next day");
|
||||
return {};
|
||||
}
|
||||
|
@ -689,12 +694,12 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
// Preload next and previous 3 days
|
||||
const dayIdx = utils.binarySearch(this.dayIds, photo.d.dayid);
|
||||
const dayIdx = utils.binarySearch(this.dayIds, photo.dayid);
|
||||
const preload = (idx: number) => {
|
||||
if (
|
||||
idx > 0 &&
|
||||
idx < this.dayIds.length &&
|
||||
!this.days.get(this.dayIds[idx]).detail
|
||||
!this.days.get(this.dayIds[idx])?.detail
|
||||
) {
|
||||
this.fetchDay(this.dayIds[idx]);
|
||||
}
|
||||
|
@ -719,13 +724,13 @@ export default defineComponent({
|
|||
});
|
||||
|
||||
// Get the thumbnail image
|
||||
this.photoswipe.addFilter("thumbEl", (thumbEl, data, index) => {
|
||||
photoswipe.addFilter("thumbEl", (thumbEl, data, index) => {
|
||||
const photo = this.list[index - this.globalAnchor];
|
||||
if (!photo || !photo.w || !photo.h) return thumbEl;
|
||||
return this.thumbElem(photo) || thumbEl;
|
||||
if (!photo || !photo.w || !photo.h) return thumbEl as HTMLElement;
|
||||
return this.thumbElem(photo) ?? (thumbEl as HTMLElement); // bug in PhotoSwipe types
|
||||
});
|
||||
|
||||
this.photoswipe.on("slideActivate", (e) => {
|
||||
photoswipe.on("slideActivate", (e) => {
|
||||
// Scroll to keep the thumbnail in view
|
||||
const thumb = this.thumbElem(e.slide.data?.photo);
|
||||
if (thumb && this.fullyOpened) {
|
||||
|
@ -741,13 +746,13 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
// Remove active class from others and add to this one
|
||||
this.photoswipe.element
|
||||
.querySelectorAll(".pswp__item")
|
||||
photoswipe.element
|
||||
?.querySelectorAll(".pswp__item")
|
||||
.forEach((el) => el.classList.remove("active"));
|
||||
e.slide.holderElement?.classList.add("active");
|
||||
});
|
||||
|
||||
this.photoswipe.init();
|
||||
photoswipe.init();
|
||||
},
|
||||
|
||||
/** Close the viewer */
|
||||
|
@ -758,14 +763,14 @@ export default defineComponent({
|
|||
/** Open with a static list of photos */
|
||||
async openStatic(photo: IPhoto, list: IPhoto[], thumbSize?: number) {
|
||||
this.list = list;
|
||||
await this.createBase({
|
||||
const photoswipe = await this.createBase({
|
||||
index: list.findIndex((p) => p.fileid === photo.fileid),
|
||||
});
|
||||
|
||||
this.globalCount = list.length;
|
||||
this.globalAnchor = 0;
|
||||
|
||||
this.photoswipe.addFilter("itemData", (itemData, index) => ({
|
||||
photoswipe.addFilter("itemData", (itemData, index) => ({
|
||||
...this.getItemData(this.list[index]),
|
||||
msrc: thumbSize
|
||||
? utils.getPreviewUrl(photo, false, thumbSize)
|
||||
|
@ -773,7 +778,7 @@ export default defineComponent({
|
|||
}));
|
||||
|
||||
this.isOpen = true;
|
||||
this.photoswipe.init();
|
||||
photoswipe!.init();
|
||||
},
|
||||
|
||||
/** Get base data object */
|
||||
|
@ -849,7 +854,7 @@ export default defineComponent({
|
|||
if (important.length > 0) return important[0] as HTMLImageElement;
|
||||
|
||||
// Find element within 500px of the screen top
|
||||
let elem: HTMLImageElement;
|
||||
let elem: HTMLImageElement | undefined;
|
||||
elems.forEach((e) => {
|
||||
const rect = e.getBoundingClientRect();
|
||||
if (rect.top > -500) {
|
||||
|
@ -896,7 +901,7 @@ export default defineComponent({
|
|||
if (!this.canEdit) return;
|
||||
|
||||
// Prevent editing Live Photos
|
||||
if (this.currentPhoto.liveid) {
|
||||
if (this.isLivePhoto) {
|
||||
showError(
|
||||
this.t("memories", "Editing is currently disabled for Live Photos")
|
||||
);
|
||||
|
@ -909,7 +914,7 @@ export default defineComponent({
|
|||
|
||||
/** Share the current photo externally */
|
||||
async shareCurrent() {
|
||||
globalThis.sharePhoto(this.currentPhoto);
|
||||
globalThis.sharePhoto(this.currentPhoto!);
|
||||
},
|
||||
|
||||
/** Key press events */
|
||||
|
@ -925,7 +930,7 @@ export default defineComponent({
|
|||
|
||||
/** Delete this photo and refresh */
|
||||
async deleteCurrent() {
|
||||
let idx = this.photoswipe.currIndex - this.globalAnchor;
|
||||
let idx = this.photoswipe!.currIndex - this.globalAnchor;
|
||||
const photo = this.list[idx];
|
||||
if (!photo) return;
|
||||
|
||||
|
@ -950,22 +955,24 @@ export default defineComponent({
|
|||
// If this is the last photo, move to the previous photo first
|
||||
// https://github.com/pulsejet/memories/issues/269
|
||||
if (idx === this.list.length - 1) {
|
||||
this.photoswipe.prev();
|
||||
this.photoswipe!.prev();
|
||||
|
||||
// Some photos might lazy load, so recompute idx for the next element
|
||||
idx = this.photoswipe.currIndex + 1 - this.globalAnchor;
|
||||
idx = this.photoswipe!.currIndex + 1 - this.globalAnchor;
|
||||
}
|
||||
|
||||
this.list.splice(idx, 1);
|
||||
this.globalCount--;
|
||||
for (let i = idx - 3; i <= idx + 3; i++) {
|
||||
this.photoswipe.refreshSlideContent(i + this.globalAnchor);
|
||||
this.photoswipe!.refreshSlideContent(i + this.globalAnchor);
|
||||
}
|
||||
},
|
||||
|
||||
/** Play the current live photo */
|
||||
playLivePhoto() {
|
||||
this.psLivePhoto.onContentActivate(this.photoswipe.currSlide as PsSlide);
|
||||
this.psLivePhoto?.onContentActivate(
|
||||
this.photoswipe!.currSlide as PsSlide
|
||||
);
|
||||
},
|
||||
|
||||
/** Is the current photo a favorite */
|
||||
|
@ -977,7 +984,7 @@ export default defineComponent({
|
|||
|
||||
/** Favorite the current photo */
|
||||
async favoriteCurrent() {
|
||||
const photo = this.currentPhoto;
|
||||
const photo = this.currentPhoto!;
|
||||
const val = !this.isFavorite();
|
||||
try {
|
||||
this.updateLoading(1);
|
||||
|
@ -1014,7 +1021,7 @@ export default defineComponent({
|
|||
/** Open the sidebar */
|
||||
async openSidebar(photo?: IPhoto) {
|
||||
globalThis.mSidebar.setTab("memories-metadata");
|
||||
photo ||= this.currentPhoto;
|
||||
photo ??= this.currentPhoto!;
|
||||
|
||||
if (this.routeIsPublic) {
|
||||
globalThis.mSidebar.open(photo.fileid);
|
||||
|
@ -1052,7 +1059,7 @@ export default defineComponent({
|
|||
closeSidebar() {
|
||||
this.hideSidebar();
|
||||
this.sidebarOpen = false;
|
||||
this.photoswipe.updateSize();
|
||||
this.photoswipe?.updateSize();
|
||||
},
|
||||
|
||||
/** Toggle the sidebar visibility */
|
||||
|
@ -1101,7 +1108,7 @@ export default defineComponent({
|
|||
// If this is a video, wait for it to finish
|
||||
if (this.isVideo) {
|
||||
// Get active video element
|
||||
const video: HTMLVideoElement = this.photoswipe?.element?.querySelector(
|
||||
const video = this.photoswipe?.element?.querySelector<HTMLVideoElement>(
|
||||
".pswp__item.active video"
|
||||
);
|
||||
|
||||
|
@ -1114,7 +1121,7 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
|
||||
this.photoswipe.next();
|
||||
this.photoswipe?.next();
|
||||
// no need to set the timer again, since next
|
||||
// calls resetSlideshowTimer anyway
|
||||
},
|
||||
|
|
|
@ -4,10 +4,15 @@ import { IPhoto } from "../../types";
|
|||
|
||||
type PsAugment = {
|
||||
data: _SlideData & {
|
||||
photo?: IPhoto;
|
||||
src: string;
|
||||
msrc: string;
|
||||
photo: IPhoto;
|
||||
};
|
||||
};
|
||||
export type PsSlide = Slide & PsAugment;
|
||||
export type PsSlide = Slide &
|
||||
PsAugment & {
|
||||
content: PsContent;
|
||||
};
|
||||
export type PsContent = Content & PsAugment;
|
||||
export type PsEvent = {
|
||||
content: PsContent;
|
||||
|
|
26
src/main.ts
26
src/main.ts
|
@ -1,5 +1,3 @@
|
|||
/// <reference types="@nextcloud/typings" />
|
||||
|
||||
import "reflect-metadata";
|
||||
import Vue from "vue";
|
||||
import VueVirtualScroller from "vue-virtual-scroller";
|
||||
|
@ -10,11 +8,11 @@ import GlobalMixin from "./mixins/GlobalMixin";
|
|||
import App from "./App.vue";
|
||||
import Admin from "./Admin.vue";
|
||||
import router from "./router";
|
||||
import { Route } from "vue-router";
|
||||
import { generateFilePath } from "@nextcloud/router";
|
||||
import { getRequestToken } from "@nextcloud/auth";
|
||||
import { IPhoto } from "./types";
|
||||
|
||||
import type { Route } from "vue-router";
|
||||
import type { IPhoto } from "./types";
|
||||
import type PlyrType from "plyr";
|
||||
import type videojsType from "video.js";
|
||||
|
||||
|
@ -61,7 +59,7 @@ globalThis.windowInnerWidth = window.innerWidth;
|
|||
globalThis.windowInnerHeight = window.innerHeight;
|
||||
|
||||
// CSP config for webpack dynamic chunk loading
|
||||
__webpack_nonce__ = window.btoa(getRequestToken());
|
||||
__webpack_nonce__ = window.btoa(getRequestToken() ?? "");
|
||||
|
||||
// Correct the root of the app for chunk loading
|
||||
// OC.linkTo matches the apps folders
|
||||
|
@ -71,21 +69,17 @@ __webpack_public_path__ = generateFilePath("memories", "", "js/");
|
|||
|
||||
// Generate client id for this instance
|
||||
// Does not need to be cryptographically secure
|
||||
const getClientId = () =>
|
||||
const getClientId = (): string =>
|
||||
Math.random().toString(36).substring(2, 15).padEnd(12, "0");
|
||||
globalThis.videoClientId = getClientId();
|
||||
globalThis.videoClientIdPersistent = localStorage.getItem(
|
||||
"videoClientIdPersistent"
|
||||
globalThis.videoClientIdPersistent =
|
||||
localStorage.getItem("videoClientIdPersistent") ?? getClientId();
|
||||
localStorage.setItem(
|
||||
"videoClientIdPersistent",
|
||||
globalThis.videoClientIdPersistent
|
||||
);
|
||||
if (!globalThis.videoClientIdPersistent) {
|
||||
globalThis.videoClientIdPersistent = getClientId();
|
||||
localStorage.setItem(
|
||||
"videoClientIdPersistent",
|
||||
globalThis.videoClientIdPersistent
|
||||
);
|
||||
}
|
||||
|
||||
Vue.mixin(GlobalMixin);
|
||||
Vue.mixin(GlobalMixin as any);
|
||||
Vue.use(VueVirtualScroller);
|
||||
Vue.component("XImg", XImg);
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { generateUrl } from "@nextcloud/router";
|
||||
import { translate as t, translatePlural as n } from "@nextcloud/l10n";
|
||||
import { translate as t } from "@nextcloud/l10n";
|
||||
import Router from "vue-router";
|
||||
import Vue from "vue";
|
||||
import Timeline from "./components/Timeline.vue";
|
||||
|
|
|
@ -1,32 +1,35 @@
|
|||
import { precacheAndRoute } from "workbox-precaching";
|
||||
import { NetworkFirst, CacheFirst, NetworkOnly } from "workbox-strategies";
|
||||
import { NetworkFirst, CacheFirst } from "workbox-strategies";
|
||||
import { registerRoute } from "workbox-routing";
|
||||
import { ExpirationPlugin } from "workbox-expiration";
|
||||
|
||||
precacheAndRoute(self.__WB_MANIFEST);
|
||||
|
||||
registerRoute(/^.*\/apps\/memories\/api\/video\/livephoto\/.*/, new CacheFirst({
|
||||
cacheName: "livephotos",
|
||||
plugins: [
|
||||
new ExpirationPlugin({
|
||||
maxAgeSeconds: 3600 * 24 * 7, // days
|
||||
maxEntries: 1000, // 1k videos
|
||||
}),
|
||||
],
|
||||
}));
|
||||
registerRoute(
|
||||
/^.*\/apps\/memories\/api\/video\/livephoto\/.*/,
|
||||
new CacheFirst({
|
||||
cacheName: "livephotos",
|
||||
plugins: [
|
||||
new ExpirationPlugin({
|
||||
maxAgeSeconds: 3600 * 24 * 7, // days
|
||||
maxEntries: 1000, // 1k videos
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
// Important: Using the NetworkOnly strategy and not registering
|
||||
// a route are NOT equivalent. The NetworkOnly strategy will
|
||||
// strip certain headers such as HTTP-Range, which is required
|
||||
// for proper playback of videos.
|
||||
|
||||
const networkOnly = [
|
||||
/^.*\/apps\/memories\/api\/.*/,
|
||||
];
|
||||
const networkOnly = [/^.*\/apps\/memories\/api\/.*/];
|
||||
|
||||
// Cache pages for same-origin requests only
|
||||
registerRoute(
|
||||
({ url }) => url.origin === self.location.origin && !networkOnly.some((regex) => regex.test(url.href)),
|
||||
({ url }) =>
|
||||
url.origin === self.location.origin &&
|
||||
!networkOnly.some((regex) => regex.test(url.href)),
|
||||
new NetworkFirst({
|
||||
cacheName: "pages",
|
||||
plugins: [
|
||||
|
|
|
@ -45,11 +45,11 @@ export class API {
|
|||
|
||||
if (typeof query === "object") {
|
||||
// Clean up undefined and null
|
||||
Object.keys(query).forEach((key) => {
|
||||
for (const key of Object.keys(query)) {
|
||||
if (query[key] === undefined || query[key] === null) {
|
||||
delete query[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check if nothing in query
|
||||
if (!Object.keys(query).length) return url;
|
||||
|
@ -138,7 +138,7 @@ export class API {
|
|||
return gen(`${BASE}/image/set-exif/{id}`, { id });
|
||||
}
|
||||
|
||||
static IMAGE_DECODABLE(id: number, etag: string) {
|
||||
static IMAGE_DECODABLE(id: number, etag?: string) {
|
||||
return tok(API.Q(gen(`${BASE}/image/decodable/{id}`, { id }), { etag }));
|
||||
}
|
||||
|
||||
|
|
|
@ -104,7 +104,7 @@ export async function* removeFromAlbum(
|
|||
} catch (e) {
|
||||
showError(
|
||||
t("memories", "Failed to remove {filename}.", {
|
||||
filename: f.basename,
|
||||
filename: f.basename ?? f.fileid,
|
||||
})
|
||||
);
|
||||
return 0;
|
||||
|
|
|
@ -60,7 +60,7 @@ export async function getFiles(photos: IPhoto[]): Promise<IFileInfo[]> {
|
|||
const fileIds = photos.map((photo) => photo.fileid);
|
||||
|
||||
// Divide fileIds into chunks of GET_FILE_CHUNK_SIZE
|
||||
const chunks = [];
|
||||
const chunks: number[][] = [];
|
||||
for (let i = 0; i < fileIds.length; i += GET_FILE_CHUNK_SIZE) {
|
||||
chunks.push(fileIds.slice(i, i + GET_FILE_CHUNK_SIZE));
|
||||
}
|
||||
|
|
|
@ -70,7 +70,7 @@ export async function* removeFaceImages(
|
|||
console.error(e);
|
||||
showError(
|
||||
t("memories", "Failed to remove {filename} from face.", {
|
||||
filename: f.basename,
|
||||
filename: f.basename ?? f.fileid,
|
||||
})
|
||||
);
|
||||
return 0;
|
||||
|
|
|
@ -62,7 +62,7 @@ export async function* favoritePhotos(
|
|||
const calls = fileInfos.map((fileInfo) => async () => {
|
||||
try {
|
||||
await favoriteFile(fileInfo.originalFilename, favoriteState);
|
||||
const photo = photos.find((p) => p.fileid === fileInfo.fileid);
|
||||
const photo = photos.find((p) => p.fileid === fileInfo.fileid)!;
|
||||
if (favoriteState) {
|
||||
photo.flag |= utils.constants.c.FLAG_IS_FAVORITE;
|
||||
} else {
|
||||
|
|
|
@ -57,7 +57,7 @@ export async function getOnThisDayData(): Promise<IDay[]> {
|
|||
|
||||
// Add to last day
|
||||
const day = ans[ans.length - 1];
|
||||
day.detail.push(photo);
|
||||
day.detail!.push(photo);
|
||||
day.count++;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { IDay } from "../../types";
|
||||
import { loadState } from "@nextcloud/initial-state";
|
||||
|
||||
let singleItem = null;
|
||||
let singleItem: any;
|
||||
try {
|
||||
singleItem = loadState("memories", "single_item", {});
|
||||
} catch (e) {
|
||||
|
|
|
@ -5,7 +5,9 @@ const config_facerecognitionEnabled = Boolean(
|
|||
loadState("memories", "facerecognitionEnabled", <string>"")
|
||||
);
|
||||
|
||||
export function emptyDescription(routeName: string): string {
|
||||
type RouteNameType = string | null | undefined;
|
||||
|
||||
export function emptyDescription(routeName: RouteNameType): string {
|
||||
switch (routeName) {
|
||||
case "timeline":
|
||||
return t(
|
||||
|
@ -45,7 +47,7 @@ export function emptyDescription(routeName: string): string {
|
|||
}
|
||||
}
|
||||
|
||||
export function viewName(routeName: string): string {
|
||||
export function viewName(routeName: RouteNameType): string {
|
||||
switch (routeName) {
|
||||
case "timeline":
|
||||
return t("memories", "Your Timeline");
|
||||
|
|
|
@ -86,7 +86,7 @@ export function randomSubarray(arr: any[], size: number) {
|
|||
export function setRenewingTimeout(
|
||||
ctx: any,
|
||||
name: string,
|
||||
callback: () => void | null,
|
||||
callback: (() => void) | null,
|
||||
delay: number
|
||||
) {
|
||||
if (ctx[name]) window.clearTimeout(ctx[name]);
|
||||
|
|
|
@ -35,13 +35,13 @@ export async function openCache() {
|
|||
}
|
||||
|
||||
/** Get data from the cache */
|
||||
export async function getCachedData<T>(url: string): Promise<T> {
|
||||
export async function getCachedData<T>(url: string): Promise<T | null> {
|
||||
if (!window.caches) return null;
|
||||
const cache = staticCache || (await openCache());
|
||||
if (!cache) return null;
|
||||
|
||||
const cachedResponse = await cache.match(url);
|
||||
if (!cachedResponse || !cachedResponse.ok) return undefined;
|
||||
if (!cachedResponse || !cachedResponse.ok) return null;
|
||||
return await cachedResponse.json();
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ export type IFileInfo = {
|
|||
/** Full file name, e.g. /pi/test/Qx0dq7dvEXA.jpg */
|
||||
filename: string;
|
||||
/** Original file name, e.g. /files/admin/pi/test/Qx0dq7dvEXA.jpg */
|
||||
originalFilename?: string;
|
||||
originalFilename: string;
|
||||
/** Base name of file e.g. Qx0dq7dvEXA.jpg */
|
||||
basename: string;
|
||||
};
|
||||
|
@ -36,7 +36,7 @@ export type IPhoto = {
|
|||
/** Bit flags */
|
||||
flag: number;
|
||||
/** DayID from server */
|
||||
dayid?: number;
|
||||
dayid: number;
|
||||
/** Width of full image */
|
||||
w?: number;
|
||||
/** Height of full image */
|
||||
|
@ -58,7 +58,7 @@ export type IPhoto = {
|
|||
/** Reference to day object */
|
||||
d?: IDay;
|
||||
/** Reference to exif object */
|
||||
imageInfo?: IImageInfo;
|
||||
imageInfo?: IImageInfo | null;
|
||||
|
||||
/** Face detection ID */
|
||||
faceid?: number;
|
||||
|
@ -174,7 +174,7 @@ export type IRow = {
|
|||
photos?: IPhoto[];
|
||||
|
||||
/** Height in px of the row */
|
||||
size?: number;
|
||||
size: number;
|
||||
/** Count of placeholders to create */
|
||||
pct?: number;
|
||||
/** Don't remove dom element */
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { constants } from "./services/Utils";
|
||||
import { translate as t, translatePlural as n } from "@nextcloud/l10n";
|
||||
import { type constants } from "./services/Utils";
|
||||
import type { translate, translatePlural } from "@nextcloud/l10n";
|
||||
|
||||
declare module "vue" {
|
||||
interface ComponentCustomProperties {
|
||||
// GlobalMixin.ts
|
||||
t: typeof t;
|
||||
n: typeof n;
|
||||
t: typeof translate;
|
||||
n: typeof translatePlural;
|
||||
|
||||
c: typeof constants.c;
|
||||
|
||||
|
@ -31,14 +31,8 @@ declare module "vue" {
|
|||
config_albumListSort: 1 | 2;
|
||||
config_eventName: string;
|
||||
|
||||
updateSetting(setting: string): Promise<void>;
|
||||
updateLocalSetting({
|
||||
setting,
|
||||
value,
|
||||
}: {
|
||||
setting: string;
|
||||
value: any;
|
||||
}): void;
|
||||
updateSetting: (setting: string) => Promise<void>;
|
||||
updateLocalSetting: (opts: { setting: string; value: any }) => void;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
declare module "*.vue" {
|
||||
import { defineComponent } from "vue";
|
||||
import type { defineComponent } from "vue";
|
||||
const Component: ReturnType<typeof defineComponent>;
|
||||
export default Component;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
/** Set the receiver function for a worker */
|
||||
export function workerExport(handlers: {
|
||||
[key: string]: (...data: any) => Promise<any>;
|
||||
}) {
|
||||
export function workerExport(
|
||||
handlers: Record<string, (...data: any) => Promise<any>>
|
||||
): void {
|
||||
/** Promise API for web worker */
|
||||
self.onmessage = async ({ data }) => {
|
||||
self.onmessage = async ({
|
||||
data,
|
||||
}: {
|
||||
data: {
|
||||
id: number;
|
||||
name: string;
|
||||
args: any[];
|
||||
};
|
||||
}) => {
|
||||
try {
|
||||
const handler = handlers[data.name];
|
||||
if (!handler) throw new Error(`No handler for type ${data.name}`);
|
||||
|
@ -23,22 +31,23 @@ export function workerExport(handlers: {
|
|||
|
||||
/** Get the CALL function for a worker. Call this only once. */
|
||||
export function workerImporter(worker: Worker) {
|
||||
const promises: { [id: string]: any } = {};
|
||||
const promises = new Map<number, { resolve: any; reject: any }>();
|
||||
|
||||
worker.onmessage = ({ data }: { data: any }) => {
|
||||
const { id, resolve, reject } = data;
|
||||
if (resolve) promises[id].resolve(resolve);
|
||||
if (reject) promises[id].reject(reject);
|
||||
delete promises[id];
|
||||
if (resolve) promises.get(id)?.resolve(resolve);
|
||||
if (reject) promises.get(id)?.reject(reject);
|
||||
promises.delete(id);
|
||||
};
|
||||
return function importer<F extends (...args: any) => Promise<any>>(
|
||||
name: string
|
||||
): (...args: Parameters<F>) => ReturnType<F> {
|
||||
return function fun(...args: any) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
type PromiseFun = (...args: any) => Promise<any>;
|
||||
return function importer<F extends PromiseFun>(name: string) {
|
||||
return async function fun(...args: Parameters<F>) {
|
||||
return await new Promise<ReturnType<Awaited<F>>>((resolve, reject) => {
|
||||
const id = Math.random();
|
||||
promises[id] = { resolve, reject };
|
||||
promises.set(id, { resolve, reject });
|
||||
worker.postMessage({ id, name, args });
|
||||
});
|
||||
} as any;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
@ -9,7 +9,8 @@
|
|||
"jsx": "preserve",
|
||||
"useDefineForClassFields": true,
|
||||
"noImplicitThis": true,
|
||||
"esModuleInterop": true
|
||||
"esModuleInterop": true,
|
||||
"strictNullChecks": true
|
||||
},
|
||||
"vueCompilerOptions": {
|
||||
"target": 2.7
|
||||
|
|
Loading…
Reference in New Issue