refactor: enable strict null checking

Signed-off-by: Varun Patil <radialapps@gmail.com>
pull/602/head
Varun Patil 2023-04-18 19:19:05 -07:00
parent 516917a0e0
commit 65448ed4c0
57 changed files with 500 additions and 400 deletions

View File

@ -641,7 +641,7 @@ export default defineComponent({
loading: 0, loading: 0,
status: null as IStatus, status: null as IStatus | null,
}), }),
mounted() { 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]; value = value ?? this[key];
const setting = settings[key]; const setting = settings[key];
@ -727,7 +727,7 @@ export default defineComponent({
"This may also cause all photos to be re-indexed!" "This may also cause all photos to be re-indexed!"
); );
const msg = const msg =
(this.status.gis_count ? warnSetup : warnLong) + " " + warnReindex; (this.status?.gis_count ? warnSetup : warnLong) + " " + warnReindex;
if (!confirm(msg)) { if (!confirm(msg)) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@ -805,6 +805,8 @@ export default defineComponent({
}, },
gisStatus() { gisStatus() {
if (!this.status) return "";
if (typeof this.status.gis_type !== "number") { if (typeof this.status.gis_type !== "number") {
return this.status.gis_type; return this.status.gis_type;
} }
@ -825,7 +827,7 @@ export default defineComponent({
}, },
gisStatusType() { gisStatusType() {
return typeof this.status.gis_type !== "number" || return typeof this.status?.gis_type !== "number" ||
this.status.gis_type <= 0 this.status.gis_type <= 0
? "error" ? "error"
: "success"; : "success";
@ -836,6 +838,8 @@ export default defineComponent({
}, },
vaapiStatusText(): string { vaapiStatusText(): string {
if (!this.status) return "";
const dev = "/dev/dri/renderD128"; const dev = "/dev/dri/renderD128";
if (this.status.vaapi_dev === "ok") { if (this.status.vaapi_dev === "ok") {
return this.t("memories", "VA-API device ({dev}) is readable", { dev }); return this.t("memories", "VA-API device ({dev}) is readable", { dev });
@ -855,7 +859,7 @@ export default defineComponent({
}, },
vaapiStatusType(): string { vaapiStatusType(): string {
return this.status.vaapi_dev === "ok" ? "success" : "error"; return this.status?.vaapi_dev === "ok" ? "success" : "error";
}, },
}, },
}); });

View File

@ -89,6 +89,13 @@ import TagsIcon from "vue-material-design-icons/Tag.vue";
import MapIcon from "vue-material-design-icons/Map.vue"; import MapIcon from "vue-material-design-icons/Map.vue";
import CogIcon from "vue-material-design-icons/Cog.vue"; import CogIcon from "vue-material-design-icons/Cog.vue";
type NavItem = {
name: string;
title: string;
icon: any;
if?: any;
};
export default defineComponent({ export default defineComponent({
name: "App", name: "App",
components: { components: {
@ -122,7 +129,7 @@ export default defineComponent({
mixins: [UserConfig], mixins: [UserConfig],
data: () => ({ data: () => ({
navItems: [], navItems: [] as NavItem[],
metadataComponent: null as any, metadataComponent: null as any,
settingsOpen: false, settingsOpen: false,
}), }),
@ -133,7 +140,7 @@ export default defineComponent({
return Number(version[0]); return Number(version[0]);
}, },
recognize(): string | boolean { recognize(): string | false {
if (!this.config_recognizeEnabled) { if (!this.config_recognizeEnabled) {
return false; return false;
} }
@ -145,7 +152,7 @@ export default defineComponent({
return t("memories", "People"); return t("memories", "People");
}, },
facerecognition(): string | boolean { facerecognition(): string | false {
if (!this.config_facerecognitionInstalled) { if (!this.config_facerecognitionInstalled) {
return false; return false;
} }
@ -189,7 +196,7 @@ export default defineComponent({
const onResize = () => { const onResize = () => {
globalThis.windowInnerWidth = window.innerWidth; globalThis.windowInnerWidth = window.innerWidth;
globalThis.windowInnerHeight = window.innerHeight; globalThis.windowInnerHeight = window.innerHeight;
emit("memories:window:resize", null); emit("memories:window:resize", {});
}; };
window.addEventListener("resize", () => { window.addEventListener("resize", () => {
utils.setRenewingTimeout(this, "resizeTimer", onResize, 100); utils.setRenewingTimeout(this, "resizeTimer", onResize, 100);
@ -258,7 +265,7 @@ export default defineComponent({
}, },
methods: { methods: {
navItemsAll() { navItemsAll(): NavItem[] {
return [ return [
{ {
name: "timeline", name: "timeline",
@ -289,13 +296,13 @@ export default defineComponent({
{ {
name: "recognize", name: "recognize",
icon: PeopleIcon, icon: PeopleIcon,
title: this.recognize, title: this.recognize || "",
if: this.recognize, if: this.recognize,
}, },
{ {
name: "facerecognition", name: "facerecognition",
icon: PeopleIcon, icon: PeopleIcon,
title: this.facerecognition, title: this.facerecognition || "",
if: this.facerecognition, if: this.facerecognition,
}, },
{ {
@ -334,7 +341,7 @@ export default defineComponent({
}, },
doRouteChecks() { doRouteChecks() {
if (this.$route.name.endsWith("-share")) { if (this.$route.name?.endsWith("-share")) {
this.putShareToken(<string>this.$route.params.token); this.putShareToken(<string>this.$route.params.token);
} }
}, },

View File

@ -23,7 +23,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from "vue";
import Cluster from "./frame/Cluster.vue"; import Cluster from "./frame/Cluster.vue";
import { ICluster } from "../types"; import type { ICluster } from "../types";
export default defineComponent({ export default defineComponent({
name: "ClusterGrid", name: "ClusterGrid",

View File

@ -21,7 +21,7 @@ import EmptyContent from "./top-matter/EmptyContent.vue";
import * as dav from "../services/DavRequests"; import * as dav from "../services/DavRequests";
import { ICluster } from "../types"; import type { ICluster } from "../types";
export default defineComponent({ export default defineComponent({
name: "ClusterView", name: "ClusterView",

View File

@ -59,7 +59,7 @@ import { getCurrentUser } from "@nextcloud/auth";
import axios from "@nextcloud/axios"; import axios from "@nextcloud/axios";
import banner from "../assets/banner.svg"; import banner from "../assets/banner.svg";
import { IDay } from "../types"; import type { IDay } from "../types";
import { API } from "../services/API"; import { API } from "../services/API";
export default defineComponent({ export default defineComponent({

View File

@ -12,7 +12,7 @@ import { defineComponent } from "vue";
import UserConfig from "../mixins/UserConfig"; import UserConfig from "../mixins/UserConfig";
import Folder from "./frame/Folder.vue"; import Folder from "./frame/Folder.vue";
import { IFolder } from "../types"; import type { IFolder } from "../types";
export default defineComponent({ export default defineComponent({
name: "ClusterGrid", name: "ClusterGrid",

View File

@ -29,7 +29,7 @@
<NcActions :inline="1"> <NcActions :inline="1">
<NcActionButton <NcActionButton
:aria-label="t('memories', 'Edit')" :aria-label="t('memories', 'Edit')"
@click="field.edit()" @click="field.edit?.()"
> >
{{ t("memories", "Edit") }} {{ t("memories", "Edit") }}
<template #icon> <EditIcon :size="20" /> </template> <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 LocationIcon from "vue-material-design-icons/MapMarker.vue";
import TagIcon from "vue-material-design-icons/Tag.vue"; import TagIcon from "vue-material-design-icons/Tag.vue";
import { API } from "../services/API"; import { API } from "../services/API";
import { IImageInfo } from "../types"; import type { IImageInfo } from "../types";
interface TopField { interface TopField {
title: string; title: string;
@ -112,8 +112,8 @@ export default defineComponent({
if (this.dateOriginal) { if (this.dateOriginal) {
list.push({ list.push({
title: this.dateOriginalStr, title: this.dateOriginalStr!,
subtitle: this.dateOriginalTime, subtitle: this.dateOriginalTime!,
icon: CalendarIcon, icon: CalendarIcon,
edit: () => edit: () =>
globalThis.editMetadata([globalThis.currentViewerPhoto], [1]), globalThis.editMetadata([globalThis.currentViewerPhoto], [1]),
@ -228,13 +228,13 @@ export default defineComponent({
return `${make} ${model}`; return `${make} ${model}`;
}, },
cameraSub(): string[] | null { cameraSub(): string[] {
const f = this.exif["FNumber"] || this.exif["Aperture"]; const f = this.exif["FNumber"] || this.exif["Aperture"];
const s = this.shutterSpeed; const s = this.shutterSpeed;
const len = this.exif["FocalLength"]; const len = this.exif["FocalLength"];
const iso = this.exif["ISO"]; const iso = this.exif["ISO"];
const parts = []; const parts: string[] = [];
if (f) parts.push(`f/${f}`); if (f) parts.push(`f/${f}`);
if (s) parts.push(`${s}`); if (s) parts.push(`${s}`);
if (len) parts.push(`${len}mm`); if (len) parts.push(`${len}mm`);
@ -263,8 +263,8 @@ export default defineComponent({
return this.baseInfo.basename; return this.baseInfo.basename;
}, },
imageInfoSub(): string[] | null { imageInfoSub(): string[] {
let parts = []; let parts: string[] = [];
let mp = Number(this.exif["Megapixels"]); let mp = Number(this.exif["Megapixels"]);
if (this.baseInfo.w && this.baseInfo.h) { if (this.baseInfo.w && this.baseInfo.h) {
@ -282,7 +282,7 @@ export default defineComponent({
return parts; return parts;
}, },
address(): string | null { address(): string | undefined {
return this.baseInfo.address; return this.baseInfo.address;
}, },
@ -298,11 +298,11 @@ export default defineComponent({
return Object.values(this.baseInfo?.tags || {}); return Object.values(this.baseInfo?.tags || {});
}, },
tagNamesStr(): string { tagNamesStr(): string | null {
return this.tagNames.length > 0 ? this.tagNames.join(", ") : null; return this.tagNames.length > 0 ? this.tagNames.join(", ") : null;
}, },
mapUrl(): string | null { mapUrl(): string {
const boxSize = 0.0075; const boxSize = 0.0075;
const bbox = [ const bbox = [
this.lon - boxSize, this.lon - boxSize,
@ -314,7 +314,7 @@ export default defineComponent({
return `https://www.openstreetmap.org/export/embed.html?bbox=${bbox.join()}&marker=${m}`; 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}`; 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; return this.baseInfo;
}, },
handleFileUpdated({ fileid }) { handleFileUpdated({ fileid }: { fileid: number }) {
if (fileid && this.fileid === fileid) { if (fileid && this.fileid === fileid) {
this.update(this.fileid); this.update(this.fileid);
} }

View File

@ -66,13 +66,25 @@ export default defineComponent({
props: { props: {
/** Rows from Timeline */ /** Rows from Timeline */
rows: Array as PropType<IRow[]>, rows: {
type: Array as PropType<IRow[]>,
required: true,
},
/** Total height */ /** Total height */
height: Number, height: {
type: Number,
required: true,
},
/** Actual recycler component */ /** Actual recycler component */
recycler: Object, recycler: {
type: Object,
required: false,
},
/** Recycler before slot component */ /** Recycler before slot component */
recyclerBefore: HTMLDivElement, recyclerBefore: {
type: HTMLDivElement,
required: false,
},
}, },
data: () => ({ data: () => ({
@ -81,7 +93,7 @@ export default defineComponent({
/** Height of the entire photo view */ /** Height of the entire photo view */
recyclerHeight: 100, recyclerHeight: 100,
/** Rect of scroller */ /** Rect of scroller */
scrollerRect: null as DOMRect, scrollerRect: null as DOMRect | null,
/** Computed ticks */ /** Computed ticks */
ticks: [] as ITick[], ticks: [] as ITick[],
/** Computed cursor top */ /** Computed cursor top */
@ -273,8 +285,8 @@ export default defineComponent({
/** Do adjustment synchronously */ /** Do adjustment synchronously */
adjustNow() { adjustNow() {
// Refresh height of recycler // Refresh height of recycler
this.recyclerHeight = this.recycler.$refs.wrapper.clientHeight; this.recyclerHeight = this.recycler?.$refs.wrapper.clientHeight ?? 0;
const extraY = this.recyclerBefore?.clientHeight || 0; const extraY = this.recyclerBefore?.clientHeight ?? 0;
// Start with the first tick. Walk over all rows counting the // Start with the first tick. Walk over all rows counting the
// y position. When you hit a row with the tick, update y and // 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); const date = utils.dayIdToDate(dayId);
this.hoverCursorText = utils.getShortDateStr(date); this.hoverCursorText = utils.getShortDateStr(date) ?? "";
}, },
/** Handle mouse hover */ /** Handle mouse hover */
@ -480,7 +492,7 @@ export default defineComponent({
if (this.lastRequestedRecyclerY !== targetY) { if (this.lastRequestedRecyclerY !== targetY) {
this.lastRequestedRecyclerY = targetY; this.lastRequestedRecyclerY = targetY;
this.recycler.scrollToPosition(targetY); this.recycler?.scrollToPosition(targetY);
} }
this.handleScroll(); this.handleScroll();
@ -494,6 +506,7 @@ export default defineComponent({
/** Handle touch */ /** Handle touch */
touchmove(event: any) { touchmove(event: any) {
if (!this.scrollerRect) return;
let y = event.targetTouches[0].pageY - this.scrollerRect.top; let y = event.targetTouches[0].pageY - this.scrollerRect.top;
y = Math.max(0, y - 20); // middle of touch finger y = Math.max(0, y - 20); // middle of touch finger
this.moveto(y, true); this.moveto(y, true);

View File

@ -103,27 +103,39 @@ export default defineComponent({
mixins: [UserConfig], mixins: [UserConfig],
props: { props: {
heads: Object as PropType<{ [dayid: number]: IHeadRow }>, heads: {
type: Object as PropType<{ [dayid: number]: IHeadRow }>,
required: true,
},
/** List of rows for multi selection */ /** 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) */ /** Rows are in ascending order (desc is normal) */
isreverse: Boolean, isreverse: {
type: Boolean,
required: true,
},
/** Recycler element to scroll during touch multi-select */ /** Recycler element to scroll during touch multi-select */
recycler: Object, recycler: {
type: HTMLDivElement,
required: false,
},
}, },
data: () => ({ data: () => ({
show: false, show: false,
size: 0, size: 0,
selection: new Map<number, IPhoto>(), selection: new Map<number, IPhoto>(),
defaultActions: null as ISelectionAction[], defaultActions: null! as ISelectionAction[],
touchAnchor: null as IPhoto, touchAnchor: null as IPhoto | null,
prevTouch: null as Touch, prevTouch: null as Touch | null,
touchTimer: 0, touchTimer: 0,
touchMoved: false, touchMoved: false,
touchPrevSel: null as Selection, touchPrevSel: null as Selection | null,
prevOver: null as IPhoto, prevOver: null as IPhoto | null,
touchScrollInterval: 0, touchScrollInterval: 0,
touchScrollDelta: 0, touchScrollDelta: 0,
}), }),
@ -225,6 +237,14 @@ export default defineComponent({
this.$emit("delete", photos); 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) { updateLoading(delta: number) {
this.$emit("updateLoading", delta); this.$emit("updateLoading", delta);
}, },
@ -351,7 +371,7 @@ export default defineComponent({
window.clearTimeout(this.touchTimer); window.clearTimeout(this.touchTimer);
this.touchTimer = 0; this.touchTimer = 0;
this.touchMoved = false; this.touchMoved = false;
this.prevOver = undefined; this.prevOver = null;
window.cancelAnimationFrame(this.touchScrollInterval); window.cancelAnimationFrame(this.touchScrollInterval);
this.touchScrollInterval = 0; this.touchScrollInterval = 0;
@ -419,7 +439,8 @@ export default defineComponent({
let frameCount = 3; let frameCount = 3;
const fun = () => { const fun = () => {
this.recycler.$el.scrollTop += this.touchScrollDelta; if (!this.prevTouch) return;
this.recycler!.scrollTop += this.touchScrollDelta;
if (frameCount++ >= 3) { if (frameCount++ >= 3) {
this.touchMoveSelect(this.prevTouch, rowIdx); this.touchMoveSelect(this.prevTouch, rowIdx);
@ -442,11 +463,14 @@ export default defineComponent({
/** Multi-select triggered by touchmove */ /** Multi-select triggered by touchmove */
touchMoveSelect(touch: Touch, rowIdx: number) { touchMoveSelect(touch: Touch, rowIdx: number) {
// Assertions
if (!this.touchAnchor) return;
// Which photo is the cursor over, if any // Which photo is the cursor over, if any
const elem: any = document const elem: any = document
.elementFromPoint(touch.clientX, touch.clientY) .elementFromPoint(touch.clientX, touch.clientY)
?.closest(".p-outer-super"); ?.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) if (overPhoto && overPhoto.flag & this.c.FLAG_PLACEHOLDER)
overPhoto = null; overPhoto = null;
@ -460,7 +484,8 @@ export default defineComponent({
// days reverse XOR rows reverse // days reverse XOR rows reverse
let reverse: boolean; let reverse: boolean;
if (overPhoto.dayid === this.touchAnchor.dayid) { 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 ai = l.indexOf(this.touchAnchor);
const oi = l.indexOf(overPhoto); const oi = l.indexOf(overPhoto);
if (ai === -1 || oi === -1) return; // Shouldn't happen if (ai === -1 || oi === -1) return; // Shouldn't happen
@ -474,14 +499,16 @@ export default defineComponent({
// Walk over rows // Walk over rows
let i = rowIdx; 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) { while (true) {
if (j < 0) { if (j < 0) {
while (i > 0 && !this.rows[--i].photos); while (i > 0 && !this.rows[--i].photos);
if (!this.rows[i].photos) break; const plen = this.rows[i].photos?.length;
j = this.rows[i].photos.length - 1; if (!plen) break;
j = plen - 1;
continue; 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); while (i < this.rows.length - 1 && !this.rows[++i].photos);
if (!this.rows[i].photos) break; if (!this.rows[i].photos) break;
j = 0; j = 0;
@ -549,7 +576,7 @@ export default defineComponent({
} }
if (!noUpdate) { if (!noUpdate) {
this.updateHeadSelected(this.heads[photo.d.dayid]); this.updateHeadSelected(this.heads[photo.dayid]);
this.$forceUpdate(); this.$forceUpdate();
} }
}, },
@ -557,11 +584,11 @@ export default defineComponent({
/** Multi-select */ /** Multi-select */
selectMulti(photo: IPhoto, rows: IRow[], rowIdx: number) { selectMulti(photo: IPhoto, rows: IRow[], rowIdx: number) {
const pRow = rows[rowIdx]; const pRow = rows[rowIdx];
const pIdx = pRow.photos.indexOf(photo); const pIdx = pRow.photos?.indexOf(photo) ?? -1;
if (pIdx === -1) return; if (pIdx === -1) return;
const updateDaySet = new Set<number>(); const updateDaySet = new Set<number>();
let behind = []; let behind: IPhoto[] = [];
let behindFound = false; let behindFound = false;
// Look behind // Look behind
@ -570,16 +597,16 @@ export default defineComponent({
if (rows[i].type !== IRowType.PHOTOS) continue; if (rows[i].type !== IRowType.PHOTOS) continue;
if (!rows[i].photos?.length) break; 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--) { 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_PLACEHOLDER || !p.fileid) continue;
if (p.flag & this.c.FLAG_SELECTED) { if (p.flag & this.c.FLAG_SELECTED) {
behindFound = true; behindFound = true;
break; break;
} }
behind.push(p); behind.push(p);
updateDaySet.add(p.d.dayid); updateDaySet.add(p.dayid);
} }
if (behindFound) break; if (behindFound) break;
@ -587,23 +614,25 @@ export default defineComponent({
// Select everything behind // Select everything behind
if (behindFound) { if (behindFound) {
const detail = photo.d!.detail!;
// Clear everything in front in this day // Clear everything in front in this day
const pdIdx = photo.d.detail.indexOf(photo); const pdIdx = detail.indexOf(photo);
for (let i = pdIdx + 1; i < photo.d.detail.length; i++) { for (let i = pdIdx + 1; i < detail.length; i++) {
const p = photo.d.detail[i]; if (detail[i].flag & this.c.FLAG_SELECTED)
if (p.flag & this.c.FLAG_SELECTED) this.selectPhoto(p, false, true); this.selectPhoto(detail[i], false, true);
} }
// Clear everything else in front // Clear everything else in front
Array.from(this.selection.values()) Array.from(this.selection.values())
.filter((p: IPhoto) => { .filter((p: IPhoto) => {
return this.isreverse return this.isreverse
? p.d.dayid > photo.d.dayid ? p.dayid > photo.dayid
: p.d.dayid < photo.d.dayid; : p.dayid < photo.dayid;
}) })
.forEach((photo: IPhoto) => { .forEach((photo: IPhoto) => {
this.selectPhoto(photo, false, true); this.selectPhoto(photo, false, true);
updateDaySet.add(photo.d.dayid); updateDaySet.add(photo.dayid);
}); });
behind.forEach((p) => this.selectPhoto(p, true, true)); behind.forEach((p) => this.selectPhoto(p, true, true));
@ -615,8 +644,8 @@ export default defineComponent({
/** Select or deselect all photos in a head */ /** Select or deselect all photos in a head */
selectHead(head: IHeadRow) { selectHead(head: IHeadRow) {
head.selected = !head.selected; head.selected = !head.selected;
for (const row of head.day.rows) { for (const row of head.day.rows ?? []) {
for (const photo of row.photos) { for (const photo of row.photos ?? []) {
this.selectPhoto(photo, head.selected, true); this.selectPhoto(photo, head.selected, true);
} }
} }
@ -628,8 +657,8 @@ export default defineComponent({
let selected = true; let selected = true;
// Check if all photos are selected // Check if all photos are selected
for (const row of head.day.rows) { for (const row of head.day.rows ?? []) {
for (const photo of row.photos) { for (const photo of row.photos ?? []) {
if (!(photo.flag & this.c.FLAG_SELECTED)) { if (!(photo.flag & this.c.FLAG_SELECTED)) {
selected = false; selected = false;
break; break;
@ -647,7 +676,7 @@ export default defineComponent({
const toClear = only || this.selection.values(); const toClear = only || this.selection.values();
Array.from(toClear).forEach((photo: IPhoto) => { Array.from(toClear).forEach((photo: IPhoto) => {
photo.flag &= ~this.c.FLAG_SELECTED; 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.selection.delete(photo.fileid);
this.selectionChanged(); this.selectionChanged();
}); });
@ -663,7 +692,7 @@ export default defineComponent({
// FileID => Photo for new day // FileID => Photo for new day
const dayMap = new Map<number, IPhoto>(); const dayMap = new Map<number, IPhoto>();
day.detail.forEach((photo) => { day.detail?.forEach((photo) => {
dayMap.set(photo.fileid, photo); dayMap.set(photo.fileid, photo);
}); });
@ -674,13 +703,13 @@ export default defineComponent({
} }
// Remove all selections that are not in the new day // 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); this.selection.delete(fileid);
return; return;
} }
// Update the photo object // Update the photo object
const newPhoto = dayMap.get(fileid);
this.selection.set(fileid, newPhoto); this.selection.set(fileid, newPhoto);
newPhoto.flag |= this.c.FLAG_SELECTED; newPhoto.flag |= this.c.FLAG_SELECTED;
}); });
@ -749,10 +778,7 @@ export default defineComponent({
for await (const delIds of dav.deletePhotos( for await (const delIds of dav.deletePhotos(
Array.from(selection.values()) Array.from(selection.values())
)) { )) {
const delPhotos = delIds this.deleteSelectedPhotosById(delIds, selection);
.filter((id) => id)
.map((id) => selection.get(id));
this.deletePhotos(delPhotos);
} }
}, },
@ -793,12 +819,7 @@ export default defineComponent({
Array.from(selection.keys()), Array.from(selection.keys()),
!this.routeIsArchive() !this.routeIsArchive()
)) { )) {
delIds = delIds.filter((x) => x); this.deleteSelectedPhotosById(delIds, selection);
if (delIds.length === 0) {
continue;
}
const delPhotos = delIds.map((id) => selection.get(id));
this.deletePhotos(delPhotos);
} }
}, },
@ -858,10 +879,7 @@ export default defineComponent({
<string>name, <string>name,
Array.from(selection.values()) Array.from(selection.values())
)) { )) {
const delPhotos = delIds this.deleteSelectedPhotosById(delIds, selection);
.filter((x) => x)
.map((id) => selection.get(id));
this.deletePhotos(delPhotos);
} }
}, },

View File

@ -110,7 +110,7 @@ export default defineComponent({
}, },
handleClose() { handleClose() {
emit("memories:sidebar:closed", null); emit("memories:sidebar:closed", {});
}, },
handleOpen() { handleOpen() {
@ -120,7 +120,7 @@ export default defineComponent({
if (e.key.length === 1) e.stopPropagation(); if (e.key.length === 1) e.stopPropagation();
}); });
emit("memories:sidebar:opened", null); emit("memories:sidebar:opened", {});
}, },
handleNativeOpen() { handleNativeOpen() {

View File

@ -46,7 +46,7 @@ export default defineComponent({
primaryPos: 0, primaryPos: 0,
containerSize: 0, containerSize: 0,
mobileOpen: 1, mobileOpen: 1,
hammer: null as HammerManager, hammer: null as HammerManager | null,
photoCount: 0, photoCount: 0,
}), }),
@ -85,7 +85,7 @@ export default defineComponent({
beforeDestroy() { beforeDestroy() {
this.pointerUp(); this.pointerUp();
this.hammer.destroy(); this.hammer?.destroy();
}, },
methods: { methods: {
@ -133,7 +133,7 @@ export default defineComponent({
this.pointerDown = false; this.pointerDown = false;
document.removeEventListener("pointermove", this.documentPointerMove); document.removeEventListener("pointermove", this.documentPointerMove);
document.removeEventListener("pointerup", this.pointerUp); document.removeEventListener("pointerup", this.pointerUp);
emit("memories:window:resize", null); emit("memories:window:resize", {});
}, },
setFlexBasis(pos: { clientX: number; clientY: number }) { setFlexBasis(pos: { clientX: number; clientY: number }) {
@ -154,7 +154,7 @@ export default defineComponent({
// so that we can prepare in advance for showing more photos // so that we can prepare in advance for showing more photos
// on the timeline // on the timeline
await this.$nextTick(); await this.$nextTick();
emit("memories:window:resize", null); emit("memories:window:resize", {});
}, },
async mobileSwipeDown() { async mobileSwipeDown() {
@ -165,7 +165,7 @@ export default defineComponent({
// ends. Note that this is necesary: the height of the timeline inner // ends. Note that this is necesary: the height of the timeline inner
// div is also animated to the smaller size. // div is also animated to the smaller size.
await new Promise((resolve) => setTimeout(resolve, 300)); await new Promise((resolve) => setTimeout(resolve, 300));
emit("memories:window:resize", null); emit("memories:window:resize", {});
}, },
}, },
}); });

View File

@ -88,7 +88,7 @@
:heads="heads" :heads="heads"
:rows="list" :rows="list"
:isreverse="isMonthView" :isreverse="isMonthView"
:recycler="$refs.recycler" :recycler="$refs.recycler?.$el"
@refresh="softRefresh" @refresh="softRefresh"
@delete="deleteFromViewWithAnimation" @delete="deleteFromViewWithAnimation"
@updateLoading="updateLoading" @updateLoading="updateLoading"
@ -374,8 +374,8 @@ export default defineComponent({
this.loadedDays.clear(); this.loadedDays.clear();
this.sizedDays.clear(); this.sizedDays.clear();
this.fetchDayQueue = []; this.fetchDayQueue = [];
window.clearTimeout(this.fetchDayTimer); window.clearTimeout(this.fetchDayTimer ?? 0);
window.clearTimeout(this.resizeTimer); window.clearTimeout(this.resizeTimer ?? 0);
}, },
/** Recreate everything */ /** Recreate everything */
@ -492,7 +492,7 @@ export default defineComponent({
} }
// Initialize photos and add placeholders // Initialize photos and add placeholders
if (row.pct && !row.photos.length) { if (row.pct && !row.photos?.length) {
row.photos = new Array(row.pct); row.photos = new Array(row.pct);
for (let j = 0; j < row.pct; j++) { for (let j = 0; j < row.pct; j++) {
// Any row that has placeholders has ONLY placeholders // Any row that has placeholders has ONLY placeholders
@ -500,6 +500,7 @@ export default defineComponent({
row.photos[j] = { row.photos[j] = {
flag: this.c.FLAG_PLACEHOLDER, flag: this.c.FLAG_PLACEHOLDER,
fileid: Math.random(), fileid: Math.random(),
dayid: row.dayId,
dispW: utils.roundHalf(this.rowWidth / this.numCols), dispW: utils.roundHalf(this.rowWidth / this.numCols),
dispX: utils.roundHalf((j * this.rowWidth) / this.numCols), dispX: utils.roundHalf((j * this.rowWidth) / this.numCols),
dispH: this.rowHeight, dispH: this.rowHeight,
@ -568,7 +569,7 @@ export default defineComponent({
if (this.loadedDays.has(item.dayId)) { if (this.loadedDays.has(item.dayId)) {
if (!this.sizedDays.has(item.dayId)) { if (!this.sizedDays.has(item.dayId)) {
// Just quietly reflow without refetching // Just quietly reflow without refetching
this.processDay(item.dayId, item.day.detail); this.processDay(item.dayId, item.day.detail!);
} }
continue; continue;
} }
@ -692,7 +693,7 @@ export default defineComponent({
// Filter out hidden folders // Filter out hidden folders
if (!this.config_showHidden) { if (!this.config_showHidden) {
this.folders = this.folders.filter( 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; const cacheUrl = <string>this.$route.name + url;
// Try cache first // Try cache first
let cache: IDay[]; let cache: IDay[] | null = null;
// Make sure to refresh scroll later // Make sure to refresh scroll later
this.currentEnd = -1; this.currentEnd = -1;
@ -730,18 +731,19 @@ export default defineComponent({
data = await dav.getOnThisDayData(); data = await dav.getOnThisDayData();
} else if (dav.isSingleItem()) { } else if (dav.isSingleItem()) {
data = await dav.getSingleItemData(); data = await dav.getSingleItemData();
this.$router.replace(utils.getViewerRoute(data[0]!.detail[0])); this.$router.replace(utils.getViewerRoute(data[0]!.detail![0]));
} else { } else {
// Try the cache // Try the cache
try { if (!noCache) {
cache = noCache ? null : await utils.getCachedData(cacheUrl); try {
if (cache) { if ((cache = await utils.getCachedData(cacheUrl))) {
await this.processDays(cache); await this.processDays(cache);
this.loading--; 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 // Get from network
@ -760,6 +762,7 @@ export default defineComponent({
console.error(err); console.error(err);
showError(err?.response?.data?.message || err.message); showError(err?.response?.data?.message || err.message);
} finally { } finally {
// If cache is set here, loading was already decremented
if (!cache) this.loading--; if (!cache) this.loading--;
} }
}, },
@ -891,7 +894,8 @@ export default defineComponent({
// Look for cache // Look for cache
const cacheUrl = this.getDayUrl(dayId); const cacheUrl = this.getDayUrl(dayId);
try { try {
this.processDay(dayId, await utils.getCachedData(cacheUrl)); const cache = await utils.getCachedData<IPhoto[]>(cacheUrl);
if (cache) this.processDay(dayId, cache);
} catch { } catch {
console.warn(`Failed to process day cache: ${cacheUrl}`); console.warn(`Failed to process day cache: ${cacheUrl}`);
} }
@ -936,8 +940,8 @@ export default defineComponent({
// It is already sorted in dayid DESC // It is already sorted in dayid DESC
const dayMap = new Map<number, IPhoto[]>(); const dayMap = new Map<number, IPhoto[]>();
for (const photo of data) { for (const photo of data) {
if (!dayMap.get(photo.dayid)) dayMap.set(photo.dayid, []); if (!dayMap.has(photo.dayid)) dayMap.set(photo.dayid, []);
dayMap.get(photo.dayid).push(photo); dayMap.get(photo.dayid)!.push(photo);
} }
// Store cache asynchronously // Store cache asynchronously
@ -997,9 +1001,10 @@ export default defineComponent({
// Set and make reactive // Set and make reactive
day.count = data.length; day.count = data.length;
day.detail = data; day.detail = data;
day.rows ??= [];
// Reset rows including placeholders // Reset rows including placeholders
for (const row of head.day.rows || []) { for (const row of day.rows) {
row.photos = []; row.photos = [];
} }
@ -1136,8 +1141,8 @@ export default defineComponent({
// These may be valid, e.g. in face rects. All we need to have // These may be valid, e.g. in face rects. All we need to have
// is a unique Vue key for the v-for loop. // is a unique Vue key for the v-for loop.
const key = photo.faceid || photo.fileid; 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}`; photo.key = `${key}-${val}`;
seen.set(key, val + 1); seen.set(key, val + 1);
} else { } else {
@ -1146,7 +1151,7 @@ export default defineComponent({
} }
// Add photo to row // Add photo to row
row.photos.push(photo); row.photos!.push(photo);
delete row.pct; delete row.pct;
} }
@ -1187,8 +1192,8 @@ export default defineComponent({
needAdjust = true; needAdjust = true;
// Remove from day // Remove from day
const idx = head.day.rows.indexOf(row); const idx = day.rows.indexOf(row);
if (idx >= 0) head.day.rows.splice(idx, 1); if (idx >= 0) day.rows.splice(idx, 1);
} }
// This will be true even if the head is being spliced // 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 */ /** Add and get a new blank photos row */
addRow(day: IDay): IRow { addRow(day: IDay): IRow {
// Make sure rows exists
day.rows ??= [];
// Create new row // Create new row
const row = { const row = {
id: `${day.dayid}-${day.rows.length}`, id: `${day.dayid}-${day.rows.length}`,
@ -1242,7 +1250,7 @@ export default defineComponent({
if (delPhotos.length === 0) return; if (delPhotos.length === 0) return;
// Get all days that need to be updatd // 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); const delPhotosSet = new Set(delPhotos);
// Animate the deletion // Animate the deletion
@ -1258,8 +1266,8 @@ export default defineComponent({
// Reflow all touched days // Reflow all touched days
for (const day of updatedDays) { for (const day of updatedDays) {
const newDetail = day.detail.filter((p) => !delPhotosSet.has(p)); const newDetail = day.detail?.filter((p) => !delPhotosSet.has(p));
this.processDay(day.dayid, newDetail); this.processDay(day.dayid, newDetail!);
} }
}, },
}, },

View File

@ -36,7 +36,7 @@ import { defineComponent, PropType } from "vue";
import { getCurrentUser } from "@nextcloud/auth"; import { getCurrentUser } from "@nextcloud/auth";
import NcCounterBubble from "@nextcloud/vue/dist/Components/NcCounterBubble"; 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 { getPreviewUrl } from "../../services/utils/helpers";
import errorsvg from "../../assets/error.svg"; import errorsvg from "../../assets/error.svg";
@ -66,7 +66,11 @@ export default defineComponent({
if (this.error) return errorsvg; if (this.error) return errorsvg;
if (this.album) { 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); return getPreviewUrl(mock, true, 512);
} }

View File

@ -46,7 +46,10 @@ export default defineComponent({
mixins: [UserConfig], mixins: [UserConfig],
props: { props: {
data: Object as PropType<IFolder>, data: {
type: Object as PropType<IFolder>,
required: true,
},
}, },
data: () => ({ data: () => ({

View File

@ -99,7 +99,7 @@ export default defineComponent({
data: () => ({ data: () => ({
touchTimer: 0, touchTimer: 0,
faceSrc: null, faceSrc: null as string | null,
}), }),
watch: { watch: {
@ -144,6 +144,7 @@ export default defineComponent({
if (this.data.liveid) { if (this.data.liveid) {
return utils.getLivePhotoVideoUrl(this.data, true); return utils.getLivePhotoVideoUrl(this.data, true);
} }
return null;
}, },
src(): string | null { src(): string | null {
@ -171,7 +172,7 @@ export default defineComponent({
let base = 256; let base = 256;
// Check if displayed size is larger than the image // 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 // Get a bigger image
// 1. No trickery here, just get one size bigger. This is to // 1. No trickery here, just get one size bigger. This is to
// ensure that the images can be cached even after reflow. // ensure that the images can be cached even after reflow.
@ -208,6 +209,7 @@ export default defineComponent({
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
const context = canvas.getContext("2d"); const context = canvas.getContext("2d");
if (!context) return; // failed to create canvas
canvas.width = img.naturalWidth; canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight; canvas.height = img.naturalHeight;
@ -223,6 +225,7 @@ export default defineComponent({
canvas.toBlob( canvas.toBlob(
(blob) => { (blob) => {
if (!blob) return;
this.faceSrc = URL.createObjectURL(blob); this.faceSrc = URL.createObjectURL(blob);
}, },
"image/jpeg", "image/jpeg",

View File

@ -49,7 +49,7 @@ document.addEventListener("DOMContentLoaded", () => {
/** Change stickiness for a BLOB url */ /** Change stickiness for a BLOB url */
export async function sticky(url: string, delta: number) { export async function sticky(url: string, delta: number) {
if (!BLOB_STICKY.has(url)) BLOB_STICKY.set(url, 0); 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) { if (val <= 0) {
BLOB_STICKY.delete(url); BLOB_STICKY.delete(url);
} else { } else {
@ -62,15 +62,16 @@ export async function fetchImage(url: string) {
startWorker(); startWorker();
// Check memcache entry // 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 // Fetch image
const blobUrl = await importer<typeof w.fetchImageSrc>("fetchImageSrc")(url); const blobUrl = await importer<typeof w.fetchImageSrc>("fetchImageSrc")(url);
// Check memcache entry again and revoke if it was added in the meantime // 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); URL.revokeObjectURL(blobUrl);
return BLOB_CACHE.get(url)[1]; return entry[1];
} }
// Create new memecache entry // Create new memecache entry

View File

@ -1,19 +1,19 @@
import { CacheExpiration } from "workbox-expiration"; import { CacheExpiration } from "workbox-expiration";
import { workerExport } from "../../worker"; import { workerExport } from "../../worker";
type BlobCallback = { interface BlobCallback {
resolve: (blob: Blob) => void; resolve: (blob: Blob) => void;
reject: (err: Error) => void; reject: (err: Error) => void;
}; }
// Queue of requests to fetch preview images // Queue of requests to fetch preview images
type FetchPreviewObject = { interface FetchPreviewObject {
origUrl: string; origUrl: string;
url: URL; url: URL;
fileid: number; fileid: number;
reqid: number; reqid: number;
done?: boolean; done?: boolean;
}; }
let fetchPreviewQueue: FetchPreviewObject[] = []; let fetchPreviewQueue: FetchPreviewObject[] = [];
// Pending requests // Pending requests
@ -22,9 +22,12 @@ const pendingUrls = new Map<string, BlobCallback[]>();
// Cache for preview images // Cache for preview images
const cacheName = "images"; const cacheName = "images";
let imageCache: Cache; let imageCache: Cache;
(async () => { caches
imageCache = await caches.open(cacheName); .open(cacheName)
})(); .then((c) => (imageCache = c))
.catch(() => {
/* ignore */
});
// Expiration for cache // Expiration for cache
const expirationManager = new CacheExpiration(cacheName, { const expirationManager = new CacheExpiration(cacheName, {
@ -60,7 +63,9 @@ async function flushPreviewQueue() {
// it came from a multipreview, so that we can try fetching // it came from a multipreview, so that we can try fetching
// the single image instead // the single image instead
const blob = await res.blob(); const blob = await res.blob();
pendingUrls.get(url)?.forEach((cb) => cb?.resolve?.(blob)); pendingUrls.get(url)?.forEach((cb) => {
cb?.resolve?.(blob);
});
pendingUrls.delete(url); pendingUrls.delete(url);
// Cache response // Cache response
@ -68,15 +73,17 @@ async function flushPreviewQueue() {
}; };
// Throw error on URL // Throw error on URL
const reject = (url: string, e: any) => { const reject = (url: string, e: any): void => {
pendingUrls.get(url)?.forEach((cb) => cb?.reject?.(e)); pendingUrls.get(url)?.forEach((cb) => {
cb?.reject?.(e);
});
pendingUrls.delete(url); pendingUrls.delete(url);
}; };
// Make a single-file request // Make a single-file request
const fetchOneSafe = async (p: FetchPreviewObject) => { const fetchOneSafe = async (p: FetchPreviewObject) => {
try { try {
resolve(p.origUrl, await fetchOneImage(p.origUrl)); await resolve(p.origUrl, await fetchOneImage(p.origUrl));
} catch (e) { } catch (e) {
reject(p.origUrl, e); reject(p.origUrl, e);
} }
@ -84,7 +91,8 @@ async function flushPreviewQueue() {
// Check if only one request, not worth a multipreview // Check if only one request, not worth a multipreview
if (fetchPreviewQueueCopy.length === 1) { if (fetchPreviewQueueCopy.length === 1) {
return fetchOneSafe(fetchPreviewQueueCopy[0]); await fetchOneSafe(fetchPreviewQueueCopy[0]);
return;
} }
// Create aggregated request body // Create aggregated request body
@ -99,7 +107,8 @@ async function flushPreviewQueue() {
try { try {
// Fetch multipreview // Fetch multipreview
const res = await fetchMultipreview(files); 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 // Create fake headers for 7-day expiry
const headers = { const headers = {
@ -119,7 +128,7 @@ async function flushPreviewQueue() {
reqid: number; reqid: number;
len: number; len: number;
type: string; type: string;
} = null; } | null = null;
// Index at which we are currently reading // Index at which we are currently reading
let idx = 0; let idx = 0;
@ -161,30 +170,31 @@ async function flushPreviewQueue() {
if (bufSize - jsonStart < jsonLen) break; if (bufSize - jsonStart < jsonLen) break;
const jsonB = buffer.slice(jsonStart, jsonStart + jsonLen); const jsonB = buffer.slice(jsonStart, jsonStart + jsonLen);
const jsonT = new TextDecoder().decode(jsonB); const jsonT = new TextDecoder().decode(jsonB);
params = JSON.parse(jsonT);
idx = jsonStart + jsonLen; idx = jsonStart + jsonLen;
params = JSON.parse(jsonT);
params = params!;
} }
// Read the image data // Read the image data
if (bufSize - idx < params.len) break; if (bufSize - idx < params!.len) break;
const imgBlob = new Blob([buffer.slice(idx, idx + params.len)], { const imgBlob = new Blob([buffer.slice(idx, idx + params!.len)], {
type: params.type, type: params!.type,
}); });
idx += params.len; idx += params!.len;
// Initiate callbacks // Initiate callbacks
fetchPreviewQueueCopy for (const p of fetchPreviewQueueCopy) {
.filter((p) => p.reqid === params.reqid && !p.done) if (p.reqid === params.reqid && !p.done) {
.forEach((p) => {
try { try {
const dummy = getResponse(imgBlob, params.type, headers); const dummy = getResponse(imgBlob, params!.type, headers);
resolve(p.origUrl, dummy); await resolve(p.origUrl, dummy);
p.done = true; p.done = true;
} catch (e) { } catch (e) {
// In case of error, we want to try fetching the single // In case of error, we want to try fetching the single
// image instead, so we don't reject here // image instead, so we don't reject here
} }
}); }
}
// Reset for next iteration // Reset for next iteration
params = null; params = null;
@ -272,7 +282,7 @@ function getResponse(blob: Blob, type: string | null, headers: any = {}) {
"Content-Type": type || headers["content-type"], "Content-Type": type || headers["content-type"],
"Content-Length": blob.size.toString(), "Content-Length": blob.size.toString(),
"Cache-Control": headers["cache-control"], "Cache-Control": headers["cache-control"],
Expires: headers["expires"], Expires: headers.expires,
}, },
}); });
} }

View File

@ -9,11 +9,11 @@
const pathname = self.location.pathname; const pathname = self.location.pathname;
__webpack_public_path__ = pathname.substring(0, pathname.lastIndexOf("/") + 1); __webpack_public_path__ = pathname.substring(0, pathname.lastIndexOf("/") + 1);
const missedQueue = []; const missedQueue: any[] = [];
self.onmessage = function (val: any) { self.onmessage = function (val: any) {
missedQueue.push(val); missedQueue.push(val);
}; };
import("./XImgWorker").then(function () { import("./XImgWorker").then(function () {
missedQueue.forEach((data: any) => self.onmessage(data)); missedQueue.forEach((data: any) => self.onmessage?.(data));
}); });

View File

@ -210,7 +210,7 @@ export default defineComponent({
selectedCollaboratorsKeys: [] as string[], selectedCollaboratorsKeys: [] as string[],
currentSearchResults: [] as Collaborator[], currentSearchResults: [] as Collaborator[],
loadingAlbum: false, loadingAlbum: false,
errorFetchingAlbum: null, errorFetchingAlbum: null as number | null,
loadingCollaborators: false, loadingCollaborators: false,
errorFetchingCollaborators: null, errorFetchingCollaborators: null,
randomId: Math.random().toString().substring(2, 10), randomId: Math.random().toString().substring(2, 10),
@ -370,10 +370,9 @@ export default defineComponent({
this.loadingAlbum = true; this.loadingAlbum = true;
this.errorFetchingAlbum = null; this.errorFetchingAlbum = null;
const album = await dav.getAlbum( const uid = getCurrentUser()?.uid.toString();
getCurrentUser()?.uid.toString(), if (!uid) return;
this.albumName const album = await dav.getAlbum(uid, this.albumName);
);
this.populateCollaborators(album.collaborators); this.populateCollaborators(album.collaborators);
} catch (error) { } catch (error) {
if (error.response?.status === 404) { if (error.response?.status === 404) {
@ -401,10 +400,9 @@ export default defineComponent({
async updateAlbumCollaborators() { async updateAlbumCollaborators() {
try { try {
const album = await dav.getAlbum( const uid = getCurrentUser()?.uid?.toString();
getCurrentUser()?.uid.toString(), if (!uid) return;
this.albumName const album = await dav.getAlbum(uid, this.albumName);
);
await dav.updateAlbum(album, { await dav.updateAlbum(album, {
albumName: this.albumName, albumName: this.albumName,
properties: { properties: {

View File

@ -176,16 +176,18 @@ export default defineComponent({
}, },
dateDiff() { dateDiff() {
return this.date.getTime() - this.dateLast.getTime(); return this.date && this.dateLast
? this.date.getTime() - this.dateLast.getTime()
: 0;
}, },
origDateNewest() { origDateNewest() {
return new Date(this.sortedPhotos[0].datetaken); return new Date(this.sortedPhotos[0].datetaken!);
}, },
origDateOldest() { origDateOldest() {
return new Date( 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: { methods: {
init() { 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 // 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 // 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.year = date.getUTCFullYear().toString();
this.month = (date.getUTCMonth() + 1).toString(); this.month = (date.getUTCMonth() + 1).toString();
this.day = date.getUTCDate().toString(); this.day = date.getUTCDate().toString();
@ -228,7 +233,7 @@ export default defineComponent({
// Get date of oldest photo // Get date of oldest photo
if (photos.length > 1) { 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.yearLast = date.getUTCFullYear().toString();
this.monthLast = (date.getUTCMonth() + 1).toString(); this.monthLast = (date.getUTCMonth() + 1).toString();
this.dayLast = date.getUTCDate().toString(); this.dayLast = date.getUTCDate().toString();
@ -262,7 +267,7 @@ export default defineComponent({
return undefined; return undefined;
} }
if (this.sortedPhotos.length === 0) { if (this.sortedPhotos.length === 0 || !this.date) {
return undefined; return undefined;
} }
@ -278,7 +283,7 @@ export default defineComponent({
}, },
newestChange(time = false) { newestChange(time = false) {
if (this.sortedPhotos.length === 0) { if (this.sortedPhotos.length === 0 || !this.date) {
return; return;
} }

View File

@ -203,16 +203,17 @@ export default defineComponent({
}, },
result() { result() {
if (!this.dirty) { if (!this.dirty) return null;
return null;
} const lat = (this.lat || 0).toFixed(6);
const lon = (this.lon || 0).toFixed(6);
return { return {
GPSLatitude: this.lat, GPSLatitude: lat,
GPSLongitude: this.lon, GPSLongitude: lon,
GPSLatitudeRef: this.lat, GPSLatitudeRef: lat,
GPSLongitudeRef: this.lon, GPSLongitudeRef: lon,
GPSCoordinates: `${this.lat.toFixed(6)}, ${this.lon.toFixed(6)}`, GPSCoordinates: `${lat}, ${lon}`,
}; };
}, },
}, },

View File

@ -92,7 +92,7 @@ export default defineComponent({
mixins: [UserConfig], mixins: [UserConfig],
data: () => ({ data: () => ({
photos: null as IPhoto[], photos: null as IPhoto[] | null,
sections: [] as number[], sections: [] as number[],
show: false, show: false,
processing: false, processing: false,
@ -183,7 +183,7 @@ export default defineComponent({
this.processing = true; this.processing = true;
// Update exif fields // Update exif fields
const calls = this.photos.map((p) => async () => { const calls = this.photos!.map((p) => async () => {
try { try {
let dirty = false; let dirty = false;
const fileid = p.fileid; const fileid = p.fileid;
@ -223,7 +223,7 @@ export default defineComponent({
} }
} finally { } finally {
done++; done++;
this.progress = Math.round((done * 100) / this.photos.length); this.progress = Math.round((done * 100) / this.photos!.length);
} }
}); });

View File

@ -40,7 +40,7 @@ export default defineComponent({
methods: { methods: {
init() { init() {
let tagIds: number[] = null; let tagIds: number[] | null = null;
// Find common tags in all selected photos // Find common tags in all selected photos
for (const photo of this.photos) { for (const photo of this.photos) {
@ -53,7 +53,7 @@ export default defineComponent({
tagIds = tagIds ? [...tagIds].filter((x) => s.has(x)) : [...s]; tagIds = tagIds ? [...tagIds].filter((x) => s.has(x)) : [...s];
} }
this.tagSelection = tagIds; this.tagSelection = tagIds || [];
this.origIds = new Set(this.tagSelection); this.origIds = new Set(this.tagSelection);
}, },

View File

@ -83,7 +83,7 @@ export default defineComponent({
} else { } else {
await dav.setVisibilityPeopleFaceRecognition(this.name, false); await dav.setVisibilityPeopleFaceRecognition(this.name, false);
} }
this.$router.push({ name: this.$route.name }); this.$router.push({ name: this.$route.name as string });
this.close(); this.close();
} catch (error) { } catch (error) {
console.log(error); console.log(error);
@ -96,4 +96,4 @@ export default defineComponent({
}, },
}, },
}); });
</script> </script>

View File

@ -97,7 +97,7 @@ export default defineComponent({
await dav.renamePeopleFaceRecognition(this.oldName, this.name); await dav.renamePeopleFaceRecognition(this.oldName, this.name);
} }
this.$router.push({ this.$router.push({
name: this.$route.name, name: this.$route.name as string,
params: { user: this.user, name: this.name }, params: { user: this.user, name: this.name },
}); });
this.close(); this.close();

View File

@ -48,7 +48,7 @@ export default defineComponent({
user: "", user: "",
name: "", name: "",
list: null as ICluster[] | null, list: null as ICluster[] | null,
fuse: null as Fuse<ICluster>, fuse: null as Fuse<ICluster> | null,
search: "", search: "",
}), }),

View File

@ -130,7 +130,8 @@ export default defineComponent({
} }
}); });
for await (const resp of dav.runInParallel(calls, 10)) { 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) { } catch (error) {
console.error(error); console.error(error);

View File

@ -46,8 +46,8 @@ export default defineComponent({
data: () => ({ data: () => ({
isSidebarShown: false, isSidebarShown: false,
sidebarWidth: 400, sidebarWidth: 400,
trapElements: [], trapElements: [] as HTMLElement[],
_mutationObserver: null, _mutationObserver: null! as MutationObserver,
}), }),
beforeMount() { beforeMount() {
@ -87,7 +87,8 @@ export default defineComponent({
* That way we can adjust the focusTrap * That way we can adjust the focusTrap
*/ */
handleBodyMutation(mutations: MutationRecord[]) { handleBodyMutation(mutations: MutationRecord[]) {
const test = (node: HTMLElement) => const test = (node: Node): node is HTMLElement =>
node instanceof HTMLElement &&
node?.classList?.contains("v-popper__popper"); node?.classList?.contains("v-popper__popper");
mutations.forEach((mutation) => { mutations.forEach((mutation) => {

View File

@ -189,7 +189,7 @@ export default defineComponent({
}, },
getShareLabels(share: IShare): string { getShareLabels(share: IShare): string {
const labels = []; const labels: string[] = [];
if (share.hasPassword) { if (share.hasPassword) {
labels.push(this.t("memories", "Password protected")); labels.push(this.t("memories", "Password protected"));
} }

View File

@ -112,7 +112,7 @@ export default defineComponent({
data: () => { data: () => {
return { return {
photo: null as IPhoto, photo: null as IPhoto | null,
loading: 0, loading: 0,
}; };
}, },
@ -127,7 +127,7 @@ export default defineComponent({
isVideo() { isVideo() {
return ( return (
this.photo && this.photo &&
(this.photo.mimetype.startsWith("video/") || (this.photo.mimetype?.startsWith("video/") ||
this.photo.flag & this.c.FLAG_IS_VIDEO) this.photo.flag & this.c.FLAG_IS_VIDEO)
); );
}, },
@ -160,32 +160,32 @@ export default defineComponent({
}, },
async sharePreview() { async sharePreview() {
const src = utils.getPreviewUrl(this.photo, false, 2048); const src = utils.getPreviewUrl(this.photo!, false, 2048);
this.shareWithHref(src, true); this.shareWithHref(src, true);
}, },
async shareHighRes() { async shareHighRes() {
const fileid = this.photo.fileid; const fileid = this.photo!.fileid;
const src = this.isVideo const src = this.isVideo
? API.VIDEO_TRANSCODE(fileid, "max.mov") ? 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); this.shareWithHref(src, !this.isVideo);
}, },
async shareOriginal() { async shareOriginal() {
this.shareWithHref(dav.getDownloadLink(this.photo)); this.shareWithHref(dav.getDownloadLink(this.photo!));
}, },
async shareLink() { async shareLink() {
this.l(async () => { this.l(async () => {
const fileInfo = (await dav.getFiles([this.photo]))[0]; const fileInfo = (await dav.getFiles([this.photo!]))[0];
globalThis.shareNodeLink(fileInfo.filename, true); globalThis.shareNodeLink(fileInfo.filename, true);
}); });
this.close(); this.close();
}, },
async shareWithHref(href: string, replaceExt = false) { async shareWithHref(href: string, replaceExt = false) {
let blob: Blob; let blob: Blob | undefined;
await this.l(async () => { await this.l(async () => {
const res = await axios.get(href, { responseType: "blob" }); const res = await axios.get(href, { responseType: "blob" });
blob = res.data; blob = res.data;
@ -196,11 +196,11 @@ export default defineComponent({
return; return;
} }
let basename = this.photo.basename; let basename = this.photo?.basename ?? "blank";
if (replaceExt) { if (replaceExt) {
// Fix basename extension // Fix basename extension
let targetExts = []; let targetExts: string[] = [];
if (blob.type === "image/png") { if (blob.type === "image/png") {
targetExts = ["png"]; targetExts = ["png"];
} else { } else {
@ -208,7 +208,8 @@ export default defineComponent({
} }
// Append extension if not found // 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]; basename += "." + targetExts[0];
} }
} }

View File

@ -46,7 +46,7 @@ export default defineComponent({
methods: { methods: {
back() { back() {
this.$router.push({ name: this.$route.name }); this.$router.push({ name: this.$route.name as string });
}, },
}, },
}); });

View File

@ -13,7 +13,7 @@
<NcActions :inline="1"> <NcActions :inline="1">
<NcActionButton <NcActionButton
:aria-label="t('memories', 'Rename person')" :aria-label="t('memories', 'Rename person')"
@click="$refs.editModal.open()" @click="$refs.editModal?.open()"
close-after-click close-after-click
> >
{{ t("memories", "Rename person") }} {{ t("memories", "Rename person") }}
@ -21,7 +21,7 @@
</NcActionButton> </NcActionButton>
<NcActionButton <NcActionButton
:aria-label="t('memories', 'Merge with different person')" :aria-label="t('memories', 'Merge with different person')"
@click="$refs.mergeModal.open()" @click="$refs.mergeModal?.open()"
close-after-click close-after-click
> >
{{ t("memories", "Merge with different person") }} {{ t("memories", "Merge with different person") }}
@ -36,7 +36,7 @@
</NcActionCheckbox> </NcActionCheckbox>
<NcActionButton <NcActionButton
:aria-label="t('memories', 'Remove person')" :aria-label="t('memories', 'Remove person')"
@click="$refs.deleteModal.open()" @click="$refs.deleteModal?.open()"
close-after-click close-after-click
> >
{{ t("memories", "Remove person") }} {{ t("memories", "Remove person") }}
@ -104,7 +104,7 @@ export default defineComponent({
}, },
back() { back() {
this.$router.push({ name: this.$route.name }); this.$router.push({ name: this.$route.name as string });
}, },
changeShowFaceRect() { changeShowFaceRect() {

View File

@ -65,10 +65,10 @@ const OSM_ATTRIBUTION =
const CLUSTER_TRANSITION_TIME = 300; const CLUSTER_TRANSITION_TIME = 300;
type IMarkerCluster = { type IMarkerCluster = {
id?: number; id: number;
center: [number, number]; center: [number, number];
count: number; count: number;
preview?: IPhoto; preview: IPhoto;
dummy?: boolean; dummy?: boolean;
}; };

View File

@ -82,7 +82,7 @@ export default defineComponent({
hasRight: false, hasRight: false,
hasLeft: false, hasLeft: false,
scrollStack: [] as number[], scrollStack: [] as number[],
resizeObserver: null as ResizeObserver, resizeObserver: null! as ResizeObserver,
}), }),
mounted() { mounted() {
@ -146,7 +146,7 @@ export default defineComponent({
year, year,
text, text,
url: "", url: "",
preview: null, preview: null!,
photos: [], photos: [],
}); });
currentText = text; currentText = text;
@ -167,7 +167,7 @@ export default defineComponent({
for (const year of this.years) { for (const year of this.years) {
// Try to prioritize landscape photos on desktop // Try to prioritize landscape photos on desktop
if (globalThis.windowInnerWidth <= 600) { 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); year.preview = utils.randomChoice(landscape);
} }
@ -214,7 +214,7 @@ export default defineComponent({
click(year: IYear) { click(year: IYear) {
const allPhotos = this.years.flatMap((y) => y.photos); const allPhotos = this.years.flatMap((y) => y.photos);
this.viewer.openStatic(year.preview, allPhotos, 512); this.viewer?.openStatic(year.preview, allPhotos, 512);
}, },
}, },
}); });

View File

@ -45,8 +45,8 @@ export default defineComponent({
}, },
data: () => ({ data: () => ({
exif: null as any, exif: null as Object | null,
imageEditor: null as FilerobotImageEditor, imageEditor: null as FilerobotImageEditor | null,
}), }),
computed: { computed: {
@ -116,14 +116,14 @@ export default defineComponent({
}, },
defaultSavedImageName(): string { defaultSavedImageName(): string {
return this.photo.basename; return this.photo.basename || "";
}, },
defaultSavedImageType(): "jpeg" | "png" | "webp" { defaultSavedImageType(): "jpeg" | "png" | "webp" {
if ( 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"; return "jpeg";
}, },
@ -169,7 +169,7 @@ export default defineComponent({
methods: { methods: {
async getImage(): Promise<HTMLImageElement> { async getImage(): Promise<HTMLImageElement> {
const img = new Image(); const img = new Image();
img.name = this.photo.basename; img.name = this.defaultSavedImageName;
await new Promise(async (resolve) => { await new Promise(async (resolve) => {
img.onload = resolve; img.onload = resolve;
@ -200,11 +200,11 @@ export default defineComponent({
*/ */
async onSave( async onSave(
data: { data: {
name?: string; name: string;
extension: string;
width?: number; width?: number;
height?: number; height?: number;
quality?: number; quality?: number;
extension?: string;
fullName?: string; fullName?: string;
imageBase64?: string; imageBase64?: string;
}, },

View File

@ -79,8 +79,8 @@ export default class ImageContentSetup {
slideActivate() { slideActivate() {
const slide = this.lightbox.currSlide; const slide = this.lightbox.currSlide;
if (slide.data.highSrcCond === "always") { if (slide?.data.highSrcCond === "always") {
this.loadFullImage(slide); this.loadFullImage(slide as PsSlide);
} }
} }
@ -97,7 +97,7 @@ export default class ImageContentSetup {
img.classList.add("ximg--full"); img.classList.add("ximg--full");
this.loading++; this.loading++;
this.lightbox.ui.updatePreloaderVisibility(); this.lightbox.ui?.updatePreloaderVisibility();
fetchImage(slide.data.highSrc) fetchImage(slide.data.highSrc)
.then((blobSrc) => { .then((blobSrc) => {
@ -112,7 +112,7 @@ export default class ImageContentSetup {
}) })
.finally(() => { .finally(() => {
this.loading--; this.loading--;
this.lightbox.ui.updatePreloaderVisibility(); this.lightbox.ui?.updatePreloaderVisibility();
}); });
} }
} }

View File

@ -6,17 +6,19 @@ import { getCurrentUser } from "@nextcloud/auth";
import axios from "@nextcloud/axios"; import axios from "@nextcloud/axios";
import { API } from "../../services/API"; 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 Player from "video.js/dist/types/player";
import { QualityLevelList } from "videojs-contrib-quality-levels"; import { QualityLevelList } from "videojs-contrib-quality-levels";
type VideoContent = PsContent & { type VideoContent = PsContent & {
videoElement: HTMLVideoElement; videoElement: HTMLVideoElement | null;
videojs: Player & { videojs:
qualityLevels?: () => QualityLevelList; | (Player & {
}; qualityLevels?: () => QualityLevelList;
plyr: globalThis.Plyr; })
| null;
plyr: globalThis.Plyr | null;
}; };
type PsVideoEvent = PsEvent & { type PsVideoEvent = PsEvent & {
@ -32,7 +34,7 @@ const config_video_default_quality = Number(
/** /**
* Check if slide has video content * 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"; return content?.data?.type === "video";
} }
@ -109,12 +111,12 @@ class VideoContentSetup {
}); });
pswp.on("close", () => { pswp.on("close", () => {
this.destroyVideo(pswp.currSlide.content as VideoContent); this.destroyVideo(pswp.currSlide?.content as VideoContent);
}); });
// Prevent closing when video fullscreen is active // Prevent closing when video fullscreen is active
pswp.on("pointerMove", (e) => { pswp.on("pointerMove", (e) => {
const plyr = (<VideoContent>pswp.currSlide.content)?.plyr; const plyr = (<VideoContent>pswp.currSlide?.content)?.plyr;
if (plyr?.fullscreen.active) { if (plyr?.fullscreen.active) {
e.preventDefault(); e.preventDefault();
} }
@ -161,7 +163,7 @@ class VideoContentSetup {
content.videoElement.setAttribute("playsinline", ""); content.videoElement.setAttribute("playsinline", "");
// Add the video element to the actual container // Add the video element to the actual container
content.element.appendChild(content.videoElement); content.element?.appendChild(content.videoElement);
// Create hls sources if enabled // Create hls sources if enabled
const sources: { const sources: {
@ -202,7 +204,7 @@ class VideoContentSetup {
let hlsFailed = false; let hlsFailed = false;
vjs.on("error", () => { vjs.on("error", () => {
if (vjs.src(undefined).includes("m3u8")) { if (vjs.src(undefined)?.includes("m3u8")) {
hlsFailed = true; hlsFailed = true;
console.warn("PsVideo: HLS stream could not be opened."); console.warn("PsVideo: HLS stream could not be opened.");
@ -245,7 +247,7 @@ class VideoContentSetup {
playWithDelay(); playWithDelay();
}); });
content.videojs.qualityLevels()?.on("addqualitylevel", (e) => { content.videojs.qualityLevels?.()?.on("addqualitylevel", (e) => {
if (e.qualityLevel?.label?.includes("max.m3u8")) { if (e.qualityLevel?.label?.includes("max.m3u8")) {
// This is the highest quality level // This is the highest quality level
// and guaranteed to be the last one // and guaranteed to be the last one
@ -274,6 +276,7 @@ class VideoContentSetup {
destroyVideo(content: VideoContent) { destroyVideo(content: VideoContent) {
if (isVideoContent(content)) { if (isVideoContent(content)) {
// Destroy videojs // Destroy videojs
content.videojs?.pause?.();
content.videojs?.dispose?.(); content.videojs?.dispose?.();
content.videojs = null; content.videojs = null;
@ -283,9 +286,11 @@ class VideoContentSetup {
content.plyr = null; content.plyr = null;
// Clear the video element // Clear the video element
const elem: HTMLDivElement = content.element; if (content.element instanceof HTMLDivElement) {
while (elem.lastElementChild) { const elem = content.element;
elem.removeChild(elem.lastElementChild); while (elem.lastElementChild) {
elem.removeChild(elem.lastElementChild);
}
} }
content.videoElement = null; content.videoElement = null;
@ -301,17 +306,17 @@ class VideoContentSetup {
if (!content.videoElement) return; if (!content.videoElement) return;
// Retain original parent for video element // Retain original parent for video element
const origParent = content.videoElement.parentElement; const origParent = content.videoElement.parentElement!;
// Populate quality list // Populate quality list
let qualityList = content.videojs?.qualityLevels(); let qualityList = content.videojs?.qualityLevels?.();
let qualityNums: number[]; let qualityNums: number[] | undefined;
if (qualityList && qualityList.length >= 1) { if (qualityList && qualityList.length >= 1) {
const s = new Set<number>(); const s = new Set<number>();
let hasMax = false; let hasMax = false;
for (let i = 0; i < qualityList?.length; i++) { for (let i = 0; i < qualityList?.length; i++) {
const { width, height, label } = qualityList[i]; const { width, height, label } = qualityList[i];
s.add(Math.min(width, height)); s.add(Math.min(width!, height!));
if (label?.includes("max.m3u8")) { if (label?.includes("max.m3u8")) {
hasMax = true; hasMax = true;
@ -351,10 +356,10 @@ class VideoContentSetup {
options: qualityNums, options: qualityNums,
forced: true, forced: true,
onChange: (quality: number) => { onChange: (quality: number) => {
qualityList = content.videojs?.qualityLevels(); qualityList = content.videojs?.qualityLevels?.();
if (!qualityList || !content.videojs) return; if (!qualityList || !content.videojs) return;
const isHLS = content.videojs.src(undefined).includes("m3u8"); const isHLS = content.videojs.src(undefined)?.includes("m3u8");
if (quality === -2) { if (quality === -2) {
// Direct playback // Direct playback
@ -378,7 +383,7 @@ class VideoContentSetup {
// Enable only the selected quality // Enable only the selected quality
for (let i = 0; i < qualityList.length; ++i) { for (let i = 0; i < qualityList.length; ++i) {
const { width, height, label } = qualityList[i]; const { width, height, label } = qualityList[i];
const pixels = Math.min(width, height); const pixels = Math.min(width!, height!);
qualityList[i].enabled = qualityList[i].enabled =
!quality || // auto !quality || // auto
pixels === quality || // exact match pixels === quality || // exact match
@ -390,40 +395,42 @@ class VideoContentSetup {
// Initialize Plyr and custom CSS // Initialize Plyr and custom CSS
const plyr = new Plyr(content.videoElement, opts); const plyr = new Plyr(content.videoElement, opts);
plyr.elements.container.style.height = "100%"; const container = plyr.elements.container!;
plyr.elements.container.style.width = "100%";
plyr.elements.container container.style.height = "100%";
container.style.width = "100%";
container
.querySelectorAll("button") .querySelectorAll("button")
.forEach((el) => el.classList.add("button-vue")); .forEach((el) => el.classList.add("button-vue"));
plyr.elements.container container
.querySelectorAll("progress") .querySelectorAll("progress")
.forEach((el) => el.classList.add("vue")); .forEach((el) => el.classList.add("vue"));
plyr.elements.container.style.backgroundColor = "transparent"; container.style.backgroundColor = "transparent";
plyr.elements.wrapper.style.backgroundColor = "transparent"; plyr.elements.wrapper!.style.backgroundColor = "transparent";
// Set the fullscreen element to the container // Set the fullscreen element to the container
plyr.elements.fullscreen = content.slide.holderElement; plyr.elements.fullscreen = content.slide?.holderElement || null;
// Done with init // Done with init
content.plyr = plyr; content.plyr = plyr;
// Wait for animation to end before showing Plyr // Wait for animation to end before showing Plyr
plyr.elements.container.style.opacity = "0"; container.style.opacity = "0";
setTimeout(() => { setTimeout(() => {
plyr.elements.container.style.opacity = "1"; container.style.opacity = "1";
}, 250); }, 250);
// Restore original parent of video element // Restore original parent of video element
origParent.appendChild(content.videoElement); origParent.appendChild(content.videoElement);
// Move plyr to the slide container // Move plyr to the slide container
content.slide.holderElement.appendChild(plyr.elements.container); content.slide?.holderElement?.appendChild(container);
// Add fullscreen orientation hooks // Add fullscreen orientation hooks
if (screen.orientation?.lock) { if (screen.orientation?.lock) {
// Store the previous orientation // Store the previous orientation
// This is because unlocking (at least on Chrome) does // This is because unlocking (at least on Chrome) does
// not restore the previous orientation // not restore the previous orientation
let previousOrientation: OrientationLockType; let previousOrientation: OrientationLockType | undefined;
// Lock orientation when entering fullscreen // Lock orientation when entering fullscreen
plyr.on("enterfullscreen", async (event) => { plyr.on("enterfullscreen", async (event) => {
@ -461,25 +468,25 @@ class VideoContentSetup {
} }
updateRotation(content: VideoContent, val?: number): boolean { updateRotation(content: VideoContent, val?: number): boolean {
if (!content.videojs) return; if (!content.videojs) return false;
content.videoElement = content.videojs.el()?.querySelector("video"); content.videoElement = content.videojs.el()?.querySelector("video");
if (!content.videoElement) return; if (!content.videoElement) return false;
const photo = content.data.photo; const photo = content.data.photo;
const exif = photo.imageInfo?.exif; const exif = photo.imageInfo?.exif;
const rotation = val ?? Number(exif?.Rotation || 0); 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) { if (rotation && shouldRotate) {
let transform = `rotate(${rotation}deg)`; let transform = `rotate(${rotation}deg)`;
const hasRotation = rotation === 90 || rotation === 270; const hasRotation = rotation === 90 || rotation === 270;
if (hasRotation) { if (hasRotation) {
content.videoElement.style.width = content.element.style.height; content.videoElement.style.width = content.element!.style.height;
content.videoElement.style.height = content.element.style.width; 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"; content.videoElement.style.transformOrigin = "bottom left";
} }

View File

@ -237,7 +237,7 @@ export default defineComponent({
data: () => ({ data: () => ({
isOpen: false, isOpen: false,
originalTitle: null, originalTitle: null as string | null,
editorOpen: false, editorOpen: false,
editorSrc: "", editorSrc: "",
@ -301,7 +301,7 @@ export default defineComponent({
/** Route is public */ /** Route is public */
routeIsPublic(): boolean { routeIsPublic(): boolean {
return this.$route.name?.endsWith("-share"); return this.$route.name?.endsWith("-share") ?? false;
}, },
/** Route is album */ /** Route is album */
@ -323,7 +323,7 @@ export default defineComponent({
/** Is the current slide a video */ /** Is the current slide a video */
isVideo(): boolean { 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 */ /** Is the current slide a live photo */
@ -354,12 +354,12 @@ export default defineComponent({
/** Show edit buttons */ /** Show edit buttons */
canEdit(): boolean { canEdit(): boolean {
return this.currentPhoto?.imageInfo?.permissions?.includes("U"); return this.currentPhoto?.imageInfo?.permissions?.includes("U") ?? false;
}, },
/** Show delete button */ /** Show delete button */
canDelete(): boolean { canDelete(): boolean {
return this.currentPhoto?.imageInfo?.permissions?.includes("D"); return this.currentPhoto?.imageInfo?.permissions?.includes("D") ?? false;
}, },
/** Show share button */ /** Show share button */
@ -399,7 +399,7 @@ export default defineComponent({
const photo = this.currentPhoto; const photo = this.currentPhoto;
const isvideo = photo && photo.flag & this.c.FLAG_IS_VIDEO; const isvideo = photo && photo.flag & this.c.FLAG_IS_VIDEO;
if (photo && !isvideo && photo.fileid === fileid) { if (photo && !isvideo && photo.fileid === fileid) {
this.photoswipe.refreshSlideContent(this.currIndex); this.photoswipe?.refreshSlideContent(this.currIndex);
} }
}, },
@ -487,7 +487,7 @@ export default defineComponent({
) { ) {
return; return;
} }
_onFocusIn.call(this.photoswipe.keyboard, e); _onFocusIn.call(this.photoswipe!.keyboard, e);
}; };
// Refresh sidebar on change // Refresh sidebar on change
@ -556,7 +556,7 @@ export default defineComponent({
// Update vue route for deep linking // Update vue route for deep linking
this.photoswipe.on("slideActivate", (e) => { this.photoswipe.on("slideActivate", (e) => {
this.currIndex = this.photoswipe.currIndex; this.currIndex = this.photoswipe!.currIndex;
const photo = e.slide?.data?.photo; const photo = e.slide?.data?.photo;
this.setRouteHash(photo); this.setRouteHash(photo);
this.updateTitle(photo); this.updateTitle(photo);
@ -619,15 +619,20 @@ export default defineComponent({
}, },
/** Open using start photo and rows list */ /** Open using start photo and rows list */
async open(anchorPhoto: IPhoto, rows?: IRow[]) { async open(anchorPhoto: IPhoto, rows: IRow[]) {
this.list = [...anchorPhoto.d.detail]; const detail = anchorPhoto.d?.detail;
let startIndex = -1; 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 // Get days list and map
for (const r of rows) { for (const r of rows) {
if (r.type === IRowType.HEAD) { if (r.type === IRowType.HEAD) {
if (r.day.dayid == anchorPhoto.d.dayid) { if (r.day.dayid == anchorPhoto.dayid) {
startIndex = r.day.detail.indexOf(anchorPhoto);
this.globalAnchor = this.globalCount; this.globalAnchor = this.globalCount;
} }
@ -638,18 +643,18 @@ export default defineComponent({
} }
// Create basic viewer // Create basic viewer
await this.createBase({ const photoswipe = await this.createBase({
index: this.globalAnchor + startIndex, index: this.globalAnchor + startIndex,
}); });
// Lazy-generate item data. // Lazy-generate item data.
// Load the next two days in the timeline. // Load the next two days in the timeline.
this.photoswipe.addFilter("itemData", (itemData, index) => { photoswipe.addFilter("itemData", (itemData, index) => {
// Get photo object from list // Get photo object from list
let idx = index - this.globalAnchor; let idx = index - this.globalAnchor;
if (idx < 0) { if (idx < 0) {
// Load previous day // Load previous day
const firstDayId = this.list[0].d.dayid; const firstDayId = this.list[0].dayid;
const firstDayIdx = utils.binarySearch(this.dayIds, firstDayId); const firstDayIdx = utils.binarySearch(this.dayIds, firstDayId);
if (firstDayIdx === 0) { if (firstDayIdx === 0) {
// No previous day // No previous day
@ -657,7 +662,7 @@ export default defineComponent({
} }
const prevDayId = this.dayIds[firstDayIdx - 1]; const prevDayId = this.dayIds[firstDayIdx - 1];
const prevDay = this.days.get(prevDayId); const prevDay = this.days.get(prevDayId);
if (!prevDay.detail) { if (!prevDay?.detail) {
console.error("[BUG] No detail for previous day"); console.error("[BUG] No detail for previous day");
return {}; return {};
} }
@ -665,7 +670,7 @@ export default defineComponent({
this.globalAnchor -= prevDay.count; this.globalAnchor -= prevDay.count;
} else if (idx >= this.list.length) { } else if (idx >= this.list.length) {
// Load next day // 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); const lastDayIdx = utils.binarySearch(this.dayIds, lastDayId);
if (lastDayIdx === this.dayIds.length - 1) { if (lastDayIdx === this.dayIds.length - 1) {
// No next day // No next day
@ -673,7 +678,7 @@ export default defineComponent({
} }
const nextDayId = this.dayIds[lastDayIdx + 1]; const nextDayId = this.dayIds[lastDayIdx + 1];
const nextDay = this.days.get(nextDayId); const nextDay = this.days.get(nextDayId);
if (!nextDay.detail) { if (!nextDay?.detail) {
console.error("[BUG] No detail for next day"); console.error("[BUG] No detail for next day");
return {}; return {};
} }
@ -689,12 +694,12 @@ export default defineComponent({
} }
// Preload next and previous 3 days // 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) => { const preload = (idx: number) => {
if ( if (
idx > 0 && idx > 0 &&
idx < this.dayIds.length && idx < this.dayIds.length &&
!this.days.get(this.dayIds[idx]).detail !this.days.get(this.dayIds[idx])?.detail
) { ) {
this.fetchDay(this.dayIds[idx]); this.fetchDay(this.dayIds[idx]);
} }
@ -719,13 +724,13 @@ export default defineComponent({
}); });
// Get the thumbnail image // Get the thumbnail image
this.photoswipe.addFilter("thumbEl", (thumbEl, data, index) => { photoswipe.addFilter("thumbEl", (thumbEl, data, index) => {
const photo = this.list[index - this.globalAnchor]; const photo = this.list[index - this.globalAnchor];
if (!photo || !photo.w || !photo.h) return thumbEl; if (!photo || !photo.w || !photo.h) return thumbEl as HTMLElement;
return this.thumbElem(photo) || thumbEl; 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 // Scroll to keep the thumbnail in view
const thumb = this.thumbElem(e.slide.data?.photo); const thumb = this.thumbElem(e.slide.data?.photo);
if (thumb && this.fullyOpened) { if (thumb && this.fullyOpened) {
@ -741,13 +746,13 @@ export default defineComponent({
} }
// Remove active class from others and add to this one // Remove active class from others and add to this one
this.photoswipe.element photoswipe.element
.querySelectorAll(".pswp__item") ?.querySelectorAll(".pswp__item")
.forEach((el) => el.classList.remove("active")); .forEach((el) => el.classList.remove("active"));
e.slide.holderElement?.classList.add("active"); e.slide.holderElement?.classList.add("active");
}); });
this.photoswipe.init(); photoswipe.init();
}, },
/** Close the viewer */ /** Close the viewer */
@ -758,14 +763,14 @@ export default defineComponent({
/** Open with a static list of photos */ /** Open with a static list of photos */
async openStatic(photo: IPhoto, list: IPhoto[], thumbSize?: number) { async openStatic(photo: IPhoto, list: IPhoto[], thumbSize?: number) {
this.list = list; this.list = list;
await this.createBase({ const photoswipe = await this.createBase({
index: list.findIndex((p) => p.fileid === photo.fileid), index: list.findIndex((p) => p.fileid === photo.fileid),
}); });
this.globalCount = list.length; this.globalCount = list.length;
this.globalAnchor = 0; this.globalAnchor = 0;
this.photoswipe.addFilter("itemData", (itemData, index) => ({ photoswipe.addFilter("itemData", (itemData, index) => ({
...this.getItemData(this.list[index]), ...this.getItemData(this.list[index]),
msrc: thumbSize msrc: thumbSize
? utils.getPreviewUrl(photo, false, thumbSize) ? utils.getPreviewUrl(photo, false, thumbSize)
@ -773,7 +778,7 @@ export default defineComponent({
})); }));
this.isOpen = true; this.isOpen = true;
this.photoswipe.init(); photoswipe!.init();
}, },
/** Get base data object */ /** Get base data object */
@ -849,7 +854,7 @@ export default defineComponent({
if (important.length > 0) return important[0] as HTMLImageElement; if (important.length > 0) return important[0] as HTMLImageElement;
// Find element within 500px of the screen top // Find element within 500px of the screen top
let elem: HTMLImageElement; let elem: HTMLImageElement | undefined;
elems.forEach((e) => { elems.forEach((e) => {
const rect = e.getBoundingClientRect(); const rect = e.getBoundingClientRect();
if (rect.top > -500) { if (rect.top > -500) {
@ -896,7 +901,7 @@ export default defineComponent({
if (!this.canEdit) return; if (!this.canEdit) return;
// Prevent editing Live Photos // Prevent editing Live Photos
if (this.currentPhoto.liveid) { if (this.isLivePhoto) {
showError( showError(
this.t("memories", "Editing is currently disabled for Live Photos") this.t("memories", "Editing is currently disabled for Live Photos")
); );
@ -909,7 +914,7 @@ export default defineComponent({
/** Share the current photo externally */ /** Share the current photo externally */
async shareCurrent() { async shareCurrent() {
globalThis.sharePhoto(this.currentPhoto); globalThis.sharePhoto(this.currentPhoto!);
}, },
/** Key press events */ /** Key press events */
@ -925,7 +930,7 @@ export default defineComponent({
/** Delete this photo and refresh */ /** Delete this photo and refresh */
async deleteCurrent() { async deleteCurrent() {
let idx = this.photoswipe.currIndex - this.globalAnchor; let idx = this.photoswipe!.currIndex - this.globalAnchor;
const photo = this.list[idx]; const photo = this.list[idx];
if (!photo) return; if (!photo) return;
@ -950,22 +955,24 @@ export default defineComponent({
// If this is the last photo, move to the previous photo first // If this is the last photo, move to the previous photo first
// https://github.com/pulsejet/memories/issues/269 // https://github.com/pulsejet/memories/issues/269
if (idx === this.list.length - 1) { if (idx === this.list.length - 1) {
this.photoswipe.prev(); this.photoswipe!.prev();
// Some photos might lazy load, so recompute idx for the next element // 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.list.splice(idx, 1);
this.globalCount--; this.globalCount--;
for (let i = idx - 3; i <= idx + 3; i++) { 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 */ /** Play the current live photo */
playLivePhoto() { playLivePhoto() {
this.psLivePhoto.onContentActivate(this.photoswipe.currSlide as PsSlide); this.psLivePhoto?.onContentActivate(
this.photoswipe!.currSlide as PsSlide
);
}, },
/** Is the current photo a favorite */ /** Is the current photo a favorite */
@ -977,7 +984,7 @@ export default defineComponent({
/** Favorite the current photo */ /** Favorite the current photo */
async favoriteCurrent() { async favoriteCurrent() {
const photo = this.currentPhoto; const photo = this.currentPhoto!;
const val = !this.isFavorite(); const val = !this.isFavorite();
try { try {
this.updateLoading(1); this.updateLoading(1);
@ -1014,7 +1021,7 @@ export default defineComponent({
/** Open the sidebar */ /** Open the sidebar */
async openSidebar(photo?: IPhoto) { async openSidebar(photo?: IPhoto) {
globalThis.mSidebar.setTab("memories-metadata"); globalThis.mSidebar.setTab("memories-metadata");
photo ||= this.currentPhoto; photo ??= this.currentPhoto!;
if (this.routeIsPublic) { if (this.routeIsPublic) {
globalThis.mSidebar.open(photo.fileid); globalThis.mSidebar.open(photo.fileid);
@ -1052,7 +1059,7 @@ export default defineComponent({
closeSidebar() { closeSidebar() {
this.hideSidebar(); this.hideSidebar();
this.sidebarOpen = false; this.sidebarOpen = false;
this.photoswipe.updateSize(); this.photoswipe?.updateSize();
}, },
/** Toggle the sidebar visibility */ /** Toggle the sidebar visibility */
@ -1101,7 +1108,7 @@ export default defineComponent({
// If this is a video, wait for it to finish // If this is a video, wait for it to finish
if (this.isVideo) { if (this.isVideo) {
// Get active video element // Get active video element
const video: HTMLVideoElement = this.photoswipe?.element?.querySelector( const video = this.photoswipe?.element?.querySelector<HTMLVideoElement>(
".pswp__item.active video" ".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 // no need to set the timer again, since next
// calls resetSlideshowTimer anyway // calls resetSlideshowTimer anyway
}, },

View File

@ -4,10 +4,15 @@ import { IPhoto } from "../../types";
type PsAugment = { type PsAugment = {
data: _SlideData & { 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 PsContent = Content & PsAugment;
export type PsEvent = { export type PsEvent = {
content: PsContent; content: PsContent;

View File

@ -1,5 +1,3 @@
/// <reference types="@nextcloud/typings" />
import "reflect-metadata"; import "reflect-metadata";
import Vue from "vue"; import Vue from "vue";
import VueVirtualScroller from "vue-virtual-scroller"; import VueVirtualScroller from "vue-virtual-scroller";
@ -10,11 +8,11 @@ import GlobalMixin from "./mixins/GlobalMixin";
import App from "./App.vue"; import App from "./App.vue";
import Admin from "./Admin.vue"; import Admin from "./Admin.vue";
import router from "./router"; import router from "./router";
import { Route } from "vue-router";
import { generateFilePath } from "@nextcloud/router"; import { generateFilePath } from "@nextcloud/router";
import { getRequestToken } from "@nextcloud/auth"; 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 PlyrType from "plyr";
import type videojsType from "video.js"; import type videojsType from "video.js";
@ -61,7 +59,7 @@ globalThis.windowInnerWidth = window.innerWidth;
globalThis.windowInnerHeight = window.innerHeight; globalThis.windowInnerHeight = window.innerHeight;
// CSP config for webpack dynamic chunk loading // 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 // Correct the root of the app for chunk loading
// OC.linkTo matches the apps folders // OC.linkTo matches the apps folders
@ -71,21 +69,17 @@ __webpack_public_path__ = generateFilePath("memories", "", "js/");
// Generate client id for this instance // Generate client id for this instance
// Does not need to be cryptographically secure // Does not need to be cryptographically secure
const getClientId = () => const getClientId = (): string =>
Math.random().toString(36).substring(2, 15).padEnd(12, "0"); Math.random().toString(36).substring(2, 15).padEnd(12, "0");
globalThis.videoClientId = getClientId(); globalThis.videoClientId = getClientId();
globalThis.videoClientIdPersistent = localStorage.getItem( globalThis.videoClientIdPersistent =
"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.use(VueVirtualScroller);
Vue.component("XImg", XImg); Vue.component("XImg", XImg);

View File

@ -1,5 +1,5 @@
import { generateUrl } from "@nextcloud/router"; 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 Router from "vue-router";
import Vue from "vue"; import Vue from "vue";
import Timeline from "./components/Timeline.vue"; import Timeline from "./components/Timeline.vue";

View File

@ -1,32 +1,35 @@
import { precacheAndRoute } from "workbox-precaching"; import { precacheAndRoute } from "workbox-precaching";
import { NetworkFirst, CacheFirst, NetworkOnly } from "workbox-strategies"; import { NetworkFirst, CacheFirst } from "workbox-strategies";
import { registerRoute } from "workbox-routing"; import { registerRoute } from "workbox-routing";
import { ExpirationPlugin } from "workbox-expiration"; import { ExpirationPlugin } from "workbox-expiration";
precacheAndRoute(self.__WB_MANIFEST); precacheAndRoute(self.__WB_MANIFEST);
registerRoute(/^.*\/apps\/memories\/api\/video\/livephoto\/.*/, new CacheFirst({ registerRoute(
cacheName: "livephotos", /^.*\/apps\/memories\/api\/video\/livephoto\/.*/,
plugins: [ new CacheFirst({
new ExpirationPlugin({ cacheName: "livephotos",
maxAgeSeconds: 3600 * 24 * 7, // days plugins: [
maxEntries: 1000, // 1k videos new ExpirationPlugin({
}), maxAgeSeconds: 3600 * 24 * 7, // days
], maxEntries: 1000, // 1k videos
})); }),
],
})
);
// Important: Using the NetworkOnly strategy and not registering // Important: Using the NetworkOnly strategy and not registering
// a route are NOT equivalent. The NetworkOnly strategy will // a route are NOT equivalent. The NetworkOnly strategy will
// strip certain headers such as HTTP-Range, which is required // strip certain headers such as HTTP-Range, which is required
// for proper playback of videos. // for proper playback of videos.
const networkOnly = [ const networkOnly = [/^.*\/apps\/memories\/api\/.*/];
/^.*\/apps\/memories\/api\/.*/,
];
// Cache pages for same-origin requests only // Cache pages for same-origin requests only
registerRoute( 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({ new NetworkFirst({
cacheName: "pages", cacheName: "pages",
plugins: [ plugins: [

View File

@ -45,11 +45,11 @@ export class API {
if (typeof query === "object") { if (typeof query === "object") {
// Clean up undefined and null // Clean up undefined and null
Object.keys(query).forEach((key) => { for (const key of Object.keys(query)) {
if (query[key] === undefined || query[key] === null) { if (query[key] === undefined || query[key] === null) {
delete query[key]; delete query[key];
} }
}); }
// Check if nothing in query // Check if nothing in query
if (!Object.keys(query).length) return url; if (!Object.keys(query).length) return url;
@ -138,7 +138,7 @@ export class API {
return gen(`${BASE}/image/set-exif/{id}`, { id }); 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 })); return tok(API.Q(gen(`${BASE}/image/decodable/{id}`, { id }), { etag }));
} }

View File

@ -104,7 +104,7 @@ export async function* removeFromAlbum(
} catch (e) { } catch (e) {
showError( showError(
t("memories", "Failed to remove {filename}.", { t("memories", "Failed to remove {filename}.", {
filename: f.basename, filename: f.basename ?? f.fileid,
}) })
); );
return 0; return 0;

View File

@ -60,7 +60,7 @@ export async function getFiles(photos: IPhoto[]): Promise<IFileInfo[]> {
const fileIds = photos.map((photo) => photo.fileid); const fileIds = photos.map((photo) => photo.fileid);
// Divide fileIds into chunks of GET_FILE_CHUNK_SIZE // 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) { for (let i = 0; i < fileIds.length; i += GET_FILE_CHUNK_SIZE) {
chunks.push(fileIds.slice(i, i + GET_FILE_CHUNK_SIZE)); chunks.push(fileIds.slice(i, i + GET_FILE_CHUNK_SIZE));
} }

View File

@ -70,7 +70,7 @@ export async function* removeFaceImages(
console.error(e); console.error(e);
showError( showError(
t("memories", "Failed to remove {filename} from face.", { t("memories", "Failed to remove {filename} from face.", {
filename: f.basename, filename: f.basename ?? f.fileid,
}) })
); );
return 0; return 0;

View File

@ -62,7 +62,7 @@ export async function* favoritePhotos(
const calls = fileInfos.map((fileInfo) => async () => { const calls = fileInfos.map((fileInfo) => async () => {
try { try {
await favoriteFile(fileInfo.originalFilename, favoriteState); 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) { if (favoriteState) {
photo.flag |= utils.constants.c.FLAG_IS_FAVORITE; photo.flag |= utils.constants.c.FLAG_IS_FAVORITE;
} else { } else {

View File

@ -57,7 +57,7 @@ export async function getOnThisDayData(): Promise<IDay[]> {
// Add to last day // Add to last day
const day = ans[ans.length - 1]; const day = ans[ans.length - 1];
day.detail.push(photo); day.detail!.push(photo);
day.count++; day.count++;
} }

View File

@ -1,7 +1,7 @@
import { IDay } from "../../types"; import { IDay } from "../../types";
import { loadState } from "@nextcloud/initial-state"; import { loadState } from "@nextcloud/initial-state";
let singleItem = null; let singleItem: any;
try { try {
singleItem = loadState("memories", "single_item", {}); singleItem = loadState("memories", "single_item", {});
} catch (e) { } catch (e) {

View File

@ -5,7 +5,9 @@ const config_facerecognitionEnabled = Boolean(
loadState("memories", "facerecognitionEnabled", <string>"") loadState("memories", "facerecognitionEnabled", <string>"")
); );
export function emptyDescription(routeName: string): string { type RouteNameType = string | null | undefined;
export function emptyDescription(routeName: RouteNameType): string {
switch (routeName) { switch (routeName) {
case "timeline": case "timeline":
return t( 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) { switch (routeName) {
case "timeline": case "timeline":
return t("memories", "Your Timeline"); return t("memories", "Your Timeline");

View File

@ -86,7 +86,7 @@ export function randomSubarray(arr: any[], size: number) {
export function setRenewingTimeout( export function setRenewingTimeout(
ctx: any, ctx: any,
name: string, name: string,
callback: () => void | null, callback: (() => void) | null,
delay: number delay: number
) { ) {
if (ctx[name]) window.clearTimeout(ctx[name]); if (ctx[name]) window.clearTimeout(ctx[name]);

View File

@ -35,13 +35,13 @@ export async function openCache() {
} }
/** Get data from the cache */ /** 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; if (!window.caches) return null;
const cache = staticCache || (await openCache()); const cache = staticCache || (await openCache());
if (!cache) return null; if (!cache) return null;
const cachedResponse = await cache.match(url); const cachedResponse = await cache.match(url);
if (!cachedResponse || !cachedResponse.ok) return undefined; if (!cachedResponse || !cachedResponse.ok) return null;
return await cachedResponse.json(); return await cachedResponse.json();
} }

View File

@ -6,7 +6,7 @@ export type IFileInfo = {
/** Full file name, e.g. /pi/test/Qx0dq7dvEXA.jpg */ /** Full file name, e.g. /pi/test/Qx0dq7dvEXA.jpg */
filename: string; filename: string;
/** Original file name, e.g. /files/admin/pi/test/Qx0dq7dvEXA.jpg */ /** Original file name, e.g. /files/admin/pi/test/Qx0dq7dvEXA.jpg */
originalFilename?: string; originalFilename: string;
/** Base name of file e.g. Qx0dq7dvEXA.jpg */ /** Base name of file e.g. Qx0dq7dvEXA.jpg */
basename: string; basename: string;
}; };
@ -36,7 +36,7 @@ export type IPhoto = {
/** Bit flags */ /** Bit flags */
flag: number; flag: number;
/** DayID from server */ /** DayID from server */
dayid?: number; dayid: number;
/** Width of full image */ /** Width of full image */
w?: number; w?: number;
/** Height of full image */ /** Height of full image */
@ -58,7 +58,7 @@ export type IPhoto = {
/** Reference to day object */ /** Reference to day object */
d?: IDay; d?: IDay;
/** Reference to exif object */ /** Reference to exif object */
imageInfo?: IImageInfo; imageInfo?: IImageInfo | null;
/** Face detection ID */ /** Face detection ID */
faceid?: number; faceid?: number;
@ -174,7 +174,7 @@ export type IRow = {
photos?: IPhoto[]; photos?: IPhoto[];
/** Height in px of the row */ /** Height in px of the row */
size?: number; size: number;
/** Count of placeholders to create */ /** Count of placeholders to create */
pct?: number; pct?: number;
/** Don't remove dom element */ /** Don't remove dom element */

18
src/vue-globals.d.ts vendored
View File

@ -1,11 +1,11 @@
import { constants } from "./services/Utils"; import { type constants } from "./services/Utils";
import { translate as t, translatePlural as n } from "@nextcloud/l10n"; import type { translate, translatePlural } from "@nextcloud/l10n";
declare module "vue" { declare module "vue" {
interface ComponentCustomProperties { interface ComponentCustomProperties {
// GlobalMixin.ts // GlobalMixin.ts
t: typeof t; t: typeof translate;
n: typeof n; n: typeof translatePlural;
c: typeof constants.c; c: typeof constants.c;
@ -31,14 +31,8 @@ declare module "vue" {
config_albumListSort: 1 | 2; config_albumListSort: 1 | 2;
config_eventName: string; config_eventName: string;
updateSetting(setting: string): Promise<void>; updateSetting: (setting: string) => Promise<void>;
updateLocalSetting({ updateLocalSetting: (opts: { setting: string; value: any }) => void;
setting,
value,
}: {
setting: string;
value: any;
}): void;
} }
} }

2
src/vue-shims.d.ts vendored
View File

@ -1,5 +1,5 @@
declare module "*.vue" { declare module "*.vue" {
import { defineComponent } from "vue"; import type { defineComponent } from "vue";
const Component: ReturnType<typeof defineComponent>; const Component: ReturnType<typeof defineComponent>;
export default Component; export default Component;
} }

View File

@ -1,9 +1,17 @@
/** Set the receiver function for a worker */ /** Set the receiver function for a worker */
export function workerExport(handlers: { export function workerExport(
[key: string]: (...data: any) => Promise<any>; handlers: Record<string, (...data: any) => Promise<any>>
}) { ): void {
/** Promise API for web worker */ /** Promise API for web worker */
self.onmessage = async ({ data }) => { self.onmessage = async ({
data,
}: {
data: {
id: number;
name: string;
args: any[];
};
}) => {
try { try {
const handler = handlers[data.name]; const handler = handlers[data.name];
if (!handler) throw new Error(`No handler for type ${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. */ /** Get the CALL function for a worker. Call this only once. */
export function workerImporter(worker: Worker) { export function workerImporter(worker: Worker) {
const promises: { [id: string]: any } = {}; const promises = new Map<number, { resolve: any; reject: any }>();
worker.onmessage = ({ data }: { data: any }) => { worker.onmessage = ({ data }: { data: any }) => {
const { id, resolve, reject } = data; const { id, resolve, reject } = data;
if (resolve) promises[id].resolve(resolve); if (resolve) promises.get(id)?.resolve(resolve);
if (reject) promises[id].reject(reject); if (reject) promises.get(id)?.reject(reject);
delete promises[id]; promises.delete(id);
}; };
return function importer<F extends (...args: any) => Promise<any>>(
name: string type PromiseFun = (...args: any) => Promise<any>;
): (...args: Parameters<F>) => ReturnType<F> { return function importer<F extends PromiseFun>(name: string) {
return function fun(...args: any) { return async function fun(...args: Parameters<F>) {
return new Promise((resolve, reject) => { return await new Promise<ReturnType<Awaited<F>>>((resolve, reject) => {
const id = Math.random(); const id = Math.random();
promises[id] = { resolve, reject }; promises.set(id, { resolve, reject });
worker.postMessage({ id, name, args }); worker.postMessage({ id, name, args });
}); });
} as any; };
}; };
} }

View File

@ -9,7 +9,8 @@
"jsx": "preserve", "jsx": "preserve",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"noImplicitThis": true, "noImplicitThis": true,
"esModuleInterop": true "esModuleInterop": true,
"strictNullChecks": true
}, },
"vueCompilerOptions": { "vueCompilerOptions": {
"target": 2.7 "target": 2.7