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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -36,7 +36,7 @@ import { defineComponent, PropType } from "vue";
import { getCurrentUser } from "@nextcloud/auth";
import NcCounterBubble from "@nextcloud/vue/dist/Components/NcCounterBubble";
import { IAlbum, ICluster, IFace } from "../../types";
import type { IAlbum, ICluster, IFace, IPhoto } from "../../types";
import { getPreviewUrl } from "../../services/utils/helpers";
import errorsvg from "../../assets/error.svg";
@ -66,7 +66,11 @@ export default defineComponent({
if (this.error) return errorsvg;
if (this.album) {
const mock = { fileid: this.album.last_added_photo, etag: "", flag: 0 };
const mock = {
fileid: this.album.last_added_photo,
etag: this.album.album_id,
flag: 0,
} as unknown as IPhoto;
return getPreviewUrl(mock, true, 512);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -176,16 +176,18 @@ export default defineComponent({
},
dateDiff() {
return this.date.getTime() - this.dateLast.getTime();
return this.date && this.dateLast
? this.date.getTime() - this.dateLast.getTime()
: 0;
},
origDateNewest() {
return new Date(this.sortedPhotos[0].datetaken);
return new Date(this.sortedPhotos[0].datetaken!);
},
origDateOldest() {
return new Date(
this.sortedPhotos[this.sortedPhotos.length - 1].datetaken
this.sortedPhotos[this.sortedPhotos.length - 1].datetaken!
);
},
@ -212,13 +214,16 @@ export default defineComponent({
methods: {
init() {
const photos = (this.sortedPhotos = [...this.photos] as IPhoto[]);
// Filter out only photos that have a datetaken
const photos = (this.sortedPhotos = this.photos.filter(
(photo) => photo.datetaken !== undefined
));
// Sort photos by datetaken descending
photos.sort((a, b) => b.datetaken - a.datetaken);
photos.sort((a, b) => b.datetaken! - a.datetaken!);
// Get date of newest photo
let date = new Date(photos[0].datetaken);
let date = new Date(photos[0].datetaken!);
this.year = date.getUTCFullYear().toString();
this.month = (date.getUTCMonth() + 1).toString();
this.day = date.getUTCDate().toString();
@ -228,7 +233,7 @@ export default defineComponent({
// Get date of oldest photo
if (photos.length > 1) {
date = new Date(photos[photos.length - 1].datetaken);
date = new Date(photos[photos.length - 1].datetaken!);
this.yearLast = date.getUTCFullYear().toString();
this.monthLast = (date.getUTCMonth() + 1).toString();
this.dayLast = date.getUTCDate().toString();
@ -262,7 +267,7 @@ export default defineComponent({
return undefined;
}
if (this.sortedPhotos.length === 0) {
if (this.sortedPhotos.length === 0 || !this.date) {
return undefined;
}
@ -278,7 +283,7 @@ export default defineComponent({
},
newestChange(time = false) {
if (this.sortedPhotos.length === 0) {
if (this.sortedPhotos.length === 0 || !this.date) {
return;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -130,7 +130,8 @@ export default defineComponent({
}
});
for await (const resp of dav.runInParallel(calls, 10)) {
this.moved(resp);
const valid = resp.filter((r): r is IPhoto => r !== undefined);
this.moved(valid);
}
} catch (error) {
console.error(error);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,10 +4,15 @@ import { IPhoto } from "../../types";
type PsAugment = {
data: _SlideData & {
photo?: IPhoto;
src: string;
msrc: string;
photo: IPhoto;
};
};
export type PsSlide = Slide & PsAugment;
export type PsSlide = Slide &
PsAugment & {
content: PsContent;
};
export type PsContent = Content & PsAugment;
export type PsEvent = {
content: PsContent;

View File

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

View File

@ -1,5 +1,5 @@
import { generateUrl } from "@nextcloud/router";
import { translate as t, translatePlural as n } from "@nextcloud/l10n";
import { translate as t } from "@nextcloud/l10n";
import Router from "vue-router";
import Vue from "vue";
import Timeline from "./components/Timeline.vue";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

View File

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